From 0e677d52577e3f3849f987d44e1ba541cd01b03b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Aug 2026 12:56:59 +0000 Subject: [PATCH] Pin update.sh to a specific version, never :latest (ADR-0051) :latest is three independently-mutable registry tags with no cross-image transaction -- update.sh pulling it directly could observe a torn set mid-promotion (api already retagged, ui/proxy not yet). latest-version.json is a single, atomically-committed pointer that argus-appliance's release pipeline only ever writes AFTER every image it names has finished promoting, so resolving it first cannot observe a partial release: by the time it names a version, that version is already guaranteed complete. docker-compose.yml: image tags now ${ARGUS_VERSION:-latest} -- default unchanged for manual/non-update.sh use (plain `docker compose pull` with no env var set behaves exactly as before). update.sh: resolves latest-version.json first (fails loud and untouched if unreachable/malformed, never falls back to :latest), exports ARGUS_VERSION for its own docker compose calls only (not written to .env), and -- new -- verifies every service's actual running image tag matches the target version before declaring success, not just that the API container itself reports healthy. A torn pull (network hiccup, disk full on one image) is now a loud failure instead of a silently-reported success. --- docker-compose.yml | 13 +++++++-- update.sh | 72 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3f144ac..0a2a523 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,7 +92,12 @@ services: # Go API Server api: - image: git-cloud.weboria.eu/weboria/argus-api:latest + # ARGUS_VERSION is set by update.sh before `docker compose pull`, pinned + # to a specific already-fully-promoted version tag rather than the + # floating :latest (ADR-0051 -- argus-appliance repo). Manual/non- + # update.sh use (plain `docker compose pull`, no env var set) is + # unaffected: the default is :latest, identical to before this change. + image: git-cloud.weboria.eu/weboria/argus-api:${ARGUS_VERSION:-latest} container_name: argus-api restart: unless-stopped # Container hardening: no-new-privileges blocks setuid privilege @@ -180,7 +185,8 @@ services: # React UI (Admin Panel), served over HTTPS ui: - image: git-cloud.weboria.eu/weboria/argus-ui:latest + # See the api service's own comment above -- same ADR-0051 pinning. + image: git-cloud.weboria.eu/weboria/argus-ui:${ARGUS_VERSION:-latest} container_name: argus-ui restart: unless-stopped security_opt: @@ -206,7 +212,8 @@ services: # Nginx reverse proxy — host network mode, so real client IPs are visible # directly without needing PROXY protocol. nginx: - image: git-cloud.weboria.eu/weboria/argus-proxy:latest + # See the api service's own comment above -- same ADR-0051 pinning. + image: git-cloud.weboria.eu/weboria/argus-proxy:${ARGUS_VERSION:-latest} container_name: argus-proxy restart: always network_mode: host diff --git a/update.sh b/update.sh index 043d094..b030144 100755 --- a/update.sh +++ b/update.sh @@ -1,15 +1,23 @@ #!/usr/bin/env bash # -# ARGUS Enterprise Web Security Platform — updater. Pulls the latest -# published images and restarts the stack. +# ARGUS Enterprise Web Security Platform — updater. # # cd /opt/argus && ./update.sh # (or, if you don't have it locally yet) # curl -fsSL https://git-cloud.weboria.eu/weboria/argus-installer/raw/branch/main/update.sh | bash # +# ADR-0051 (argus-appliance repo): this pulls a specific, pinned version -- +# never :latest. :latest is three independently-mutable registry tags with +# no cross-image transaction; a version resolved from latest-version.json +# is a single atomically-committed pointer that the release pipeline only +# ever writes AFTER every image it names has finished promoting. Reading +# :latest directly (the old behavior here) could observe a torn set mid- +# promotion; reading the manifest first cannot, because by the time it +# names a version, that version's images are already guaranteed complete. set -euo pipefail INSTALL_DIR="${ARGUS_INSTALL_DIR:-/opt/argus}" +MANIFEST_URL="https://git-cloud.weboria.eu/weboria/argus-installer/raw/branch/main/latest-version.json" bold() { printf '\033[1m%s\033[0m\n' "$1"; } info() { printf ' %s\n' "$1"; } @@ -28,7 +36,26 @@ API_HOST_PORT="${API_HOST_PORT:-9080}" BEFORE_VERSION="$(curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")" info "Current version: ${BEFORE_VERSION}" -bold "Pulling latest images..." +bold "Resolving target version..." +MANIFEST="$(curl -fsS "$MANIFEST_URL" 2>/dev/null)" \ + || fail "Could not reach the update manifest ($MANIFEST_URL). Nothing was changed -- your installation is untouched. Check your network connection and try again." + +TARGET_VERSION="$(printf '%s' "$MANIFEST" | grep -oP '"version"\s*:\s*"\K[^"]+' || true)" +[ -n "$TARGET_VERSION" ] || fail "The update manifest was malformed (no 'version' field). Nothing was changed -- your installation is untouched." +info "Target version: ${TARGET_VERSION}" + +if [ "$BEFORE_VERSION" = "$TARGET_VERSION" ]; then + ok "Already up to date (${TARGET_VERSION})." + exit 0 +fi + +# Pinned, not :latest -- see the header comment. Exported only for this +# script's own `docker compose` invocations below, not written to .env, so +# a later manual `docker compose pull` by the operator still defaults to +# :latest exactly as it always has. +export ARGUS_VERSION="$TARGET_VERSION" + +bold "Pulling ${TARGET_VERSION} (pinned, not :latest)..." docker compose pull bold "Restarting..." @@ -50,9 +77,38 @@ if [ "$READY" != "true" ]; then fi AFTER_VERSION="$(curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")" -ok "ARGUS is healthy — now running version ${AFTER_VERSION}" -if [ "$BEFORE_VERSION" != "$AFTER_VERSION" ]; then - info "Updated: ${BEFORE_VERSION} → ${AFTER_VERSION}" -else - info "Already up to date." + +# The API being healthy only proves the API container itself came up on +# some version -- it says nothing about whether the UI and proxy images +# actually match. A torn pull (network hiccup mid-pull, disk full on one +# image, etc.) would otherwise be reported as a clean success. Check every +# service's actual running image tag against the target explicitly. +bold "Verifying every service is running ${TARGET_VERSION}..." +INCONSISTENT=false +for svc in api ui nginx; do + CONTAINER="$(docker compose ps -q "$svc" 2>/dev/null || true)" + if [ -z "$CONTAINER" ]; then + info " ✗ ${svc}: container not found" + INCONSISTENT=true + continue + fi + RUNNING_IMAGE="$(docker inspect --format '{{.Config.Image}}' "$CONTAINER" 2>/dev/null || echo "")" + RUNNING_TAG="${RUNNING_IMAGE##*:}" + if [ "$RUNNING_TAG" = "$TARGET_VERSION" ]; then + info " ✓ ${svc}: ${RUNNING_TAG}" + else + info " ✗ ${svc}: ${RUNNING_TAG:-unknown} (expected ${TARGET_VERSION})" + INCONSISTENT=true + fi +done + +if [ "$INCONSISTENT" = "true" ]; then + fail "Update did not complete cleanly -- not every service is running ${TARGET_VERSION}. Your services were NOT automatically rolled back; investigate (docker compose ps, docker compose logs) before retrying." fi + +if [ "$AFTER_VERSION" != "$TARGET_VERSION" ]; then + fail "Every service's image is ${TARGET_VERSION}, but the API's own /health still reports ${AFTER_VERSION} -- it may not have finished restarting. Check: docker compose -f $INSTALL_DIR/docker-compose.yml logs api" +fi + +ok "ARGUS is healthy and every service is confirmed running ${AFTER_VERSION}" +info "Updated: ${BEFORE_VERSION} → ${AFTER_VERSION}"