#!/bin/bash # Docker Compose Validation Script # Validates Docker Compose files before commit to prevent broken deployments # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Function to log messages log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Check if Docker is available if ! command -v docker &> /dev/null; then log_warn "Docker not found. Skipping Docker Compose validation." exit 0 fi # Check if docker-compose is available if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then log_warn "Docker Compose not found. Skipping validation." exit 0 fi # Determine docker-compose command if command -v docker-compose &> /dev/null; then COMPOSE_CMD="docker-compose" else COMPOSE_CMD="docker compose" fi # Validation function validate_compose_file() { local file="$1" local filename=$(basename "$file") # Skip non-Docker Compose files if [[ "$file" == *".pre-commit-config.yaml" ]] || \ [[ "$file" == *".yamllint" ]] || \ [[ "$file" == *".gitea/workflows/"* ]] || \ [[ "$file" == *"secret_key.yaml" ]] || \ [[ "$file" == *"config.yml" ]] || \ [[ "$file" == *"snmp.yml" ]] || \ [[ "$file" == *"homeserver.yaml" ]]; then log_info "Skipping non-Docker Compose file: $file" return 0 fi # Skip files that don't have a 'services:' block (not Docker Compose files) if ! grep -q "^services:" "$file" 2>/dev/null; then log_info "Skipping non-Docker Compose file: $file" return 0 fi # Skip compose files with env_file references to files that don't exist locally if grep -q "env_file:" "$file" 2>/dev/null; then local compose_dir compose_dir=$(dirname "$file") local missing_env=0 while IFS= read -r env_line; do local env_file env_file=$(echo "$env_line" | sed 's/.*-\s*//' | tr -d ' "') if [[ -n "$env_file" ]] && [[ "$env_file" != "~" ]] && \ [[ ! -f "$compose_dir/$env_file" ]]; then missing_env=1 break fi done < <(grep -A1 "env_file:" "$file" | grep "^.*-") if [[ $missing_env -eq 1 ]]; then log_warn "$file: Skipping validation - missing env_file dependencies" return 0 fi fi log_info "Validating $file" # Check if file exists and is readable if [[ ! -r "$file" ]]; then log_error "Cannot read file: $file" return 1 fi # Skip if not a compose file if [[ ! "$filename" =~ \.(yml|yaml)$ ]]; then log_info "Skipping non-YAML file: $file" return 0 fi # Skip certain directories and files if [[ "$file" =~ ^(archive/|ansible/|docs/|\.git/) ]]; then log_info "Skipping excluded path: $file" return 0 fi # Validate Docker Compose syntax if ! $COMPOSE_CMD -f "$file" config > /dev/null 2>&1; then log_error "Docker Compose validation failed for: $file" log_error "Run '$COMPOSE_CMD -f $file config' to see detailed errors" return 1 fi # Check for common issues local warnings=0 # Check for missing version (Docker Compose v2 doesn't require it, but good practice) if ! grep -q "^version:" "$file" 2>/dev/null; then log_warn "$file: Consider adding 'version' field for clarity" ((warnings++)) fi # Check for hardcoded localhost references (should use service names) if grep -q "localhost\|127\.0\.0\.1" "$file" 2>/dev/null; then log_warn "$file: Found localhost references - consider using service names" ((warnings++)) fi # Check for missing restart policies on long-running services if grep -q "image:" "$file" && ! grep -q "restart:" "$file" 2>/dev/null; then log_warn "$file: Consider adding restart policy for production services" ((warnings++)) fi if [[ $warnings -eq 0 ]]; then log_info "✓ $file passed validation" else log_info "✓ $file passed validation with $warnings warnings" fi return 0 } # Main execution main() { local exit_code=0 local files_processed=0 # If no arguments provided, validate all YAML files if [[ $# -eq 0 ]]; then log_info "No files specified, validating all Docker Compose files..." while IFS= read -r -d '' file; do ((files_processed++)) if ! validate_compose_file "$file"; then exit_code=1 fi done < <(find . -name "*.yml" -o -name "*.yaml" -print0 | grep -zv -E '^(archive/|ansible/|docs/|\.git/)') else # Validate specified files for file in "$@"; do ((files_processed++)) if ! validate_compose_file "$file"; then exit_code=1 fi done fi if [[ $exit_code -eq 0 ]]; then log_info "All $files_processed files passed validation!" else log_error "Some files failed validation. Please fix the errors before committing." fi exit $exit_code } # Run main function with all arguments main "$@"