Files
stack/scripts/lib/validation.sh
Jason Woltje ab52827d9c
All checks were successful
ci/woodpecker/push/infra Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
chore: add install scripts, doctor command, and AGENTS.md
- Add one-line installer (scripts/install.sh) with platform detection
- Add doctor command (scripts/commands/doctor.sh) for environment diagnostics
- Add shared libraries: dependencies, docker, platform, validation
- Update README with quick-start installer instructions
- Add AGENTS.md with codebase patterns for AI agent context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:04:36 -06:00

748 lines
20 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
}