Files
EZ-Homelab/scripts/enhanced-setup/update.sh
Kelin f141848a10 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
2026-01-29 19:53:36 -05:00

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 "$@"