#!/bin/bash # ============================================================================================== # ================================= Docker Update ============================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Pulls the latest images for configured containers. Two modes: normal (daily) # and remainder (weekly). # # Normal mode is called by daily_sync_maintenance.sh before docker_daily_restart.sh. # Containers stay running during the pull — no extra downtime beyond what the # nightly restart already causes. # # Remainder mode is called by weekly_sync_maintenance.sh as the final update step. # It catches everything that normal mode and the weekly sync window did not already # update — derived automatically from docker ps, nothing to configure. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # Normal mode (daily): # Targets DAILY_RESTART_CONTAINERS — same list used by docker_daily_restart.sh. # Pull → compare old vs new image ID → mark updated or already current. # docker_daily_restart.sh runs after — containers restart onto the fresh image. # # Remainder mode (weekly): # Targets all currently running containers NOT in: # DAILY_RESTART_CONTAINERS — already updated daily # emby + critical-data profiles — updated inline by the weekly sync window # FALLBACK_*_TIER* — owned by the remote server's update cycle # Pull → compare → restart if updated → prune dangling images. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Single List # Normal mode reuses DAILY_RESTART_CONTAINERS rather than maintaining a # separate update list. Adding or removing a container from the restart list # automatically updates the image pull list — one change, both places. # # Version Ownership # Fallback containers are excluded from remainder mode. This server only runs # them during a failover. The remote server owns their version — if remainder # updates them independently and a handback occurs, the remote's older image # may not handle data written by the newer version. # # State Respect # Stopped containers are never targeted. Pulling while stopped adds no value # and a stopped container was likely halted intentionally. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Enforcement # Docker operations require root privileges. # # DAILY_CONTAINER_UPDATES Toggle # Normal mode exits cleanly when disabled. docker_daily_restart.sh still runs # regardless — update and restart are independent operations. # # Fallback Exclusion # Remainder mode excludes containers owned by the remote server's update cycle # to prevent version divergence across the failover boundary. # # Running-Only Filter # Stopped containers excluded from remainder mode — intentionally down. # # Image ID Comparison # Containers not restarted unless their image actually changed. Pulls that # result in "already up to date" produce no restart. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master.conf # # DAILY_CONTAINER_UPDATES # Enable or disable normal mode. docker_daily_restart.sh runs regardless. # (default: true) # # PROFILE_CRITICAL_CONTAINER_NAMES[emby|critical-data] # Container names for emby and critical-data profiles — excluded from # remainder mode (already updated by the weekly sync window) # # master_host*.conf # # HOST*_DAILY_RESTART_CONTAINERS # Containers updated in normal mode. Aliased by detect_hosts() → # DAILY_RESTART_CONTAINERS # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # docker_update.sh # Normal mode — pull latest images for DAILY_RESTART_CONTAINERS # # docker_update.sh --remainder # Remainder mode — pull all running containers not in managed lists, # restart those that received updates, prune dangling images # # docker_update.sh --dry-run # Preview which containers would be pulled without making changes # # docker_update.sh --status # Show configuration and container list for current mode # # docker_update.sh --log # Verbose per-container pull and comparison output # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" # Pre-parse --remainder before parse_args (unknown args pass through to PARSED_ARGS) REMAINDER_MODE=false _filtered_args=() for _arg in "$@"; do if [[ "$_arg" == "--remainder" ]]; then REMAINDER_MODE=true else _filtered_args+=("$_arg") fi done unset _arg parse_args "${_filtered_args[@]}" unset _filtered_args # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi if ! command -v docker &>/dev/null; then error "Docker command not found" exit 1 fi detect_hosts # ============================================================================================== # ━━━ Container Discovery ━━━ # ============================================================================================== if [[ "$REMAINDER_MODE" == true ]]; then declare -A _exclude=() # Daily containers — updated by docker_update.sh normal mode for _c in "${DAILY_RESTART_CONTAINERS[@]}"; do [[ -n "$_c" ]] && _exclude["$_c"]=1 done # Weekly sync-window containers (emby + critical-data) — updated inline by weekly_sync_maintenance.sh _weekly_str="${PROFILE_CRITICAL_CONTAINER_NAMES[emby]:-} ${PROFILE_CRITICAL_CONTAINER_NAMES[critical-data]:-}" read -r -a _weekly_arr <<< "$_weekly_str" for _c in "${_weekly_arr[@]}"; do [[ -n "$_c" ]] && _exclude["$_c"]=1 done unset _weekly_str _weekly_arr # Fallback coverage containers — owned by the remote server's update cycle. # This server runs them during failover but should never update them independently. # Updating them here risks version divergence: if remote's writeback after handback # encounters data written by a newer version, it may not handle it correctly. for _tier in 1 2 3 4; do _tier_var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER${_tier}" eval "_tier_arr=(\"\${${_tier_var}[@]:-}\")" 2>/dev/null for _c in "${_tier_arr[@]}"; do [[ -n "$_c" ]] && _exclude["$_c"]=1 done done unset _tier _tier_var _tier_arr _c mapfile -t _all_running < <(docker ps --format '{{.Names}}' | sort) TARGET_CONTAINERS=() for _c in "${_all_running[@]}"; do [[ -z "${_exclude[$_c]+x}" ]] && TARGET_CONTAINERS+=("$_c") done unset _all_running _exclude _c else if [[ "${DAILY_CONTAINER_UPDATES:-true}" != "true" ]]; then log "DAILY_CONTAINER_UPDATES=false — skipping container updates" exit 0 fi if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update" warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in master_host*.conf" exit 0 fi TARGET_CONTAINERS=("${DAILY_RESTART_CONTAINERS[@]}") fi # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_GEAR Mode: $([[ "$REMAINDER_MODE" == true ]] && echo "remainder" || echo "normal (daily)")" if [[ "$REMAINDER_MODE" == true ]]; then echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly sync, and fallback)" for _c in "${TARGET_CONTAINERS[@]}"; do echo " $_c"; done else echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}" echo "$ICON_GEAR Enabled: ${DAILY_CONTAINER_UPDATES:-true}" fi echo "$ICON_GEAR Dry Run: $DRY_RUN" echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no images will be pulled" if [[ ${#TARGET_CONTAINERS[@]} -eq 0 ]]; then log "No containers to update" exit 0 fi # ============================================================================================== # ━━━ Pull Updates ━━━ # ============================================================================================== echo "" if [[ "$REMAINDER_MODE" == true ]]; then echo "━━━ $ICON_CONTAINERS Docker Update (remainder) — $(date '+%Y-%m-%d %H:%M:%S') ━━━" echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily or weekly sync)" else echo "━━━ $ICON_CONTAINERS Docker Update — $(date '+%Y-%m-%d %H:%M:%S') ━━━" echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}" fi echo "" START=$(date +%s) UPDATED=() UP_TO_DATE=() FAILED=() SKIPPED=() for container in "${TARGET_CONTAINERS[@]}"; do [[ -z "$container" ]] && continue echo "━━━ $ICON_CONTAINERS $container ━━━" if ! docker inspect "$container" &>/dev/null; then warn "$container — not found, skipping" SKIPPED+=("$container") echo "" continue fi IMAGE=$(docker inspect --format='{{.Config.Image}}' "$container" 2>/dev/null) if [[ -z "$IMAGE" ]]; then warn "$container — could not determine image, skipping" SKIPPED+=("$container") echo "" continue fi log "$container — image: $IMAGE" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would pull: $IMAGE" UPDATED+=("$container") echo "" continue fi # Capture image ID before pull to detect whether an update landed OLD_ID=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "") echo "$ICON_SYNC Pulling $IMAGE..." if docker pull "$IMAGE" 2>&1 | grep -E "^(Status:|Digest:|Error|error)" | sed 's/^/ /'; then NEW_ID=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "") if [[ -n "$OLD_ID" ]] && [[ "$OLD_ID" != "$NEW_ID" ]]; then echo "$ICON_DONE $container — updated ✅" UPDATED+=("$container") else log "$container — already up to date" UP_TO_DATE+=("$container") fi else warn "$container — pull failed ($IMAGE)" FAILED+=("$container") fi echo "" done END=$(date +%s) # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== if [[ "$REMAINDER_MODE" == true ]]; then echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINDER) SUMMARY ━━━━━" else echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE SUMMARY ━━━━━" fi echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" [[ ${#UPDATED[@]} -gt 0 ]] && echo "$ICON_DONE Updated: ${UPDATED[*]}" [[ ${#UP_TO_DATE[@]} -gt 0 ]] && echo "$ICON_RUNNING Up to date: ${UP_TO_DATE[*]}" [[ ${#SKIPPED[@]} -gt 0 ]] && echo "$ICON_WARN Skipped: ${SKIPPED[*]}" [[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no images pulled" elif [[ ${#FAILED[@]} -eq 0 ]]; then log "$ICON_DONE Status: done ✅ — ${#UPDATED[@]} updated, ${#UP_TO_DATE[@]} current" else warn "Status: ${#FAILED[@]} pull(s) failed — restart will proceed with existing images" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Pull failures are non-fatal — restart proceeds regardless exit 0