From 416ed5bf4934b39b426f635a671e869aeedd48be Mon Sep 17 00:00:00 2001 From: bugman007 Date: Wed, 18 Mar 2026 10:40:15 +0100 Subject: [PATCH 1/3] test: add Docker Compose healthcheck audit and enforcement --- .github/workflows/test-linux.yml | 3 + .../scripts/audit-compose-healthchecks.sh | 120 ++++++++++++++++++ .../tests/test-compose-healthcheck-audit.sh | 118 +++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100755 dream-server/scripts/audit-compose-healthchecks.sh create mode 100755 dream-server/tests/test-compose-healthcheck-audit.sh diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 5de34ffd..b7c601cd 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -51,6 +51,9 @@ jobs: - name: Health Check Tests run: bash tests/test-health-check.sh + - name: Compose Healthcheck Audit Tests + run: bash tests/test-compose-healthcheck-audit.sh + - name: Validate Env Tests run: bash tests/test-validate-env.sh diff --git a/dream-server/scripts/audit-compose-healthchecks.sh b/dream-server/scripts/audit-compose-healthchecks.sh new file mode 100755 index 00000000..a6a30f7f --- /dev/null +++ b/dream-server/scripts/audit-compose-healthchecks.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Audit Docker Compose files for missing healthcheck definitions +# Usage: scripts/audit-compose-healthchecks.sh [--strict] +# +# Returns: +# 0 - All compose files have healthchecks (or only warnings) +# 1 - Missing healthchecks found in production files (strict mode) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +STRICT=false +QUIET=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --strict) + STRICT=true + shift + ;; + --quiet) + QUIET=true + shift + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +if $QUIET; then + RED="" YELLOW="" GREEN="" BLUE="" NC="" +fi + +log() { $QUIET || echo -e "$1"; } + +# Find all compose files +compose_files=() +while IFS= read -r -d '' file; do + compose_files+=("$file") +done < <(find "$ROOT_DIR" -type f \( -name "compose.yaml" -o -name "compose.*.yaml" -o -name "docker-compose*.yml" \) -print0 2>/dev/null) + +if [[ ${#compose_files[@]} -eq 0 ]]; then + log "${YELLOW}No compose files found${NC}" + exit 0 +fi + +log "${BLUE}Auditing ${#compose_files[@]} compose files for healthchecks...${NC}" +log "" + +missing_production=() +missing_local=() +missing_stub=() +has_healthcheck=() + +for file in "${compose_files[@]}"; do + rel_path="${file#$ROOT_DIR/}" + + # Skip if file is a stub (services: {}) + if grep -q "^services:\s*{}\s*$" "$file" 2>/dev/null; then + missing_stub+=("$rel_path") + continue + fi + + # Check if file has healthcheck definition + if grep -q "healthcheck:" "$file" 2>/dev/null; then + has_healthcheck+=("$rel_path") + else + # Categorize by file type + if [[ "$rel_path" == *".local."* ]]; then + missing_local+=("$rel_path") + else + missing_production+=("$rel_path") + fi + fi +done + +# Report results +log "${GREEN}✓ Files with healthchecks: ${#has_healthcheck[@]}${NC}" + +if [[ ${#missing_stub[@]} -gt 0 ]]; then + log "${BLUE}ℹ Stub files (no services): ${#missing_stub[@]}${NC}" +fi + +if [[ ${#missing_local[@]} -gt 0 ]]; then + log "${YELLOW}⚠ Local dev files without healthchecks: ${#missing_local[@]}${NC}" + if ! $QUIET; then + for file in "${missing_local[@]}"; do + echo " - $file" + done + fi +fi + +if [[ ${#missing_production[@]} -gt 0 ]]; then + log "${RED}✗ Production files without healthchecks: ${#missing_production[@]}${NC}" + if ! $QUIET; then + for file in "${missing_production[@]}"; do + echo " - $file" + done + fi + log "" + log "${YELLOW}Recommendation: Add healthcheck definitions to production compose files${NC}" + + if $STRICT; then + exit 1 + fi +fi + +log "" +log "${GREEN}Audit complete${NC}" +exit 0 diff --git a/dream-server/tests/test-compose-healthcheck-audit.sh b/dream-server/tests/test-compose-healthcheck-audit.sh new file mode 100755 index 00000000..1170a2d3 --- /dev/null +++ b/dream-server/tests/test-compose-healthcheck-audit.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# ============================================================================ +# Dream Server audit-compose-healthchecks.sh Test Suite +# ============================================================================ +# Ensures scripts/audit-compose-healthchecks.sh correctly identifies compose +# files with and without healthcheck definitions. Validates the audit tool +# used to enforce healthcheck requirements across extensions. +# +# Usage: ./tests/test-compose-healthcheck-audit.sh +# ============================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' + +PASSED=0 +FAILED=0 + +pass() { echo -e " ${GREEN}✓ PASS${NC} $1"; PASSED=$((PASSED + 1)); } +fail() { echo -e " ${RED}✗ FAIL${NC} $1"; FAILED=$((FAILED + 1)); } +skip() { echo -e " ${YELLOW}⊘ SKIP${NC} $1"; } + +echo "" +echo "╔═══════════════════════════════════════════════╗" +echo "║ Compose Healthcheck Audit Test Suite ║" +echo "╚═══════════════════════════════════════════════╝" +echo "" + +# 1. Script exists +if [[ ! -f "$ROOT_DIR/scripts/audit-compose-healthchecks.sh" ]]; then + fail "scripts/audit-compose-healthchecks.sh not found" + echo ""; echo "Result: $PASSED passed, $FAILED failed"; exit 1 +fi +pass "audit-compose-healthchecks.sh exists" + +# 2. Script runs without errors +set +e +out=$(cd "$ROOT_DIR" && bash scripts/audit-compose-healthchecks.sh --quiet 2>&1) +exit_code=$? +set -e + +if echo "$out" | grep -q "unbound variable\|syntax error\|command not found"; then + fail "audit-compose-healthchecks.sh produced shell error" +else + pass "audit-compose-healthchecks.sh runs without shell errors" +fi + +# 3. Exit code is valid (0 or 1) +if [[ "$exit_code" -eq 0 ]] || [[ "$exit_code" -eq 1 ]]; then + pass "audit-compose-healthchecks.sh exit code is valid (0|1): $exit_code" +else + fail "audit-compose-healthchecks.sh exit code should be 0 or 1; got $exit_code" +fi + +# 4. Script finds compose files +set +e +out=$(cd "$ROOT_DIR" && bash scripts/audit-compose-healthchecks.sh 2>&1) +set -e + +if echo "$out" | grep -q "Auditing.*compose files"; then + pass "audit-compose-healthchecks.sh finds compose files" +else + fail "audit-compose-healthchecks.sh did not report compose file count" +fi + +# 5. Script reports files with healthchecks +if echo "$out" | grep -q "Files with healthchecks:"; then + pass "audit-compose-healthchecks.sh reports files with healthchecks" +else + fail "audit-compose-healthchecks.sh missing healthcheck report" +fi + +# 6. Script identifies production files without healthchecks +if echo "$out" | grep -q "Production files without healthchecks:"; then + pass "audit-compose-healthchecks.sh identifies production files without healthchecks" +else + skip "No production files without healthchecks found (good!)" +fi + +# 7. --strict flag works +set +e +bash "$ROOT_DIR/scripts/audit-compose-healthchecks.sh" --strict --quiet 2>&1 +strict_exit=$? +set -e + +# In strict mode, should exit 1 if production files are missing healthchecks +if [[ "$strict_exit" -eq 0 ]] || [[ "$strict_exit" -eq 1 ]]; then + pass "audit-compose-healthchecks.sh --strict flag works (exit: $strict_exit)" +else + fail "audit-compose-healthchecks.sh --strict produced unexpected exit code: $strict_exit" +fi + +# 8. --quiet flag suppresses output +set +e +quiet_out=$(cd "$ROOT_DIR" && bash scripts/audit-compose-healthchecks.sh --quiet 2>&1) +set -e + +# Quiet mode should have minimal output (no color codes, less verbose) +if [[ $(echo "$quiet_out" | wc -l) -lt 10 ]]; then + pass "audit-compose-healthchecks.sh --quiet reduces output" +else + skip "audit-compose-healthchecks.sh --quiet output check (may vary)" +fi + +# 9. Script is executable or runnable via bash +if [[ -x "$ROOT_DIR/scripts/audit-compose-healthchecks.sh" ]] || true; then + pass "audit-compose-healthchecks.sh is runnable" +fi + +echo "" +echo "Result: $PASSED passed, $FAILED failed" +[[ $FAILED -eq 0 ]] From a7d68846be076a324687e02b21d7671464e3fb24 Mon Sep 17 00:00:00 2001 From: bugman007 Date: Wed, 18 Mar 2026 13:11:56 +0100 Subject: [PATCH 2/3] fix: address review feedback for compose healthcheck audit --- .../scripts/audit-compose-healthchecks.sh | 6 +-- .../tests/test-compose-healthcheck-audit.sh | 48 ++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/dream-server/scripts/audit-compose-healthchecks.sh b/dream-server/scripts/audit-compose-healthchecks.sh index a6a30f7f..3448e4d4 100755 --- a/dream-server/scripts/audit-compose-healthchecks.sh +++ b/dream-server/scripts/audit-compose-healthchecks.sh @@ -47,7 +47,7 @@ log() { $QUIET || echo -e "$1"; } compose_files=() while IFS= read -r -d '' file; do compose_files+=("$file") -done < <(find "$ROOT_DIR" -type f \( -name "compose.yaml" -o -name "compose.*.yaml" -o -name "docker-compose*.yml" \) -print0 2>/dev/null) +done < <(find "$ROOT_DIR" -type f \( -name "compose.yaml" -o -name "compose.*.yaml" -o -name "docker-compose*.yml" \) -print0) if [[ ${#compose_files[@]} -eq 0 ]]; then log "${YELLOW}No compose files found${NC}" @@ -66,13 +66,13 @@ for file in "${compose_files[@]}"; do rel_path="${file#$ROOT_DIR/}" # Skip if file is a stub (services: {}) - if grep -q "^services:\s*{}\s*$" "$file" 2>/dev/null; then + if grep -q "^services:[[:space:]]*{}[[:space:]]*$" "$file"; then missing_stub+=("$rel_path") continue fi # Check if file has healthcheck definition - if grep -q "healthcheck:" "$file" 2>/dev/null; then + if grep -q "healthcheck:" "$file"; then has_healthcheck+=("$rel_path") else # Categorize by file type diff --git a/dream-server/tests/test-compose-healthcheck-audit.sh b/dream-server/tests/test-compose-healthcheck-audit.sh index 1170a2d3..ecfccf0b 100755 --- a/dream-server/tests/test-compose-healthcheck-audit.sh +++ b/dream-server/tests/test-compose-healthcheck-audit.sh @@ -109,8 +109,52 @@ else fi # 9. Script is executable or runnable via bash -if [[ -x "$ROOT_DIR/scripts/audit-compose-healthchecks.sh" ]] || true; then - pass "audit-compose-healthchecks.sh is runnable" +if [[ -x "$ROOT_DIR/scripts/audit-compose-healthchecks.sh" ]]; then + pass "audit-compose-healthchecks.sh is executable" +else + # Still runnable via bash even if not executable + pass "audit-compose-healthchecks.sh is runnable via bash" +fi + +# 10. Behavioral test: Create temp compose file and verify detection +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +# Create compose file WITHOUT healthcheck +cat > "$TEMP_DIR/test-no-healthcheck.yml" <<'EOF' +services: + test-service: + image: nginx:latest + ports: + - "8080:80" +EOF + +# Create compose file WITH healthcheck +cat > "$TEMP_DIR/test-with-healthcheck.yml" <<'EOF' +services: + test-service: + image: nginx:latest + ports: + - "8080:80" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 30s + timeout: 10s + retries: 3 +EOF + +# Test detection of file without healthcheck +if grep -q "healthcheck:" "$TEMP_DIR/test-no-healthcheck.yml"; then + fail "Behavioral test: false positive on file without healthcheck" +else + pass "Behavioral test: correctly identifies file without healthcheck" +fi + +# Test detection of file with healthcheck +if grep -q "healthcheck:" "$TEMP_DIR/test-with-healthcheck.yml"; then + pass "Behavioral test: correctly identifies file with healthcheck" +else + fail "Behavioral test: false negative on file with healthcheck" fi echo "" From b3ecc1640c0dcd8dae4963d97bbe4b98ce359167 Mon Sep 17 00:00:00 2001 From: bugman007 Date: Thu, 19 Mar 2026 02:10:57 +0100 Subject: [PATCH 3/3] fix: address PR #376 review feedback --- .../tests/test-compose-healthcheck-audit.sh | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/dream-server/tests/test-compose-healthcheck-audit.sh b/dream-server/tests/test-compose-healthcheck-audit.sh index ecfccf0b..62388528 100755 --- a/dream-server/tests/test-compose-healthcheck-audit.sh +++ b/dream-server/tests/test-compose-healthcheck-audit.sh @@ -117,6 +117,39 @@ else fi # 10. Behavioral test: Create temp compose file and verify detection +temp_dir=$(mktemp -d) +trap 'rm -rf "$temp_dir"' EXIT + +# Create compose file WITH healthcheck +cat > "$temp_dir/compose-with-health.yaml" << 'EOF' +services: + test-service: + image: nginx + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 30s +EOF + +# Create compose file WITHOUT healthcheck +cat > "$temp_dir/compose-no-health.yaml" << 'EOF' +services: + test-service: + image: nginx +EOF + +# Run audit on temp directory +cd "$temp_dir" +audit_output=$(bash "$ROOT_DIR/scripts/audit-compose-healthchecks.sh" 2>&1) + +# Verify detection +if echo "$audit_output" | grep -q "compose-with-health.yaml" && \ + echo "$audit_output" | grep -q "compose-no-health.yaml"; then + pass "Behavioral test: audit correctly detects healthcheck presence/absence" +else + fail "Behavioral test: audit failed to detect healthcheck status" +fi + +cd "$ROOT_DIR" TEMP_DIR=$(mktemp -d) trap 'rm -rf "$TEMP_DIR"' EXIT