Add EZ-Homelab Enhanced Setup System
- Complete modular bash-based setup system replacing Python TUI - Phase 1-4 implementation: Core Infrastructure, Configuration Management, Deployment Engine, Service Orchestration & Management - 9 production-ready scripts: preflight.sh, setup.sh, pre-deployment-wizard.sh, localize.sh, generalize.sh, validate.sh, deploy.sh, service.sh, monitor.sh, backup.sh, update.sh - Shared libraries: common.sh (utilities), ui.sh (text interface) - Template-based configuration system with environment variable substitution - Comprehensive documentation: PRD, standards, and quick reference guides - Automated backup, monitoring, and update management capabilities - Cross-platform compatibility with robust error handling and logging
This commit is contained in:
440
scripts/enhanced-setup/deploy.sh
Executable file
440
scripts/enhanced-setup/deploy.sh
Executable file
@@ -0,0 +1,440 @@
|
||||
#!/bin/bash
|
||||
# EZ-Homelab Enhanced Setup Scripts - Deployment Engine
|
||||
# Orchestrated deployment of services with proper sequencing and health checks
|
||||
|
||||
SCRIPT_NAME="deploy"
|
||||
SCRIPT_VERSION="1.0.0"
|
||||
|
||||
# Load common library
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
|
||||
|
||||
# =============================================================================
|
||||
# DEPLOYMENT CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Service deployment order (dependencies must come first)
|
||||
DEPLOYMENT_ORDER=(
|
||||
"core" # Infrastructure services (Traefik, Authelia, etc.)
|
||||
"infrastructure" # Development tools (code-server, etc.)
|
||||
"dashboards" # Homepage, monitoring dashboards
|
||||
"monitoring" # Grafana, Prometheus, Loki
|
||||
"media" # Plex, Jellyfin, etc.
|
||||
"media-management" # Sonarr, Radarr, etc.
|
||||
"home" # Home Assistant, Node-RED
|
||||
"productivity" # Nextcloud, Gitea, etc.
|
||||
"utilities" # Duplicati, FreshRSS, etc.
|
||||
"vpn" # VPN services
|
||||
"alternatives" # Alternative services
|
||||
"wikis" # Wiki services
|
||||
)
|
||||
|
||||
# Core services that must be running for the system to function
|
||||
CORE_SERVICES=("traefik" "authelia" "duckdns")
|
||||
|
||||
# Service health check timeouts (seconds)
|
||||
HEALTH_CHECK_TIMEOUT=300
|
||||
SERVICE_STARTUP_TIMEOUT=60
|
||||
|
||||
# =============================================================================
|
||||
# DEPLOYMENT FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
# Get list of available service stacks
|
||||
get_available_stacks() {
|
||||
local stacks=()
|
||||
local stack_dir="$EZ_HOME/docker-compose"
|
||||
|
||||
if [[ -d "$stack_dir" ]]; then
|
||||
while IFS= read -r -d '' dir; do
|
||||
local stack_name="$(basename "$dir")"
|
||||
if [[ -f "$dir/docker-compose.yml" ]]; then
|
||||
stacks+=("$stack_name")
|
||||
fi
|
||||
done < <(find "$stack_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${stacks[@]}"
|
||||
}
|
||||
|
||||
# Check if a service stack exists
|
||||
stack_exists() {
|
||||
local stack="$1"
|
||||
local stack_dir="$EZ_HOME/docker-compose/$stack"
|
||||
|
||||
[[ -d "$stack_dir" && -f "$stack_dir/docker-compose.yml" ]]
|
||||
}
|
||||
|
||||
# Get services in a stack
|
||||
get_stack_services() {
|
||||
local stack="$1"
|
||||
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
|
||||
|
||||
if [[ ! -f "$compose_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract service names from docker-compose.yml
|
||||
# Look for lines that start at column 0 followed by a service name
|
||||
sed -n '/^services:/,/^[^ ]/p' "$compose_file" 2>/dev/null | \
|
||||
grep '^ [a-zA-Z0-9_-]\+:' | \
|
||||
sed 's/^\s*//' | sed 's/:.*$//' || true
|
||||
}
|
||||
|
||||
# Check if a service is running
|
||||
is_service_running() {
|
||||
local service="$1"
|
||||
|
||||
docker ps --filter "name=$service" --filter "status=running" --format "{{.Names}}" | grep -q "^${service}$"
|
||||
}
|
||||
|
||||
# Check service health
|
||||
check_service_health() {
|
||||
local service="$1"
|
||||
local timeout="${2:-$HEALTH_CHECK_TIMEOUT}"
|
||||
local start_time=$(date +%s)
|
||||
|
||||
print_info "Checking health of service: $service"
|
||||
|
||||
while (( $(date +%s) - start_time < timeout )); do
|
||||
if is_service_running "$service"; then
|
||||
# Additional health checks could be added here
|
||||
# For now, just check if container is running
|
||||
print_success "Service $service is healthy"
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
print_error "Service $service failed health check (timeout: ${timeout}s)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Deploy a single service stack
|
||||
deploy_stack() {
|
||||
local stack="$1"
|
||||
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
|
||||
|
||||
if [[ ! -f "$compose_file" ]]; then
|
||||
print_error "Compose file not found: $compose_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Deploying stack: $stack"
|
||||
|
||||
# Validate the compose file first
|
||||
if ! validate_yaml "$compose_file"; then
|
||||
print_error "Invalid YAML in $compose_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Pull images first
|
||||
print_info "Pulling images for stack: $stack"
|
||||
if ! docker compose -f "$compose_file" pull; then
|
||||
print_warning "Failed to pull some images for $stack, continuing..."
|
||||
fi
|
||||
|
||||
# Deploy the stack
|
||||
print_info "Starting services in stack: $stack"
|
||||
if ! docker compose -f "$compose_file" up -d; then
|
||||
print_error "Failed to deploy stack: $stack"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get list of services in this stack
|
||||
local services
|
||||
mapfile -t services < <(get_stack_services "$stack")
|
||||
|
||||
# Wait for services to start and check health
|
||||
for service in "${services[@]}"; do
|
||||
print_info "Waiting for service to start: $service"
|
||||
sleep "$SERVICE_STARTUP_TIMEOUT"
|
||||
|
||||
if ! check_service_health "$service"; then
|
||||
print_error "Service $service in stack $stack failed health check"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
print_success "Successfully deployed stack: $stack"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Stop a service stack
|
||||
stop_stack() {
|
||||
local stack="$1"
|
||||
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
|
||||
|
||||
if [[ ! -f "$compose_file" ]]; then
|
||||
print_warning "Compose file not found: $compose_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "Stopping stack: $stack"
|
||||
|
||||
if docker compose -f "$compose_file" down; then
|
||||
print_success "Successfully stopped stack: $stack"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to stop stack: $stack"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Rollback deployment
|
||||
rollback_deployment() {
|
||||
local failed_stack="$1"
|
||||
local deployed_stacks=("${@:2}")
|
||||
|
||||
print_warning "Rolling back deployment due to failure in: $failed_stack"
|
||||
|
||||
# Stop the failed stack first
|
||||
stop_stack "$failed_stack" || true
|
||||
|
||||
# Stop all previously deployed stacks in reverse order
|
||||
for ((i=${#deployed_stacks[@]}-1; i>=0; i--)); do
|
||||
local stack="${deployed_stacks[i]}"
|
||||
if [[ "$stack" != "$failed_stack" ]]; then
|
||||
stop_stack "$stack" || true
|
||||
fi
|
||||
done
|
||||
|
||||
print_info "Rollback completed"
|
||||
}
|
||||
|
||||
# Deploy all stacks in order
|
||||
deploy_all() {
|
||||
local deployed_stacks=()
|
||||
local total_stacks=${#DEPLOYMENT_ORDER[@]}
|
||||
local current_stack=0
|
||||
|
||||
print_info "Starting full deployment of $total_stacks stacks"
|
||||
|
||||
for stack in "${DEPLOYMENT_ORDER[@]}"; do
|
||||
current_stack=$((current_stack + 1))
|
||||
local percent=$(( current_stack * 100 / total_stacks ))
|
||||
|
||||
if ui_available && ! $non_interactive; then
|
||||
ui_gauge "Deploying $stack... ($current_stack/$total_stacks)" "$percent"
|
||||
fi
|
||||
|
||||
print_info "[$current_stack/$total_stacks] Deploying stack: $stack"
|
||||
|
||||
if ! stack_exists "$stack"; then
|
||||
print_warning "Stack $stack not found, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
if deploy_stack "$stack"; then
|
||||
deployed_stacks+=("$stack")
|
||||
else
|
||||
print_error "Failed to deploy stack: $stack"
|
||||
rollback_deployment "$stack" "${deployed_stacks[@]}"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
print_success "All stacks deployed successfully!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Deploy specific stacks
|
||||
deploy_specific() {
|
||||
local stacks=("$@")
|
||||
local deployed_stacks=()
|
||||
local total_stacks=${#stacks[@]}
|
||||
local current_stack=0
|
||||
|
||||
print_info "Starting deployment of $total_stacks specific stacks"
|
||||
|
||||
for stack in "${stacks[@]}"; do
|
||||
current_stack=$((current_stack + 1))
|
||||
local percent=$(( current_stack * 100 / total_stacks ))
|
||||
|
||||
if ui_available && ! $non_interactive; then
|
||||
ui_gauge "Deploying $stack... ($current_stack/$total_stacks)" "$percent"
|
||||
fi
|
||||
|
||||
print_info "[$current_stack/$total_stacks] Deploying stack: $stack"
|
||||
|
||||
if ! stack_exists "$stack"; then
|
||||
print_error "Stack $stack not found"
|
||||
rollback_deployment "$stack" "${deployed_stacks[@]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if deploy_stack "$stack"; then
|
||||
deployed_stacks+=("$stack")
|
||||
else
|
||||
print_error "Failed to deploy stack: $stack"
|
||||
rollback_deployment "$stack" "${deployed_stacks[@]}"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
print_success "Specified stacks deployed successfully!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Stop all stacks
|
||||
stop_all() {
|
||||
local stacks
|
||||
mapfile -t stacks < <(get_available_stacks)
|
||||
local total_stacks=${#stacks[@]}
|
||||
local current_stack=0
|
||||
|
||||
print_info "Stopping all $total_stacks stacks"
|
||||
|
||||
for stack in "${stacks[@]}"; do
|
||||
current_stack=$((current_stack + 1))
|
||||
local percent=$(( current_stack * 100 / total_stacks ))
|
||||
|
||||
if ui_available && ! $non_interactive; then
|
||||
ui_gauge "Stopping $stack... ($current_stack/$total_stacks)" "$percent"
|
||||
fi
|
||||
|
||||
stop_stack "$stack" || true
|
||||
done
|
||||
|
||||
print_success "All stacks stopped"
|
||||
}
|
||||
|
||||
# Show deployment status
|
||||
show_status() {
|
||||
print_info "EZ-Homelab Deployment Status"
|
||||
echo
|
||||
|
||||
local stacks
|
||||
mapfile -t stacks < <(get_available_stacks)
|
||||
|
||||
for stack in "${stacks[@]}"; do
|
||||
echo "Stack: $stack"
|
||||
|
||||
local services
|
||||
mapfile -t services < <(get_stack_services "$stack")
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
if is_service_running "$service"; then
|
||||
echo " ✅ $service - Running"
|
||||
else
|
||||
echo " ❌ $service - Stopped"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
done
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN FUNCTION
|
||||
# =============================================================================
|
||||
|
||||
main() {
|
||||
local action="deploy"
|
||||
local stacks=()
|
||||
local non_interactive=false
|
||||
local verbose=false
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
cat << EOF
|
||||
EZ-Homelab Deployment Engine
|
||||
|
||||
USAGE:
|
||||
deploy [OPTIONS] [ACTION] [STACKS...]
|
||||
|
||||
ACTIONS:
|
||||
deploy Deploy all stacks (default)
|
||||
stop Stop all stacks
|
||||
status Show deployment status
|
||||
restart Restart all stacks
|
||||
|
||||
ARGUMENTS:
|
||||
STACKS Specific stacks to deploy (optional, deploys all if not specified)
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Show this help message
|
||||
-v, --verbose Enable verbose logging
|
||||
--no-ui Run without interactive UI
|
||||
--no-rollback Skip rollback on deployment failure
|
||||
|
||||
EXAMPLES:
|
||||
deploy # Deploy all stacks
|
||||
deploy core media # Deploy only core and media stacks
|
||||
deploy stop # Stop all stacks
|
||||
deploy status # Show status of all services
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
-v|--verbose)
|
||||
verbose=true
|
||||
shift
|
||||
;;
|
||||
--no-ui)
|
||||
non_interactive=true
|
||||
shift
|
||||
;;
|
||||
--no-rollback)
|
||||
NO_ROLLBACK=true
|
||||
shift
|
||||
;;
|
||||
deploy|stop|status|restart)
|
||||
action="$1"
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
stacks+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Handle remaining arguments as stacks
|
||||
while [[ $# -gt 0 ]]; do
|
||||
stacks+=("$1")
|
||||
shift
|
||||
done
|
||||
|
||||
# Initialize script
|
||||
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
|
||||
init_logging "$SCRIPT_NAME"
|
||||
|
||||
# Check prerequisites
|
||||
if ! docker_available; then
|
||||
print_error "Docker is not available. Please run setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute action
|
||||
case "$action" in
|
||||
deploy)
|
||||
if [[ ${#stacks[@]} -eq 0 ]]; then
|
||||
deploy_all
|
||||
else
|
||||
deploy_specific "${stacks[@]}"
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
stop_all
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
restart)
|
||||
print_info "Restarting all stacks..."
|
||||
stop_all
|
||||
sleep 5
|
||||
deploy_all
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown action: $action"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user