#!/bin/bash # Validation functions for Mosaic Stack installer # Post-install validation and health checks # shellcheck source=lib/platform.sh source "${BASH_SOURCE[0]%/*}/platform.sh" # ============================================================================ # Validation Result Codes # ============================================================================ readonly CHECK_PASS=0 readonly CHECK_WARN=1 readonly CHECK_FAIL=2 # ============================================================================ # Port Validation # ============================================================================ # Check if a port is in use check_port_in_use() { local port="$1" # Try ss first (most common on modern Linux) if command -v ss &>/dev/null; then ss -tuln 2>/dev/null | grep -q ":${port} " return $? fi # Fall back to netstat if command -v netstat &>/dev/null; then netstat -tuln 2>/dev/null | grep -q ":${port} " return $? fi # Fall back to lsof if command -v lsof &>/dev/null; then lsof -i ":$port" &>/dev/null return $? fi # Can't check, assume port is free return 1 } # Get process using a port get_process_on_port() { local port="$1" if command -v lsof &>/dev/null; then lsof -i ":$port" -t 2>/dev/null | head -1 elif command -v ss &>/dev/null; then ss -tulnp 2>/dev/null | grep ":${port} " | grep -oP 'pid=\K[0-9]+' | head -1 else echo "unknown" fi } # Validate port number validate_port() { local port="$1" if [[ "$port" =~ ^[0-9]+$ ]] && [[ "$port" -ge 1 ]] && [[ "$port" -le 65535 ]]; then return 0 fi return 1 } # Check all configured ports check_all_ports() { local env_file="${1:-.env}" local errors=0 local warnings=0 # Load env file if it exists if [[ -f "$env_file" ]]; then set -a # shellcheck source=/dev/null source "$env_file" 2>/dev/null || true set +a fi # Default ports declare -A default_ports=( [WEB_PORT]=3000 [API_PORT]=3001 [POSTGRES_PORT]=5432 [VALKEY_PORT]=6379 [AUTHENTIK_PORT_HTTP]=9000 [AUTHENTIK_PORT_HTTPS]=9443 [OLLAMA_PORT]=11434 [TRAEFIK_HTTP_PORT]=80 [TRAEFIK_HTTPS_PORT]=443 [TRAEFIK_DASHBOARD_PORT]=8080 ) echo -e "${BOLD}Checking ports...${NC}" echo "" for port_var in "${!default_ports[@]}"; do local port="${!port_var:-${default_ports[$port_var]}}" if check_port_in_use "$port"; then local process process=$(get_process_on_port "$port") echo -e "${WARN}⚠${NC} $port_var: Port $port is in use (PID: $process)" ((warnings++)) else echo -e "${SUCCESS}✓${NC} $port_var: Port $port available" fi done echo "" if [[ $warnings -gt 0 ]]; then return $CHECK_WARN fi return $CHECK_PASS } # ============================================================================ # Environment Validation # ============================================================================ # Required environment variables REQUIRED_ENV_VARS=( "DATABASE_URL" "JWT_SECRET" "BETTER_AUTH_SECRET" "ENCRYPTION_KEY" ) # Optional but recommended environment variables RECOMMENDED_ENV_VARS=( "POSTGRES_PASSWORD" "VALKEY_URL" "NEXT_PUBLIC_API_URL" "NEXT_PUBLIC_APP_URL" ) # Check if env file exists check_env_file() { local env_file="${1:-.env}" if [[ -f "$env_file" ]]; then echo -e "${SUCCESS}✓${NC} .env file exists" return 0 else echo -e "${ERROR}✗${NC} .env file not found" return $CHECK_FAIL fi } # Check required environment variables check_required_env() { local env_file="${1:-.env}" local errors=0 echo -e "${BOLD}Checking required environment variables...${NC}" echo "" # Load env file if [[ -f "$env_file" ]]; then set -a # shellcheck source=/dev/null source "$env_file" 2>/dev/null || true set +a fi for var in "${REQUIRED_ENV_VARS[@]}"; do local value="${!var:-}" if [[ -z "$value" ]]; then echo -e "${ERROR}✗${NC} $var: Not set" ((errors++)) elif is_placeholder "$value"; then echo -e "${WARN}⚠${NC} $var: Contains placeholder value" ((errors++)) else echo -e "${SUCCESS}✓${NC} $var: Set" fi done echo "" if [[ $errors -gt 0 ]]; then return $CHECK_FAIL fi return $CHECK_PASS } # Check recommended environment variables check_recommended_env() { local env_file="${1:-.env}" local warnings=0 echo -e "${BOLD}Checking recommended environment variables...${NC}" echo "" # Load env file if [[ -f "$env_file" ]]; then set -a # shellcheck source=/dev/null source "$env_file" 2>/dev/null || true set +a fi for var in "${RECOMMENDED_ENV_VARS[@]}"; do local value="${!var:-}" if [[ -z "$value" ]]; then echo -e "${WARN}⚠${NC} $var: Not set (using default)" ((warnings++)) elif is_placeholder "$value"; then echo -e "${WARN}⚠${NC} $var: Contains placeholder value" ((warnings++)) else echo -e "${SUCCESS}✓${NC} $var: Set" fi done echo "" if [[ $warnings -gt 0 ]]; then return $CHECK_WARN fi return $CHECK_PASS } # Check if a value is a placeholder is_placeholder() { local value="$1" if [[ -z "$value" ]]; then return 0 fi # Common placeholder patterns case "$value" in *"REPLACE_WITH"*|*"CHANGE_ME"*|*"changeme"*|*"your-"*|*"example"*|*"placeholder"*|*"TODO"*|*"FIXME"*) return 0 ;; *"xxx"*|*"<"*">"*|*"\${"*|*"$${"*) return 0 ;; esac return 1 } # ============================================================================ # Secret Validation # ============================================================================ # Minimum secret lengths declare -A MIN_SECRET_LENGTHS=( [JWT_SECRET]=32 [BETTER_AUTH_SECRET]=32 [ENCRYPTION_KEY]=64 [AUTHENTIK_SECRET_KEY]=50 [COORDINATOR_API_KEY]=32 [ORCHESTRATOR_API_KEY]=32 ) # Check secret strength check_secrets() { local env_file="${1:-.env}" local errors=0 local warnings=0 echo -e "${BOLD}Checking secret strength...${NC}" echo "" # Load env file if [[ -f "$env_file" ]]; then set -a # shellcheck source=/dev/null source "$env_file" 2>/dev/null || true set +a fi for secret_var in "${!MIN_SECRET_LENGTHS[@]}"; do local value="${!secret_var:-}" local min_len="${MIN_SECRET_LENGTHS[$secret_var]}" if [[ -z "$value" ]]; then echo -e "${WARN}⚠${NC} $secret_var: Not set" ((warnings++)) elif is_placeholder "$value"; then echo -e "${ERROR}✗${NC} $secret_var: Contains placeholder (MUST change)" ((errors++)) elif [[ ${#value} -lt $min_len ]]; then echo -e "${WARN}⚠${NC} $secret_var: Too short (${#value} chars, minimum $min_len)" ((warnings++)) else echo -e "${SUCCESS}✓${NC} $secret_var: Strong (${#value} chars)" fi done echo "" if [[ $errors -gt 0 ]]; then return $CHECK_FAIL fi if [[ $warnings -gt 0 ]]; then return $CHECK_WARN fi return $CHECK_PASS } # ============================================================================ # Docker Validation # ============================================================================ # Check Docker containers are running check_docker_containers() { local compose_file="${1:-docker-compose.yml}" local errors=0 echo -e "${BOLD}Checking Docker containers...${NC}" echo "" # Expected container names local containers=("mosaic-postgres" "mosaic-valkey" "mosaic-api" "mosaic-web") for container in "${containers[@]}"; do local status status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || echo "not_found") case "$status" in running) echo -e "${SUCCESS}✓${NC} $container: Running" ;; exited) echo -e "${ERROR}✗${NC} $container: Exited" ((errors++)) ;; not_found) # Container might not be in current profile echo -e "${MUTED}○${NC} $container: Not found (may not be in profile)" ;; *) echo -e "${WARN}⚠${NC} $container: $status" ((errors++)) ;; esac done echo "" if [[ $errors -gt 0 ]]; then return $CHECK_FAIL fi return $CHECK_PASS } # Check container health check_container_health() { local errors=0 echo -e "${BOLD}Checking container health...${NC}" echo "" # Get all mosaic containers local containers containers=$(docker ps --filter "name=mosaic-" --format "{{.Names}}" 2>/dev/null) for container in $containers; do local health health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "no_healthcheck") case "$health" in healthy) echo -e "${SUCCESS}✓${NC} $container: Healthy" ;; unhealthy) echo -e "${ERROR}✗${NC} $container: Unhealthy" ((errors++)) ;; starting) echo -e "${WARN}⚠${NC} $container: Starting..." ;; no_healthcheck) echo -e "${INFO}ℹ${NC} $container: No health check" ;; *) echo -e "${WARN}⚠${NC} $container: $health" ;; esac done echo "" if [[ $errors -gt 0 ]]; then return $CHECK_FAIL fi return $CHECK_PASS } # ============================================================================ # Service Connectivity # ============================================================================ # Check if a URL responds check_url_responds() { local url="$1" local expected_status="${2:-200}" local timeout="${3:-10}" if command -v curl &>/dev/null; then local status status=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$timeout" "$url" 2>/dev/null) if [[ "$status" == "$expected_status" ]]; then return 0 fi fi return 1 } # Check API health endpoint check_api_health() { local api_url="${1:-http://localhost:3001}" echo -e "${BOLD}Checking API health...${NC}" echo "" if check_url_responds "${api_url}/health" 200 10; then echo -e "${SUCCESS}✓${NC} API health check passed" return $CHECK_PASS else echo -e "${ERROR}✗${NC} API health check failed" return $CHECK_FAIL fi } # Check Web frontend check_web_health() { local web_url="${1:-http://localhost:3000}" echo -e "${BOLD}Checking Web frontend...${NC}" echo "" if check_url_responds "$web_url" 200 10; then echo -e "${SUCCESS}✓${NC} Web frontend responding" return $CHECK_PASS else echo -e "${WARN}⚠${NC} Web frontend not responding (may still be starting)" return $CHECK_WARN fi } # Check database connectivity check_database_connection() { local host="${1:-localhost}" local port="${2:-5432}" local user="${3:-mosaic}" local database="${4:-mosaic}" echo -e "${BOLD}Checking database connection...${NC}" echo "" # Try via Docker if postgres container exists if docker exec mosaic-postgres pg_isready -U "$user" -d "$database" &>/dev/null; then echo -e "${SUCCESS}✓${NC} Database connection successful" return $CHECK_PASS fi # Try via psql if available if command -v psql &>/dev/null; then if PGPASSWORD="${POSTGRES_PASSWORD:-}" psql -h "$host" -p "$port" -U "$user" -d "$database" -c "SELECT 1" &>/dev/null; then echo -e "${SUCCESS}✓${NC} Database connection successful" return $CHECK_PASS fi fi # Try via TCP if command -v nc &>/dev/null; then if nc -z "$host" "$port" 2>/dev/null; then echo -e "${WARN}⚠${NC} Database port open but could not verify connection" return $CHECK_WARN fi fi echo -e "${ERROR}✗${NC} Database connection failed" return $CHECK_FAIL } # Check Valkey/Redis connectivity check_valkey_connection() { local host="${1:-localhost}" local port="${2:-6379}" echo -e "${BOLD}Checking Valkey/Redis connection...${NC}" echo "" # Try via Docker if valkey container exists if docker exec mosaic-valkey valkey-cli ping 2>/dev/null | grep -q PONG; then echo -e "${SUCCESS}✓${NC} Valkey connection successful" return $CHECK_PASS fi # Try via redis-cli if available if command -v redis-cli &>/dev/null; then if redis-cli -h "$host" -p "$port" ping 2>/dev/null | grep -q PONG; then echo -e "${SUCCESS}✓${NC} Valkey/Redis connection successful" return $CHECK_PASS fi fi # Try via TCP if command -v nc &>/dev/null; then if nc -z "$host" "$port" 2>/dev/null; then echo -e "${WARN}⚠${NC} Valkey port open but could not verify connection" return $CHECK_WARN fi fi echo -e "${ERROR}✗${NC} Valkey/Redis connection failed" return $CHECK_FAIL } # ============================================================================ # System Requirements # ============================================================================ # Check minimum system requirements check_system_requirements() { local min_ram="${1:-2048}" local min_disk="${2:-10}" local errors=0 local warnings=0 echo -e "${BOLD}Checking system requirements...${NC}" echo "" # RAM check local ram ram=$(get_total_ram) if [[ "$ram" -lt "$min_ram" ]]; then echo -e "${ERROR}✗${NC} RAM: ${ram}MB (minimum: ${min_ram}MB)" ((errors++)) else echo -e "${SUCCESS}✓${NC} RAM: ${ram}MB" fi # Disk check local disk disk=$(get_available_disk "$HOME") if [[ "$disk" -lt "$min_disk" ]]; then echo -e "${WARN}⚠${NC} Disk: ${disk}GB available (recommended: ${min_disk}GB+)" ((warnings++)) else echo -e "${SUCCESS}✓${NC} Disk: ${disk}GB available" fi # Docker disk (if using Docker) if command -v docker &>/dev/null && docker info &>/dev/null; then local docker_disk docker_disk=$(docker system df --format "{{.Total}}" 2>/dev/null | head -1 || echo "unknown") echo -e "${INFO}ℹ${NC} Docker storage: $docker_disk" fi echo "" if [[ $errors -gt 0 ]]; then return $CHECK_FAIL fi if [[ $warnings -gt 0 ]]; then return $CHECK_WARN fi return $CHECK_PASS } # ============================================================================ # File Permissions # ============================================================================ # Check .env file permissions check_env_permissions() { local env_file="${1:-.env}" echo -e "${BOLD}Checking file permissions...${NC}" echo "" if [[ ! -f "$env_file" ]]; then echo -e "${WARN}⚠${NC} .env file not found" return $CHECK_WARN fi local perms perms=$(stat -c "%a" "$env_file" 2>/dev/null || stat -f "%OLp" "$env_file" 2>/dev/null) # Check if world-readable if [[ "$perms" =~ [0-7][0-7][4-7]$ ]]; then echo -e "${WARN}⚠${NC} .env is world-readable (permissions: $perms)" echo -e " ${INFO}Fix: chmod 600 $env_file${NC}" return $CHECK_WARN fi echo -e "${SUCCESS}✓${NC} .env permissions: $perms" return $CHECK_PASS } # ============================================================================ # Comprehensive Doctor Check # ============================================================================ # Run all checks and report results run_doctor() { local env_file="${1:-.env}" local compose_file="${2:-docker-compose.yml}" local mode="${3:-docker}" local errors=0 local warnings=0 echo "" echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD} Mosaic Stack Doctor${NC}" echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" echo "" # System requirements run_doctor_check "System Requirements" check_system_requirements 2048 10 collect_result $? # Environment file run_doctor_check "Environment File" check_env_file "$env_file" collect_result $? # Required environment variables run_doctor_check "Required Variables" check_required_env "$env_file" collect_result $? # Secret strength run_doctor_check "Secret Strength" check_secrets "$env_file" collect_result $? # File permissions run_doctor_check "File Permissions" check_env_permissions "$env_file" collect_result $? if [[ "$mode" == "docker" ]]; then # Docker containers run_doctor_check "Docker Containers" check_docker_containers "$compose_file" collect_result $? # Container health run_doctor_check "Container Health" check_container_health collect_result $? # Database connection run_doctor_check "Database" check_database_connection collect_result $? # Valkey connection run_doctor_check "Cache (Valkey)" check_valkey_connection collect_result $? # API health run_doctor_check "API" check_api_health collect_result $? # Web frontend run_doctor_check "Web Frontend" check_web_health collect_result $? fi echo "" echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" # Summary if [[ $errors -gt 0 ]]; then echo -e "${ERROR}✗${NC} ${BOLD}Failed${NC}: $errors errors, $warnings warnings" echo "" echo "Fix the errors above and run doctor again." return $CHECK_FAIL elif [[ $warnings -gt 0 ]]; then echo -e "${WARN}⚠${NC} ${BOLD}Warnings${NC}: $warnings warnings" echo "" echo "System is operational but some optimizations are recommended." return $CHECK_WARN else echo -e "${SUCCESS}✓${NC} ${BOLD}All checks passed${NC}" echo "" echo "Mosaic Stack is healthy and ready to use." return $CHECK_PASS fi } # Helper to run a check and print result run_doctor_check() { local name="$1" shift echo -e "${BOLD}Checking: $name${NC}" echo "" "$@" return $? } # Helper to collect check results collect_result() { local result=$1 case $result in $CHECK_PASS) ;; $CHECK_WARN) ((warnings++)) ;; $CHECK_FAIL) ((errors++)) ;; esac } # ============================================================================ # Quick Health Check # ============================================================================ # Quick check for CI/CD or scripts quick_health_check() { local api_url="${1:-http://localhost:3001}" check_url_responds "${api_url}/health" 200 5 } # Wait for healthy state wait_for_healthy() { local timeout="${1:-120}" local interval="${2:-5}" echo -e "${INFO}ℹ${NC} Waiting for healthy state..." local elapsed=0 while [[ $elapsed -lt $timeout ]]; do if quick_health_check &>/dev/null; then echo -e "${SUCCESS}✓${NC} System is healthy" return 0 fi sleep "$interval" ((elapsed += interval)) echo -n "." done echo "" echo -e "${ERROR}✗${NC} Timeout waiting for healthy state" return 1 }