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.
This commit is contained in:
+10
-3
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user