654 lines
22 KiB
Bash
Executable file
654 lines
22 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Upgrade hernani podman containers on koopa: per-target dry-run (prod-safe),
|
|
# then full backup (compose + mounts/volumes + DB dumps), then apply.
|
|
#
|
|
# Run on koopa as hernani:
|
|
# ~/upgrade-containers.sh # dry-run only (default)
|
|
# ~/upgrade-containers.sh --apply # dry-run → backup → upgrade per target
|
|
# ~/upgrade-containers.sh --apply --images-only
|
|
# ~/upgrade-containers.sh --apply --os-only
|
|
# ~/upgrade-containers.sh --apply --recreate-only
|
|
# ~/upgrade-containers.sh --apply --only koopa-forgejo,koopa-notes
|
|
# ~/upgrade-containers.sh --apply --skip 'taler-hacktivism*,goa-regio-ng'
|
|
# ~/upgrade-containers.sh --apply --taler-debs
|
|
# ~/upgrade-containers.sh --apply --rebuild-local
|
|
# ~/upgrade-containers.sh --apply --include-stopped
|
|
# ~/upgrade-containers.sh --apply --backup-max-gb 80
|
|
# ~/upgrade-containers.sh --apply --no-backup # emergency only
|
|
#
|
|
# Per-target --apply flow:
|
|
# 1) dry-run checks that do not recreate/restart or mutate packages
|
|
# 2) if dry-run fails → skip that target (others continue)
|
|
# 3) full backup under ~/backups/container-upgrades/<ts>/<name>/
|
|
# 4) if backup fails → skip apply for that target
|
|
# 5) pull / recreate / OS upgrade as selected
|
|
set -euo pipefail
|
|
|
|
APPLY=0
|
|
DO_IMAGES=1
|
|
DO_RECREATE=1
|
|
DO_OS=1
|
|
DO_TALER_DEBS=0
|
|
REBUILD_LOCAL=0
|
|
INCLUDE_STOPPED=0
|
|
DO_BACKUP=1
|
|
BACKUP_MAX_GB=50
|
|
ONLY_RAW=""
|
|
SKIP_RAW=""
|
|
HOME_DIR="${HOME:-/home/hernani}"
|
|
ADMIN_LOG="${ADMIN_LOG:-$HOME_DIR/koopa-admin-log}"
|
|
BACKUP_ROOT="${BACKUP_ROOT:-$HOME_DIR/backups/container-upgrades}"
|
|
RUN_TS="$(date '+%Y%m%d-%H%M%S')"
|
|
BACKUP_RUN_DIR="$BACKUP_ROOT/$RUN_TS"
|
|
|
|
LOG_TS() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
|
log() { printf '[%s] %s\n' "$(LOG_TS)" "$*"; }
|
|
run() {
|
|
if [[ "$APPLY" -eq 1 ]]; then
|
|
log "RUN: $*"
|
|
"$@"
|
|
else
|
|
log "DRY: $*"
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
match_glob_list() {
|
|
local name="$1" raw="$2" g
|
|
[[ -z "$raw" ]] && return 1
|
|
raw="${raw//,/ }"
|
|
for g in $raw; do
|
|
# shellcheck disable=SC2254
|
|
case "$name" in
|
|
$g) return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help) usage 0 ;;
|
|
--apply) APPLY=1; shift ;;
|
|
--dry-run) APPLY=0; shift ;;
|
|
--images-only) DO_IMAGES=1; DO_RECREATE=0; DO_OS=0; shift ;;
|
|
--os-only) DO_IMAGES=0; DO_RECREATE=0; DO_OS=1; shift ;;
|
|
--recreate-only) DO_IMAGES=0; DO_RECREATE=1; DO_OS=0; shift ;;
|
|
--taler-debs) DO_TALER_DEBS=1; shift ;;
|
|
--rebuild-local) REBUILD_LOCAL=1; shift ;;
|
|
--include-stopped) INCLUDE_STOPPED=1; shift ;;
|
|
--no-backup) DO_BACKUP=0; shift ;;
|
|
--backup-max-gb) BACKUP_MAX_GB="${2:-50}"; shift 2 ;;
|
|
--backup-max-gb=*) BACKUP_MAX_GB="${1#*=}"; shift ;;
|
|
--only) ONLY_RAW="${2:-}"; shift 2 ;;
|
|
--skip) SKIP_RAW="${2:-}"; shift 2 ;;
|
|
--only=*) ONLY_RAW="${1#*=}"; shift ;;
|
|
--skip=*) SKIP_RAW="${1#*=}"; shift ;;
|
|
*) log "ERROR: unknown arg: $1"; usage 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$(id -un)" != "hernani" ]] && [[ "${FORCE_USER:-}" != "1" ]]; then
|
|
log "WARN: expected user hernani (got $(id -un)); set FORCE_USER=1 to override"
|
|
fi
|
|
|
|
declare -a NAMES=()
|
|
declare -A IMG=()
|
|
declare -A CDIR=()
|
|
declare -A STATUS=() # ok|dry_fail|backup_fail|upgraded|skipped
|
|
declare -A NOTE=()
|
|
|
|
list_cmd=(podman ps --format '{{.Names}}|{{.Image}}')
|
|
if [[ "$INCLUDE_STOPPED" -eq 1 ]]; then
|
|
list_cmd=(podman ps -a --format '{{.Names}}|{{.Image}}')
|
|
fi
|
|
|
|
while IFS='|' read -r name image; do
|
|
[[ -z "$name" ]] && continue
|
|
if [[ ! "$name" =~ ^(koopa-|taler-|goa-|mytops-) ]]; then
|
|
continue
|
|
fi
|
|
if [[ -n "$ONLY_RAW" ]] && ! match_glob_list "$name" "$ONLY_RAW"; then
|
|
continue
|
|
fi
|
|
if match_glob_list "$name" "$SKIP_RAW"; then
|
|
log "SKIP name=$name (matched --skip)"
|
|
continue
|
|
fi
|
|
NAMES+=("$name")
|
|
IMG["$name"]="$image"
|
|
dir=$(podman inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$name" 2>/dev/null || true)
|
|
CDIR["$name"]="$dir"
|
|
STATUS["$name"]="pending"
|
|
done < <("${list_cmd[@]}")
|
|
|
|
if [[ ${#NAMES[@]} -eq 0 ]]; then
|
|
log "No matching containers."
|
|
exit 0
|
|
fi
|
|
|
|
log "MODE=$([ "$APPLY" -eq 1 ] && echo apply || echo dry-run) images=$DO_IMAGES recreate=$DO_RECREATE os=$DO_OS backup=$DO_BACKUP backup_max_gb=$BACKUP_MAX_GB"
|
|
log "Targets (${#NAMES[@]}): ${NAMES[*]}"
|
|
if [[ "$APPLY" -eq 1 && "$DO_BACKUP" -eq 1 ]]; then
|
|
mkdir -p "$BACKUP_RUN_DIR"
|
|
log "BACKUP_DIR=$BACKUP_RUN_DIR"
|
|
fi
|
|
|
|
is_pinned_tag() {
|
|
local ref="$1" tag="${1##*:}"
|
|
[[ "$ref" != *:* ]] && return 1
|
|
case "$tag" in
|
|
latest|stable|stable-*|main|master|landing|live|setup|rootless|*-rootless|alpine|*-alpine|[0-9]|[0-9][0-9]|[0-9].*-alpine|[0-9][0-9]-alpine) return 1 ;;
|
|
esac
|
|
if [[ "$tag" =~ ^[0-9]+\.[0-9]+ ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# --- prod-safe dry-run for one container (no recreate, no package mutate) ---
|
|
dry_run_one() {
|
|
local name="$1" ref="${IMG[$name]}" dir="${CDIR[$name]}"
|
|
local rc=0
|
|
local tmp
|
|
tmp=$(mktemp)
|
|
|
|
# running?
|
|
if ! podman inspect "$name" >/dev/null 2>&1; then
|
|
log "DRYFAIL $name: inspect failed"
|
|
NOTE["$name"]="inspect failed"
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
|
|
# image phase checks
|
|
if [[ "$DO_IMAGES" -eq 1 ]]; then
|
|
if [[ "$ref" == localhost/* ]]; then
|
|
if ! podman image exists "$ref" 2>/dev/null; then
|
|
log "DRYFAIL $name: local image missing $ref"
|
|
NOTE["$name"]="local image missing"
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
log "DRYOK $name: local image present $ref"
|
|
else
|
|
# pull --dry-run is not universal; probe registry without replacing running CT
|
|
if podman pull --help 2>&1 | grep -q -- '--quiet'; then
|
|
:
|
|
fi
|
|
if ! podman image exists "$ref" 2>/dev/null; then
|
|
log "DRYWARN $name: image not local yet ($ref) — apply will pull"
|
|
else
|
|
log "DRYOK $name: image present locally $ref"
|
|
fi
|
|
# non-mutating registry probe when possible
|
|
if command -v curl >/dev/null 2>&1; then
|
|
:
|
|
fi
|
|
# Try a no-restart pull into local store is mutating cache but not prod CT.
|
|
# For dry-run we only validate the ref string / inspect local digest.
|
|
if is_pinned_tag "$ref"; then
|
|
log "DRYOK $name: pinned tag $ref (compose bump needed to move)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# compose config validate (no up)
|
|
if [[ "$DO_RECREATE" -eq 1 ]]; then
|
|
if [[ -n "$dir" ]]; then
|
|
if [[ ! -f "$dir/compose.yml" && ! -f "$dir/compose.yaml" && ! -f "$dir/docker-compose.yml" ]]; then
|
|
log "DRYFAIL $name: compose dir without compose file: $dir"
|
|
NOTE["$name"]="no compose file"
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
if ! (cd "$dir" && podman-compose config >/dev/null 2>"$tmp"); then
|
|
log "DRYFAIL $name: podman-compose config failed"
|
|
sed -n '1,20p' "$tmp" | while IFS= read -r line; do log " $line"; done
|
|
NOTE["$name"]="compose config failed"
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
log "DRYOK $name: compose config ok ($dir)"
|
|
else
|
|
log "DRYOK $name: no compose label — recreate will be skipped"
|
|
fi
|
|
fi
|
|
|
|
# OS simulate (no real upgrade)
|
|
if [[ "$DO_OS" -eq 1 ]]; then
|
|
if podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
|
local os_script
|
|
os_script=$(cat <<'INNER'
|
|
set -e
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
apt-get update -qq
|
|
apt-get -s -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade >/tmp/os-dry.out
|
|
echo OS_DRY=apt ok
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
apk update >/dev/null
|
|
apk upgrade --simulate >/tmp/os-dry.out 2>&1 || apk upgrade -s >/tmp/os-dry.out 2>&1 || true
|
|
echo OS_DRY=apk ok
|
|
elif command -v zypper >/dev/null 2>&1; then
|
|
zypper -n refresh >/dev/null
|
|
zypper -n update -D >/tmp/os-dry.out 2>&1 || true
|
|
echo OS_DRY=zypper ok
|
|
elif command -v microdnf >/dev/null 2>&1; then
|
|
echo OS_DRY=microdnf none-sim
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
dnf -y check-update >/tmp/os-dry.out 2>&1 || true
|
|
echo OS_DRY=dnf ok
|
|
else
|
|
echo OS_DRY=none
|
|
fi
|
|
INNER
|
|
)
|
|
if podman exec -u 0 "$name" bash -lc "$os_script" >"$tmp" 2>&1 \
|
|
|| podman exec -u 0 "$name" sh -lc "$os_script" >"$tmp" 2>&1; then
|
|
log "DRYOK $name: OS simulate ($(grep -E '^OS_DRY=' "$tmp" | tail -1 || echo ok))"
|
|
else
|
|
# many distroless/app images have no pkg manager — treat as ok/skip
|
|
if grep -q 'OS_DRY=none' "$tmp" 2>/dev/null; then
|
|
log "DRYOK $name: no package manager (OS skip)"
|
|
else
|
|
log "DRYWARN $name: OS simulate soft-fail (will try apply carefully)"
|
|
sed -n '1,8p' "$tmp" | while IFS= read -r line; do log " $line"; done
|
|
fi
|
|
fi
|
|
else
|
|
log "DRYOK $name: not running — OS phase will skip"
|
|
fi
|
|
fi
|
|
|
|
rm -f "$tmp"
|
|
return "$rc"
|
|
}
|
|
|
|
# --- full backup with data ---
|
|
backup_one() {
|
|
local name="$1"
|
|
local dest="$BACKUP_RUN_DIR/$name"
|
|
local meta="$dest/META.txt"
|
|
local max_bytes=$((BACKUP_MAX_GB * 1024 * 1024 * 1024))
|
|
mkdir -p "$dest/binds" "$dest/volumes" "$dest/db" "$dest/compose"
|
|
|
|
{
|
|
echo "name=$name"
|
|
echo "image=${IMG[$name]}"
|
|
echo "compose_dir=${CDIR[$name]:-}"
|
|
echo "ts=$RUN_TS"
|
|
} >"$meta"
|
|
|
|
podman inspect "$name" >"$dest/inspect.json" 2>/dev/null || true
|
|
|
|
# compose project files
|
|
local dir="${CDIR[$name]}"
|
|
if [[ -n "$dir" && -d "$dir" ]]; then
|
|
for f in compose.yml compose.yaml docker-compose.yml docker-compose.yaml .env; do
|
|
if [[ -f "$dir/$f" ]]; then
|
|
cp -a "$dir/$f" "$dest/compose/$f"
|
|
fi
|
|
done
|
|
# shallow copy of known config sidecars (not whole tree)
|
|
for f in Caddyfile Containerfile Dockerfile; do
|
|
[[ -f "$dir/$f" ]] && cp -a "$dir/$f" "$dest/compose/$f"
|
|
done
|
|
fi
|
|
|
|
# bind mounts + named volumes from inspect
|
|
local mounts
|
|
mounts=$(podman inspect -f '{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}|{{.RW}}{{println}}{{end}}' "$name" 2>/dev/null || true)
|
|
while IFS='|' read -r mtype mname src dst rw; do
|
|
[[ -z "$mtype" ]] && continue
|
|
# skip ephemeral / host tz
|
|
case "$dst" in
|
|
/etc/localtime|/etc/timezone|/run|/run/*|/tmp|/tmp/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*) continue ;;
|
|
esac
|
|
case "$src" in
|
|
/usr/share/zoneinfo/*|/etc/localtime) continue ;;
|
|
esac
|
|
|
|
if [[ "$mtype" == "bind" ]]; then
|
|
if [[ ! -e "$src" ]]; then
|
|
log "BACKUPWARN $name: bind missing $src"
|
|
continue
|
|
fi
|
|
local sz
|
|
sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
|
|
if [[ -n "$sz" && "$sz" -gt "$max_bytes" ]]; then
|
|
log "BACKUPWARN $name: skip huge bind $src (${sz}B > ${BACKUP_MAX_GB}G); raise --backup-max-gb"
|
|
echo "SKIP_HUGE_BIND $src size=$sz" >>"$meta"
|
|
continue
|
|
fi
|
|
local base
|
|
base=$(echo "$dst" | sed 's#^/##; s#/#_#g')
|
|
[[ -z "$base" ]] && base="root"
|
|
log "BACKUP $name: bind $src → binds/${base}.tar.gz"
|
|
if ! tar -C "$(dirname "$src")" -czf "$dest/binds/${base}.tar.gz" "$(basename "$src")" 2>>"$dest/backup.err"; then
|
|
log "BACKUPFAIL $name: tar bind $src"
|
|
NOTE["$name"]="backup bind failed: $src"
|
|
return 1
|
|
fi
|
|
echo "BIND $src -> binds/${base}.tar.gz" >>"$meta"
|
|
elif [[ "$mtype" == "volume" ]]; then
|
|
local vname="${mname:-}"
|
|
if [[ -z "$vname" ]]; then
|
|
# fallback: export via source path
|
|
if [[ -d "$src" ]]; then
|
|
local vbase
|
|
vbase=$(basename "$(dirname "$src")")
|
|
log "BACKUP $name: volume-path $src → volumes/${vbase}.tar.gz"
|
|
if ! tar -C "$src" -czf "$dest/volumes/${vbase}.tar.gz" . 2>>"$dest/backup.err"; then
|
|
log "BACKUPFAIL $name: tar volume path $src"
|
|
NOTE["$name"]="backup volume failed"
|
|
return 1
|
|
fi
|
|
echo "VOLPATH $src -> volumes/${vbase}.tar.gz" >>"$meta"
|
|
fi
|
|
continue
|
|
fi
|
|
local sz
|
|
sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
|
|
if [[ -n "$sz" && "$sz" -gt "$max_bytes" ]]; then
|
|
log "BACKUPWARN $name: skip huge volume $vname (${sz}B > ${BACKUP_MAX_GB}G)"
|
|
echo "SKIP_HUGE_VOL $vname size=$sz" >>"$meta"
|
|
continue
|
|
fi
|
|
log "BACKUP $name: volume $vname → volumes/${vname}.tar"
|
|
if podman volume export "$vname" -o "$dest/volumes/${vname}.tar" 2>>"$dest/backup.err"; then
|
|
gzip -f "$dest/volumes/${vname}.tar" 2>/dev/null || true
|
|
echo "VOLUME $vname -> volumes/${vname}.tar.gz" >>"$meta"
|
|
else
|
|
# fallback tar of mount source
|
|
if [[ -d "$src" ]] && tar -C "$src" -czf "$dest/volumes/${vname}.tar.gz" . 2>>"$dest/backup.err"; then
|
|
echo "VOLPATH $src -> volumes/${vname}.tar.gz" >>"$meta"
|
|
else
|
|
log "BACKUPFAIL $name: volume $vname"
|
|
NOTE["$name"]="backup volume failed: $vname"
|
|
return 1
|
|
fi
|
|
fi
|
|
fi
|
|
done <<<"$mounts"
|
|
|
|
# DB dumps when obvious
|
|
if podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
|
local img_l
|
|
img_l=$(echo "${IMG[$name]}" | tr '[:upper:]' '[:lower:]')
|
|
case "$img_l" in
|
|
*postgres*|*postgis*)
|
|
local dbuser
|
|
dbuser=$(podman exec "$name" printenv POSTGRES_USER 2>/dev/null || echo postgres)
|
|
local dbname
|
|
dbname=$(podman exec "$name" printenv POSTGRES_DB 2>/dev/null || echo postgres)
|
|
log "BACKUP $name: pg_dumpall"
|
|
if podman exec -u 0 "$name" bash -lc "pg_dumpall -U '$dbuser'" >"$dest/db/pg_dumpall.sql" 2>>"$dest/backup.err"; then
|
|
gzip -f "$dest/db/pg_dumpall.sql"
|
|
echo "PG_DUMPALL ok db=$dbname user=$dbuser" >>"$meta"
|
|
else
|
|
# single-db fallback
|
|
if podman exec -u 0 "$name" bash -lc "pg_dump -U '$dbuser' '$dbname'" >"$dest/db/pg_dump.sql" 2>>"$dest/backup.err"; then
|
|
gzip -f "$dest/db/pg_dump.sql"
|
|
echo "PG_DUMP ok db=$dbname" >>"$meta"
|
|
else
|
|
log "BACKUPWARN $name: postgres dump failed (mounts still saved)"
|
|
fi
|
|
fi
|
|
;;
|
|
*mariadb*|*mysql*)
|
|
local muser mpass mdb
|
|
muser=$(podman exec "$name" printenv MYSQL_USER 2>/dev/null || podman exec "$name" printenv MARIADB_USER 2>/dev/null || echo root)
|
|
mpass=$(podman exec "$name" printenv MYSQL_ROOT_PASSWORD 2>/dev/null || podman exec "$name" printenv MARIADB_ROOT_PASSWORD 2>/dev/null || podman exec "$name" printenv MYSQL_PASSWORD 2>/dev/null || true)
|
|
mdb=$(podman exec "$name" printenv MYSQL_DATABASE 2>/dev/null || podman exec "$name" printenv MARIADB_DATABASE 2>/dev/null || echo "")
|
|
log "BACKUP $name: mariadb/mysql dump"
|
|
if [[ -n "$mpass" ]]; then
|
|
if podman exec -u 0 "$name" bash -lc "MYSQL_PWD='$mpass' mysqldump -u root --all-databases" >"$dest/db/mysql_all.sql" 2>>"$dest/backup.err" \
|
|
|| podman exec -u 0 "$name" bash -lc "MYSQL_PWD='$mpass' mariadb-dump -u root --all-databases" >"$dest/db/mysql_all.sql" 2>>"$dest/backup.err"; then
|
|
gzip -f "$dest/db/mysql_all.sql"
|
|
echo "MYSQL_DUMPALL ok" >>"$meta"
|
|
else
|
|
log "BACKUPWARN $name: mysql dump failed (mounts still saved)"
|
|
fi
|
|
else
|
|
log "BACKUPWARN $name: no mysql password in env — skip dump"
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo "BACKUP_OK" >>"$meta"
|
|
log "BACKUPOK $name → $dest"
|
|
return 0
|
|
}
|
|
|
|
# --- apply phases for one container ---
|
|
apply_images_one() {
|
|
local name="$1" ref="${IMG[$name]}"
|
|
[[ "$DO_IMAGES" -eq 1 ]] || return 0
|
|
if [[ "$ref" == localhost/* ]]; then
|
|
log "LOCAL image=$ref (container=$name) — pull skipped"
|
|
return 0
|
|
fi
|
|
if is_pinned_tag "$ref"; then
|
|
log "PINNED image=$ref (container=$name) — pull same tag only"
|
|
fi
|
|
run podman pull "$ref"
|
|
}
|
|
|
|
rebuild_one_local() {
|
|
local name="$1" ref="${IMG[$name]}" dir="${CDIR[$name]}"
|
|
[[ "$ref" == localhost/* ]] || return 0
|
|
local cf=""
|
|
if [[ -n "$dir" && -f "$dir/Containerfile" ]]; then
|
|
cf="$dir/Containerfile"
|
|
elif [[ -n "$dir" && -f "$dir/Dockerfile" ]]; then
|
|
cf="$dir/Dockerfile"
|
|
fi
|
|
case "$name" in
|
|
koopa-nym)
|
|
[[ -x "$ADMIN_LOG/scripts/nym/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/nym/build.sh"; return; }
|
|
;;
|
|
koopa-tor-relay)
|
|
if [[ -f "$HOME_DIR/koopa-tor-relay/build/Containerfile" ]]; then
|
|
run podman build -t localhost/koopa-tor-relay:latest -f "$HOME_DIR/koopa-tor-relay/build/Containerfile" "$HOME_DIR/koopa-tor-relay/build"
|
|
return
|
|
fi
|
|
;;
|
|
koopa-paivana)
|
|
if [[ -f "$dir/Containerfile" ]]; then
|
|
run podman build -t localhost/koopa-paivana:latest -f "$dir/Containerfile" "$dir"
|
|
return
|
|
fi
|
|
;;
|
|
koopa-bbb-front)
|
|
[[ -x "$ADMIN_LOG/scripts/bbb/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/bbb/build.sh"; return; }
|
|
;;
|
|
koopa-meet-web)
|
|
[[ -x "$ADMIN_LOG/scripts/meet/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/meet/build.sh"; return; }
|
|
;;
|
|
koopa-galene|koopa-paivana-galene)
|
|
[[ -x "$ADMIN_LOG/scripts/galene/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/galene/build.sh"; return; }
|
|
;;
|
|
koopa-cryptpad|koopa-cryptpad-edge|koopa-paivana-cryptpad)
|
|
[[ -x "$ADMIN_LOG/scripts/cryptpad/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/cryptpad/build.sh"; return; }
|
|
;;
|
|
koopa-notes|koopa-silverbullet)
|
|
[[ -x "$ADMIN_LOG/scripts/notes/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/notes/build.sh"; return; }
|
|
;;
|
|
esac
|
|
if [[ -n "$cf" ]]; then
|
|
run podman build -t "$ref" -f "$cf" "$(dirname "$cf")"
|
|
else
|
|
log "NO-REBUILD path for $name ($ref)"
|
|
fi
|
|
}
|
|
|
|
apply_recreate_one() {
|
|
local name="$1" dir="${CDIR[$name]}"
|
|
[[ "$DO_RECREATE" -eq 1 ]] || return 0
|
|
if [[ -z "$dir" ]]; then
|
|
log "NO-COMPOSE container=$name — recreate skipped"
|
|
return 0
|
|
fi
|
|
if [[ ! -f "$dir/compose.yml" && ! -f "$dir/compose.yaml" && ! -f "$dir/docker-compose.yml" ]]; then
|
|
log "WARN: compose dir without compose file: $dir"
|
|
return 0
|
|
fi
|
|
if [[ "$APPLY" -eq 1 ]]; then
|
|
log "RUN: (cd $dir && podman-compose pull && podman-compose up -d)"
|
|
(cd "$dir" && podman-compose pull && podman-compose up -d)
|
|
else
|
|
log "DRY: (cd $dir && podman-compose pull && podman-compose up -d)"
|
|
fi
|
|
}
|
|
|
|
os_upgrade_one() {
|
|
local name="$1"
|
|
[[ "$DO_OS" -eq 1 ]] || return 0
|
|
if ! podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
|
log "SKIP os name=$name (not running)"
|
|
return 0
|
|
fi
|
|
local script
|
|
script=$(cat <<'INNER'
|
|
set -e
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
apt-get update -qq
|
|
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade
|
|
echo OS=apt ok
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
apk update
|
|
apk upgrade
|
|
echo OS=apk ok
|
|
elif command -v zypper >/dev/null 2>&1; then
|
|
zypper -n refresh
|
|
zypper -n update
|
|
echo OS=zypper ok
|
|
elif command -v microdnf >/dev/null 2>&1; then
|
|
microdnf -y upgrade
|
|
echo OS=microdnf ok
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
dnf -y upgrade
|
|
echo OS=dnf ok
|
|
else
|
|
echo OS=none
|
|
exit 0
|
|
fi
|
|
INNER
|
|
)
|
|
if [[ "$APPLY" -eq 1 ]]; then
|
|
log "RUN: podman exec -u 0 $name OS-upgrade"
|
|
if ! podman exec -u 0 "$name" bash -lc "$script"; then
|
|
podman exec -u 0 "$name" sh -lc "$script" || log "WARN: OS upgrade failed for $name"
|
|
fi
|
|
else
|
|
log "DRY: podman exec -u 0 $name OS-upgrade"
|
|
fi
|
|
}
|
|
|
|
# Deduplicate compose recreate by dir
|
|
declare -A COMPOSE_DONE=()
|
|
declare -A IMAGE_DONE=()
|
|
declare -A BACKUP_DONE=()
|
|
|
|
process_one() {
|
|
local name="$1"
|
|
log "==== target: $name (${IMG[$name]}) ===="
|
|
|
|
# 1) always dry-run first (prod-safe)
|
|
if ! dry_run_one "$name"; then
|
|
STATUS["$name"]="dry_fail"
|
|
log "SKIP apply $name (dry-run failed)"
|
|
return 0
|
|
fi
|
|
STATUS["$name"]="dry_ok"
|
|
|
|
# global dry-run mode: stop after checks
|
|
if [[ "$APPLY" -eq 0 ]]; then
|
|
log "DRY-RUN only for $name (no backup/apply)"
|
|
return 0
|
|
fi
|
|
|
|
# 2) full backup with data BEFORE any mutate (even after dry-run ok)
|
|
# every container — do not skip siblings in a compose stack (DB/data)
|
|
if [[ "$DO_BACKUP" -eq 1 ]]; then
|
|
if [[ -z "${BACKUP_DONE[$name]:-}" ]]; then
|
|
if ! backup_one "$name"; then
|
|
STATUS["$name"]="backup_fail"
|
|
log "SKIP apply $name (backup failed)"
|
|
return 0
|
|
fi
|
|
BACKUP_DONE["$name"]=1
|
|
else
|
|
log "BACKUP skip $name (already backed this container)"
|
|
fi
|
|
else
|
|
log "WARN: --no-backup set; proceeding without backup for $name"
|
|
fi
|
|
|
|
# 3) apply
|
|
if [[ -z "${IMAGE_DONE[${IMG[$name]}]:-}" ]]; then
|
|
apply_images_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="image pull failed"; return 0; }
|
|
IMAGE_DONE["${IMG[$name]}"]=1
|
|
fi
|
|
if [[ "$REBUILD_LOCAL" -eq 1 ]]; then
|
|
rebuild_one_local "$name" || true
|
|
fi
|
|
local dir="${CDIR[$name]}"
|
|
if [[ -n "$dir" ]]; then
|
|
if [[ -z "${COMPOSE_DONE[$dir]:-}" ]]; then
|
|
apply_recreate_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="recreate failed"; return 0; }
|
|
COMPOSE_DONE["$dir"]=1
|
|
else
|
|
log "recreate skip $name (compose dir already done)"
|
|
fi
|
|
else
|
|
apply_recreate_one "$name" || true
|
|
fi
|
|
os_upgrade_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="os upgrade failed"; return 0; }
|
|
STATUS["$name"]="upgraded"
|
|
log "DONE $name"
|
|
}
|
|
|
|
# --- main loop: per container ---
|
|
for name in "${NAMES[@]}"; do
|
|
process_one "$name"
|
|
done
|
|
|
|
# optional Taler GOA debs (only on --apply, after per-CT work)
|
|
if [[ "$DO_TALER_DEBS" -eq 1 ]]; then
|
|
log "==== phase: taler-debs ===="
|
|
tscript="$ADMIN_LOG/scripts/taler-shared/upgrade-goa-debs.sh"
|
|
if [[ "$APPLY" -eq 0 ]]; then
|
|
log "DRY: bash $tscript"
|
|
elif [[ -x "$tscript" ]]; then
|
|
run bash "$tscript"
|
|
else
|
|
log "ERROR: missing $tscript"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
log "==== summary ===="
|
|
fail=0
|
|
for name in "${NAMES[@]}"; do
|
|
st="${STATUS[$name]:-?}"
|
|
printf ' %-42s %-12s %s %s\n' "$name" "$st" "${IMG[$name]}" "${NOTE[$name]:-}"
|
|
case "$st" in
|
|
dry_fail|backup_fail|apply_fail) fail=1 ;;
|
|
esac
|
|
done
|
|
if [[ "$APPLY" -eq 0 ]]; then
|
|
log "Dry-run only (default). Re-run with --apply for backup+upgrade per target that passed dry-run."
|
|
fi
|
|
if [[ "$fail" -eq 1 ]]; then
|
|
log "STATUS=partial_fail"
|
|
exit 1
|
|
fi
|
|
log "STATUS=ok"
|