diff --git a/scripts/test-compose.sh b/scripts/test-compose.sh new file mode 100644 index 00000000..89d4bce6 --- /dev/null +++ b/scripts/test-compose.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# ============================================================================= +# HomeLab Stack — Docker Compose Syntax & Config Validation +# Validates all stack compose files without starting containers +# Usage: ./scripts/test-compose.sh [--fix] +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASE_DIR="$SCRIPT_DIR/.." +STACKS_DIR="$BASE_DIR/stacks" + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +PASSED=0; FAILED=0; WARNINGS=0 + +pass() { echo -e " ${GREEN}PASS${NC} $1"; ((PASSED++)); } +fail() { echo -e " ${RED}FAIL${NC} $1"; ((FAILED++)); } +warn() { echo -e " ${YELLOW}WARN${NC} $1"; ((WARNINGS++)); } + +# Check docker compose is available +if ! command -v docker &>/dev/null; then + echo "docker not found — skipping compose validation" + echo "Install Docker to run full validation" + exit 0 +fi + +echo "============================================" +echo " HomeLab Stack — Compose Validation" +echo "============================================" + +# 1. Syntax validation for all compose files +echo "" +echo "[1] Docker Compose syntax validation" +for compose in "$STACKS_DIR"/*/docker-compose*.yml; do + stack=$(basename "$(dirname "$compose")") + if docker compose -f "$compose" config --quiet 2>/dev/null; then + pass "$stack: valid syntax" + else + # Try with env subst fallback + if DOMAIN=test.example.com TZ=UTC \ + docker compose -f "$compose" config >/dev/null 2>&1; then + pass "$stack: valid syntax (with env defaults)" + else + fail "$stack: invalid syntax — $(docker compose -f "$compose" config 2>&1 | head -1)" + fi + fi +done + +# 2. Image tag pinning check (no :latest) +echo "" +echo "[2] Image tag pinning (no ':latest')" +for compose in "$STACKS_DIR"/*/docker-compose*.yml; do + stack=$(basename "$(dirname "$compose")") + latest_count=$(grep -c 'image:.*:latest' "$compose" 2>/dev/null || true) + if [[ $latest_count -eq 0 ]]; then + pass "$stack: all images pinned" + else + fail "$stack: $latest_count image(s) using ':latest' tag" + fi +done + +# 3. Network validation +echo "" +echo "[3] Network configuration" +for compose in "$STACKS_DIR"/*/docker-compose*.yml; do + stack=$(basename "$(dirname "$compose")") + if grep -q 'external: true' "$compose" 2>/dev/null; then + # Check proxy network is referenced + if grep -q 'proxy' "$compose"; then + pass "$stack: proxy network configured" + else + warn "$stack: external network but no proxy reference" + fi + fi +done + +# 4. Health check presence +echo "" +echo "[4] Health check coverage" +for compose in "$STACKS_DIR"/*/docker-compose*.yml; do + stack=$(basename "$(dirname "$compose")") + total=$(grep -c 'image:' "$compose" 2>/dev/null || true) + health=$(grep -c 'healthcheck:' "$compose" 2>/dev/null || true) + if [[ $total -gt 0 ]] && [[ $health -ge $total ]]; then + pass "$stack: $health/$total services have health checks" + elif [[ $total -gt 0 ]] && [[ $health -gt 0 ]]; then + warn "$stack: $health/$total services have health checks" + elif [[ $total -gt 0 ]]; then + fail "$stack: 0/$total services have health checks" + fi +done + +# 5. Environment variable references +echo "" +echo "[5] Required env var references" +for compose in "$STACKS_DIR"/*/docker-compose*.yml; do + stack=$(basename "$(dirname "$compose")") + # Check DOMAIN is referenced (needed for Traefik labels) + if grep -q '\${DOMAIN' "$compose" 2>/dev/null; then + pass "$stack: uses DOMAIN variable" + fi + # Check for TZ + if grep -q '\${TZ' "$compose" 2>/dev/null; then + pass "$stack: uses TZ variable" + fi +done + +# 6. .env.example existence +echo "" +echo "[6] .env.example files" +for stack_dir in "$STACKS_DIR"/*/; do + stack=$(basename "$stack_dir") + if [[ -f "$stack_dir/.env.example" ]]; then + pass "$stack: .env.example exists" + else + warn "$stack: no .env.example" + fi +done + +# 7. README existence +echo "" +echo "[7] Documentation" +for stack_dir in "$STACKS_DIR"/*/; do + stack=$(basename "$stack_dir") + if [[ -f "$stack_dir/README.md" ]]; then + pass "$stack: README.md exists" + else + warn "$stack: no README.md" + fi +done + +# Summary +echo "" +echo "============================================" +echo " Results: $PASSED passed, $FAILED failed, $WARNINGS warnings" +echo "============================================" + +if [[ $FAILED -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/scripts/test-health.sh b/scripts/test-health.sh new file mode 100644 index 00000000..4a89fe06 --- /dev/null +++ b/scripts/test-health.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# ============================================================================= +# HomeLab Stack — Runtime Health Check Verification +# Tests that all running containers are healthy +# Usage: ./scripts/test-health.sh [stack_name] +# ============================================================================= +set -uo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +PASSED=0; FAILED=0; SKIPPED=0 + +pass() { echo -e " ${GREEN}✓${NC} $1"; ((PASSED++)); } +fail() { echo -e " ${RED}✗${NC} $1"; ((FAILED++)); } +skip() { echo -e " ${YELLOW}~${NC} $1"; ((SKIPPED++)); } + +TARGET_STACK="${1:-all}" + +echo "============================================" +echo " HomeLab Stack — Health Check Tests" +echo "============================================" + +check_container() { + local name=$1 + if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then + skip "$name (not running)" + return + fi + + local health + health=$(docker inspect --format '{{.State.Health.Status}}' "$name" 2>/dev/null || echo "none") + + case "$health" in + healthy) pass "$name: healthy" ;; + unhealthy) fail "$name: UNHEALTHY — $(docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{end}}' "$name" 2>/dev/null)" ;; + none|starting) skip "$name: $health (no healthcheck defined or starting)" ;; + esac +} + +# Base stack +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "base" ]]; then + echo "" + echo "[Base Infrastructure]" + check_container traefik + check_container portainer + check_container watchtower +fi + +# Databases +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "databases" ]]; then + echo "" + echo "[Database Layer]" + check_container homelab-postgres + check_container homelab-redis + check_container homelab-mariadb + check_container homelab-pgadmin +fi + +# Notifications +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "notifications" ]]; then + echo "" + echo "[Notifications]" + check_container ntfy + check_container gotify + check_container apprise +fi + +# Network +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "network" ]]; then + echo "" + echo "[Network]" + check_container adguardhome + check_container wireguard + check_container nginx-proxy-manager +fi + +# SSO +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "sso" ]]; then + echo "" + echo "[SSO]" + check_container authentik-server + check_container authentik-worker +fi + +# Storage +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "storage" ]]; then + echo "" + echo "[Storage]" + check_container nextcloud + check_container minio + check_container filebrowser +fi + +# Productivity +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "productivity" ]]; then + echo "" + echo "[Productivity]" + check_container gitea + check_container vaultwarden + check_container outline + check_container bookstack +fi + +# Media +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "media" ]]; then + echo "" + echo "[Media]" + check_container jellyfin + check_container sonarr + check_container radarr + check_container prowlarr + check_container qbittorrent + check_container jellyseerr +fi + +# Monitoring +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "monitoring" ]]; then + echo "" + echo "[Observability]" + check_container prometheus + check_container grafana + check_container loki + check_container promtail + check_container alertmanager +fi + +# Home Automation +if [[ "$TARGET_STACK" == "all" || "$TARGET_STACK" == "home-automation" ]]; then + echo "" + echo "[Home Automation]" + check_container homeassistant + check_container node-red + check_container mosquitto + check_container zigbee2mqtt +fi + +echo "" +echo "============================================" +echo " Results: $PASSED healthy, $FAILED unhealthy, $SKIPPED not running" +echo "============================================" + +if [[ $FAILED -gt 0 ]]; then + echo "" + echo "Unhealthy containers detected. Check logs:" + echo " docker compose -f stacks//docker-compose.yml logs " + exit 1 +fi +exit 0