#!/usr/bin/env bash # ============================================================= # do-update.sh # Interactive picker to update containers flagged by Watchtower. # Pulls new image, recreates container via its compose file, # and prunes old images. # # Usage: # bash /opt/stacks/scripts/do-update.sh # ============================================================= set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' GRAY='\033[0;90m' BOLD='\033[1m' NC='\033[0m' # ── Check Watchtower is running ─────────────────────────────── if ! docker inspect watchtower &>/dev/null; then echo -e "${RED}✗ Watchtower is not running.${NC}" exit 1 fi # ── Get outdated images from Watchtower logs ────────────────── OUTDATED_IMAGES=() while IFS= read -r line; do img=$(echo "$line" | grep -oP '(?<=Found new )\S+(?= image)') [ -n "$img" ] && OUTDATED_IMAGES+=("$img") done < <(docker logs watchtower --since 36h 2>&1 | grep "Found new") if [ ${#OUTDATED_IMAGES[@]} -eq 0 ]; then echo -e "${GREEN}✓ No updates available per last Watchtower scan.${NC}" exit 0 fi # ── Build list of containers to update ─────────────────────── declare -a CONTAINER_NAMES declare -a CONTAINER_IMAGES declare -a CONTAINER_COMPOSE_DIRS declare -a CONTAINER_SERVICES for image in "${OUTDATED_IMAGES[@]}"; do while IFS='|' read -r cname cimage; do COMPOSE_DIR=$(docker inspect "$cname" \ --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' 2>/dev/null || echo "") SERVICE=$(docker inspect "$cname" \ --format '{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || echo "") CONTAINER_NAMES+=("$cname") CONTAINER_IMAGES+=("$cimage") CONTAINER_COMPOSE_DIRS+=("$COMPOSE_DIR") CONTAINER_SERVICES+=("$SERVICE") done < <(docker ps -a --format '{{.Names}}|{{.Image}}' | awk -F'|' -v img="$image" '$2==img') done if [ ${#CONTAINER_NAMES[@]} -eq 0 ]; then echo -e "${YELLOW}⚠ Outdated images found but no matching running containers.${NC}" exit 0 fi # ── Show menu ───────────────────────────────────────────────── echo "" echo -e "${BOLD}Available updates:${NC}" echo "" for i in "${!CONTAINER_NAMES[@]}"; do idx=$((i + 1)) name="${CONTAINER_NAMES[$i]}" image="${CONTAINER_IMAGES[$i]}" svc="${CONTAINER_SERVICES[$i]}" dir="${CONTAINER_COMPOSE_DIRS[$i]}" printf " ${CYAN}%2d)${NC} %-20s ${GRAY}%-40s${NC}\n" "$idx" "$name" "$image" if [ -n "$dir" ]; then printf " ${GRAY}compose: %s service: %s${NC}\n" "$dir" "$svc" fi done echo "" echo -e " ${GRAY}a)${NC} update all" echo -e " ${GRAY}q)${NC} quit" echo "" read -rp " Enter numbers to update (e.g. 1 3 5): " SELECTION if [ "$SELECTION" = "q" ] || [ -z "$SELECTION" ]; then echo -e "\n ${GRAY}Cancelled.${NC}" exit 0 fi # ── Build list of selected indices ──────────────────────────── SELECTED=() if [ "$SELECTION" = "a" ]; then for i in "${!CONTAINER_NAMES[@]}"; do SELECTED+=("$i"); done else for num in $SELECTION; do idx=$((num - 1)) if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#CONTAINER_NAMES[@]}" ]; then SELECTED+=("$idx") else echo -e " ${YELLOW}⚠ Skipping invalid selection: ${num}${NC}" fi done fi if [ ${#SELECTED[@]} -eq 0 ]; then echo -e " ${YELLOW}No valid selections.${NC}" exit 1 fi # ── Confirm ─────────────────────────────────────────────────── echo "" echo -e "${BOLD}Will update:${NC}" for i in "${SELECTED[@]}"; do echo -e " ${GREEN}→${NC} ${CONTAINER_NAMES[$i]} ${GRAY}(${CONTAINER_IMAGES[$i]})${NC}" done echo "" read -rp " Proceed? [y/N] " CONFIRM [[ "$CONFIRM" =~ ^[Yy]$ ]] || { echo -e " ${GRAY}Aborted.${NC}"; exit 0; } # ── Update selected containers ──────────────────────────────── PRUNED=0 for i in "${SELECTED[@]}"; do name="${CONTAINER_NAMES[$i]}" image="${CONTAINER_IMAGES[$i]}" dir="${CONTAINER_COMPOSE_DIRS[$i]}" svc="${CONTAINER_SERVICES[$i]}" echo "" echo -e "${CYAN}── Updating ${name} ──────────────────────────────────────${NC}" echo -e " ${GRAY}pulling ${image}...${NC}" docker pull "$image" if [ -n "$dir" ] && [ -d "$dir" ]; then echo -e " ${GRAY}recreating via compose...${NC}" docker compose -f "$dir/docker-compose.yml" up -d --force-recreate "$svc" else echo -e " ${YELLOW}⚠ No compose dir found — recreating container manually${NC}" docker stop "$name" 2>/dev/null || true docker rm "$name" 2>/dev/null || true echo -e " ${YELLOW} Container removed. Restart manually with your compose file.${NC}" fi echo -e " ${GREEN}✓ ${name} updated${NC}" PRUNED=1 done # ── Prune old images ────────────────────────────────────────── if [ "$PRUNED" -eq 1 ]; then echo "" echo -e "${GRAY}Pruning dangling images...${NC}" docker image prune -f echo -e "${GREEN}✓ Done.${NC}" fi echo "" echo -e "${GREEN}${BOLD}All selected containers updated.${NC}" echo ""