- 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
600 lines
18 KiB
Bash
Executable File
600 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
|
# EZ-Homelab Enhanced Setup Scripts - Update Management
|
|
# Service update management with zero-downtime deployments
|
|
|
|
SCRIPT_NAME="update"
|
|
SCRIPT_VERSION="1.0.0"
|
|
|
|
# Load common library
|
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
|
|
|
|
# =============================================================================
|
|
# UPDATE CONFIGURATION
|
|
# =============================================================================
|
|
|
|
# Update settings
|
|
UPDATE_CHECK_INTERVAL=86400 # 24 hours in seconds
|
|
UPDATE_TIMEOUT=300 # 5 minutes timeout for updates
|
|
ROLLBACK_TIMEOUT=180 # 3 minutes for rollback
|
|
|
|
# Update sources
|
|
DOCKER_HUB_API="https://registry.hub.docker.com/v2"
|
|
GITHUB_API="https://api.github.com"
|
|
|
|
# Update strategies
|
|
UPDATE_STRATEGY_ROLLING="rolling" # Update one service at a time
|
|
UPDATE_STRATEGY_BLUE_GREEN="blue-green" # Deploy new version alongside old
|
|
UPDATE_STRATEGY_CANARY="canary" # Update subset of instances first
|
|
|
|
# Default update strategy
|
|
DEFAULT_UPDATE_STRATEGY="$UPDATE_STRATEGY_ROLLING"
|
|
|
|
# =============================================================================
|
|
# UPDATE STATE MANAGEMENT
|
|
# =============================================================================
|
|
|
|
# Update state file
|
|
UPDATE_STATE_FILE="$LOG_DIR/update_state.json"
|
|
|
|
# Initialize update state
|
|
init_update_state() {
|
|
if [[ ! -f "$UPDATE_STATE_FILE" ]]; then
|
|
cat > "$UPDATE_STATE_FILE" << EOF
|
|
{
|
|
"last_check": 0,
|
|
"updates_available": {},
|
|
"update_history": [],
|
|
"current_updates": {}
|
|
}
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
# Record update attempt
|
|
record_update_attempt() {
|
|
local service="$1"
|
|
local old_version="$2"
|
|
local new_version="$3"
|
|
local status="$4"
|
|
local timestamp
|
|
timestamp=$(date +%s)
|
|
|
|
if command_exists "jq"; then
|
|
jq --arg service "$service" --arg old_version "$old_version" --arg new_version "$new_version" \
|
|
--arg status "$status" --argjson timestamp "$timestamp" \
|
|
'.update_history |= . + [{"service": $service, "old_version": $old_version, "new_version": $new_version, "status": $status, "timestamp": $timestamp}]' \
|
|
"$UPDATE_STATE_FILE" > "${UPDATE_STATE_FILE}.tmp" && mv "${UPDATE_STATE_FILE}.tmp" "$UPDATE_STATE_FILE"
|
|
fi
|
|
}
|
|
|
|
# Get service update status
|
|
get_service_update_status() {
|
|
local service="$1"
|
|
|
|
if command_exists "jq" && [[ -f "$UPDATE_STATE_FILE" ]]; then
|
|
jq -r ".current_updates[\"$service\"] // \"idle\"" "$UPDATE_STATE_FILE"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
# Set service update status
|
|
set_service_update_status() {
|
|
local service="$1"
|
|
local status="$2"
|
|
|
|
if command_exists "jq"; then
|
|
jq --arg service "$service" --arg status "$status" \
|
|
'.current_updates[$service] = $status' \
|
|
"$UPDATE_STATE_FILE" > "${UPDATE_STATE_FILE}.tmp" && mv "${UPDATE_STATE_FILE}.tmp" "$UPDATE_STATE_FILE"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# VERSION CHECKING FUNCTIONS
|
|
# =============================================================================
|
|
|
|
# Get current service version
|
|
get_current_version() {
|
|
local service="$1"
|
|
|
|
if ! is_service_running "$service"; then
|
|
echo "unknown"
|
|
return 1
|
|
fi
|
|
|
|
# Get image from running container
|
|
local image
|
|
image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
|
|
|
|
if [[ -z "$image" ]]; then
|
|
echo "unknown"
|
|
return 1
|
|
fi
|
|
|
|
# Extract version tag
|
|
if [[ "$image" == *":"* ]]; then
|
|
echo "$image" | cut -d: -f2
|
|
else
|
|
echo "latest"
|
|
fi
|
|
}
|
|
|
|
# Check for Docker image updates
|
|
check_docker_updates() {
|
|
local service="$1"
|
|
|
|
if ! is_service_running "$service"; then
|
|
return 1
|
|
fi
|
|
|
|
local current_image
|
|
current_image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
|
|
|
|
if [[ -z "$current_image" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
# Extract repository and tag
|
|
local repo tag
|
|
if [[ "$current_image" == *":"* ]]; then
|
|
repo=$(echo "$current_image" | cut -d: -f1)
|
|
tag=$(echo "$current_image" | cut -d: -f2)
|
|
else
|
|
repo="$current_image"
|
|
tag="latest"
|
|
fi
|
|
|
|
print_info "Checking updates for $service ($repo:$tag)"
|
|
|
|
# Pull latest image to check for updates
|
|
if docker pull "$repo:latest" >/dev/null 2>&1; then
|
|
# Compare image IDs
|
|
local current_id latest_id
|
|
current_id=$(docker inspect "$repo:$tag" --format '{{.Id}}' 2>/dev/null || echo "")
|
|
latest_id=$(docker inspect "$repo:latest" --format '{{.Id}}' 2>/dev/null || echo "")
|
|
|
|
if [[ "$current_id" != "$latest_id" ]]; then
|
|
print_info "Update available for $service: $tag -> latest"
|
|
return 0
|
|
else
|
|
print_info "Service $service is up to date"
|
|
return 1
|
|
fi
|
|
else
|
|
print_warning "Failed to check updates for $service"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Check all services for updates
|
|
check_all_updates() {
|
|
print_info "Checking for service updates"
|
|
|
|
local services
|
|
mapfile -t services < <(find_all_services)
|
|
local updates_available=()
|
|
|
|
for service in "${services[@]}"; do
|
|
if check_docker_updates "$service"; then
|
|
updates_available+=("$service")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#updates_available[@]} -gt 0 ]]; then
|
|
print_info "Updates available for: ${updates_available[*]}"
|
|
return 0
|
|
else
|
|
print_info "All services are up to date"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# UPDATE EXECUTION FUNCTIONS
|
|
# =============================================================================
|
|
|
|
# Update single service with rolling strategy
|
|
update_service_rolling() {
|
|
local service="$1"
|
|
local new_image="$2"
|
|
|
|
print_info "Updating service $service with rolling strategy"
|
|
set_service_update_status "$service" "updating"
|
|
|
|
local old_version
|
|
old_version=$(get_current_version "$service")
|
|
|
|
# Get compose file
|
|
local compose_file
|
|
compose_file=$(get_service_compose_file "$service")
|
|
|
|
if [[ -z "$compose_file" ]]; then
|
|
print_error "Cannot find compose file for service $service"
|
|
set_service_update_status "$service" "failed"
|
|
return 1
|
|
fi
|
|
|
|
local compose_dir=$(dirname "$compose_file")
|
|
local compose_base=$(basename "$compose_file")
|
|
|
|
# Backup current configuration
|
|
print_info "Creating backup before update"
|
|
"$SCRIPT_DIR/backup.sh" config --quiet
|
|
|
|
# Update the service
|
|
print_info "Pulling new image: $new_image"
|
|
if ! docker pull "$new_image"; then
|
|
print_error "Failed to pull new image: $new_image"
|
|
set_service_update_status "$service" "failed"
|
|
return 1
|
|
fi
|
|
|
|
print_info "Restarting service with new image"
|
|
if (cd "$compose_dir" && docker compose -f "$compose_base" up -d "$service"); then
|
|
# Wait for service to start
|
|
local count=0
|
|
while (( count < UPDATE_TIMEOUT )) && ! is_service_running "$service"; do
|
|
sleep 5
|
|
((count += 5))
|
|
done
|
|
|
|
if is_service_running "$service"; then
|
|
# Verify service health
|
|
sleep 10
|
|
if check_service_health "$service"; then
|
|
local new_version
|
|
new_version=$(get_current_version "$service")
|
|
print_success "Service $service updated successfully: $old_version -> $new_version"
|
|
record_update_attempt "$service" "$old_version" "$new_version" "success"
|
|
set_service_update_status "$service" "completed"
|
|
return 0
|
|
else
|
|
print_error "Service $service failed health check after update"
|
|
rollback_service "$service"
|
|
return 1
|
|
fi
|
|
else
|
|
print_error "Service $service failed to start after update"
|
|
rollback_service "$service"
|
|
return 1
|
|
fi
|
|
else
|
|
print_error "Failed to update service $service"
|
|
set_service_update_status "$service" "failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Rollback service to previous version
|
|
rollback_service() {
|
|
local service="$1"
|
|
|
|
print_warning "Rolling back service $service"
|
|
set_service_update_status "$service" "rolling_back"
|
|
|
|
# For now, just restart with current configuration
|
|
# In a more advanced implementation, this would restore from backup
|
|
local compose_file
|
|
compose_file=$(get_service_compose_file "$service")
|
|
|
|
if [[ -n "$compose_file" ]]; then
|
|
local compose_dir=$(dirname "$compose_file")
|
|
local compose_base=$(basename "$compose_file")
|
|
|
|
if (cd "$compose_dir" && docker compose -f "$compose_base" restart "$service"); then
|
|
sleep 10
|
|
if check_service_health "$service"; then
|
|
print_success "Service $service rolled back successfully"
|
|
set_service_update_status "$service" "rolled_back"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
print_error "Failed to rollback service $service"
|
|
set_service_update_status "$service" "rollback_failed"
|
|
return 1
|
|
}
|
|
|
|
# Update all services
|
|
update_all_services() {
|
|
local strategy="${1:-$DEFAULT_UPDATE_STRATEGY}"
|
|
|
|
print_info "Updating all services with $strategy strategy"
|
|
|
|
local services
|
|
mapfile -t services < <(find_all_services)
|
|
local updated=0
|
|
local failed=0
|
|
|
|
for service in "${services[@]}"; do
|
|
if check_docker_updates "$service"; then
|
|
print_info "Updating service: $service"
|
|
|
|
# Get latest image
|
|
local current_image
|
|
current_image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$current_image" ]]; then
|
|
local repo
|
|
repo=$(echo "$current_image" | cut -d: -f1)
|
|
local new_image="$repo:latest"
|
|
|
|
if update_service_rolling "$service" "$new_image"; then
|
|
((updated++))
|
|
else
|
|
((failed++))
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
|
|
print_info "Update summary: $updated updated, $failed failed"
|
|
|
|
if (( failed > 0 )); then
|
|
return 1
|
|
else
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# UPDATE MONITORING FUNCTIONS
|
|
# =============================================================================
|
|
|
|
# Show update status
|
|
show_update_status() {
|
|
print_info "Update Status"
|
|
echo
|
|
|
|
local services
|
|
mapfile -t services < <(find_all_services)
|
|
|
|
echo "Service Update Status:"
|
|
echo "----------------------------------------"
|
|
|
|
for service in "${services[@]}"; do
|
|
local status
|
|
status=$(get_service_update_status "$service")
|
|
local version
|
|
version=$(get_current_version "$service")
|
|
|
|
printf " %-20s %-12s %s\n" "$service" "$status" "$version"
|
|
done
|
|
echo
|
|
|
|
# Show recent update history
|
|
if command_exists "jq" && [[ -f "$UPDATE_STATE_FILE" ]]; then
|
|
echo "Recent Update History:"
|
|
echo "----------------------------------------"
|
|
jq -r '.update_history | reverse | .[0:5][] | "\(.timestamp | strftime("%Y-%m-%d %H:%M")) \(.service) \(.old_version)->\(.new_version) [\(.status)]"' "$UPDATE_STATE_FILE" 2>/dev/null || echo "No update history available"
|
|
fi
|
|
}
|
|
|
|
# Monitor ongoing updates
|
|
monitor_updates() {
|
|
print_info "Monitoring ongoing updates (Ctrl+C to stop)"
|
|
|
|
while true; do
|
|
clear
|
|
show_update_status
|
|
echo
|
|
echo "Press Ctrl+C to stop monitoring"
|
|
sleep 10
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# AUTOMATED UPDATE FUNCTIONS
|
|
# =============================================================================
|
|
|
|
# Setup automated updates
|
|
setup_automated_updates() {
|
|
local schedule="${1:-0 3 * * 0}" # Weekly on Sunday at 3 AM
|
|
|
|
print_info "Setting up automated updates with schedule: $schedule"
|
|
|
|
# Create update script
|
|
local update_script="$HOME/.ez-homelab/update.sh"
|
|
cat > "$update_script" << EOF
|
|
#!/bin/bash
|
|
# Automated update script for EZ-Homelab
|
|
|
|
SCRIPT_DIR="$SCRIPT_DIR"
|
|
|
|
# Run updates
|
|
"\$SCRIPT_DIR/update.sh" all --quiet
|
|
|
|
# Log completion
|
|
echo "\$(date): Automated update completed" >> "$LOG_DIR/update.log"
|
|
EOF
|
|
|
|
chmod +x "$update_script"
|
|
|
|
# Add to crontab
|
|
local cron_entry="$schedule $update_script"
|
|
if ! crontab -l 2>/dev/null | grep -q "update.sh"; then
|
|
(crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
|
|
print_info "Added automated updates to crontab: $cron_entry"
|
|
fi
|
|
|
|
print_success "Automated updates configured"
|
|
}
|
|
|
|
# Remove automated updates
|
|
remove_automated_updates() {
|
|
print_info "Removing automated updates"
|
|
|
|
# Remove from crontab
|
|
crontab -l 2>/dev/null | grep -v "update.sh" | crontab -
|
|
|
|
print_success "Automated updates removed"
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN FUNCTION
|
|
# =============================================================================
|
|
|
|
main() {
|
|
local action=""
|
|
local service=""
|
|
local strategy="$DEFAULT_UPDATE_STRATEGY"
|
|
local schedule=""
|
|
local quiet=false
|
|
|
|
# Parse command line arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help)
|
|
cat << EOF
|
|
EZ-Homelab Update Management
|
|
|
|
USAGE:
|
|
update [OPTIONS] <ACTION> [SERVICE]
|
|
|
|
ACTIONS:
|
|
check Check for available updates
|
|
update Update a service or all services
|
|
status Show update status and history
|
|
monitor Monitor ongoing updates
|
|
rollback Rollback a service
|
|
schedule Setup automated updates
|
|
unschedule Remove automated updates
|
|
|
|
OPTIONS:
|
|
-s, --strategy STRATEGY Update strategy (rolling, blue-green, canary)
|
|
--schedule CRON Cron schedule for automated updates
|
|
-q, --quiet Suppress non-error output
|
|
|
|
STRATEGIES:
|
|
rolling Update one service at a time (default)
|
|
blue-green Deploy new version alongside old
|
|
canary Update subset of instances first
|
|
|
|
EXAMPLES:
|
|
update check # Check for updates
|
|
update update traefik # Update Traefik service
|
|
update update all # Update all services
|
|
update status # Show update status
|
|
update rollback traefik # Rollback Traefik
|
|
update monitor # Monitor updates
|
|
update schedule "0 3 * * 0" # Weekly updates Sunday 3 AM
|
|
|
|
EOF
|
|
exit 0
|
|
;;
|
|
-s|--strategy)
|
|
strategy="$2"
|
|
shift 2
|
|
;;
|
|
--schedule)
|
|
schedule="$2"
|
|
shift 2
|
|
;;
|
|
-q|--quiet)
|
|
quiet=true
|
|
shift
|
|
;;
|
|
check|update|status|monitor|rollback|schedule|unschedule)
|
|
action="$1"
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
if [[ -z "$service" ]]; then
|
|
service="$1"
|
|
else
|
|
print_error "Too many arguments"
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Handle remaining arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
if [[ -z "$service" ]]; then
|
|
service="$1"
|
|
else
|
|
print_error "Too many arguments"
|
|
exit 1
|
|
fi
|
|
shift
|
|
done
|
|
|
|
# Initialize script
|
|
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
|
|
init_logging "$SCRIPT_NAME"
|
|
init_update_state
|
|
|
|
# Check prerequisites
|
|
if ! docker_available; then
|
|
print_error "Docker is not available"
|
|
exit 1
|
|
fi
|
|
|
|
# Execute action
|
|
case "$action" in
|
|
check)
|
|
if [[ -n "$service" ]]; then
|
|
check_docker_updates "$service"
|
|
else
|
|
check_all_updates
|
|
fi
|
|
;;
|
|
update)
|
|
if [[ "$service" == "all" || -z "$service" ]]; then
|
|
update_all_services "$strategy"
|
|
else
|
|
# Get latest image for the service
|
|
local current_image
|
|
current_image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$current_image" ]]; then
|
|
local repo
|
|
repo=$(echo "$current_image" | cut -d: -f1)
|
|
local new_image="$repo:latest"
|
|
|
|
update_service_rolling "$service" "$new_image"
|
|
else
|
|
print_error "Cannot determine current image for service $service"
|
|
exit 1
|
|
fi
|
|
fi
|
|
;;
|
|
status)
|
|
show_update_status
|
|
;;
|
|
monitor)
|
|
monitor_updates
|
|
;;
|
|
rollback)
|
|
if [[ -n "$service" ]]; then
|
|
rollback_service "$service"
|
|
else
|
|
print_error "Service name required for rollback"
|
|
exit 1
|
|
fi
|
|
;;
|
|
schedule)
|
|
setup_automated_updates "$schedule"
|
|
;;
|
|
unschedule)
|
|
remove_automated_updates
|
|
;;
|
|
"")
|
|
print_error "No action specified. Use --help for usage information."
|
|
exit 1
|
|
;;
|
|
*)
|
|
print_error "Unknown action: $action"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Run main function
|
|
main "$@" |