Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
120 changes: 120 additions & 0 deletions dream-server/scripts/audit-compose-healthchecks.sh
Original file line number Diff line number Diff line change
@@ -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)

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:[[:space:]]*{}[[:space:]]*$" "$file"; then
missing_stub+=("$rel_path")
continue
fi

# Check if file has healthcheck definition
if grep -q "healthcheck:" "$file"; 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
195 changes: 195 additions & 0 deletions dream-server/tests/test-compose-healthcheck-audit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/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" ]]; 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 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

# 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 ""
echo "Result: $PASSED passed, $FAILED failed"
[[ $FAILED -eq 0 ]]
Loading