#!/usr/bin/env bash # GPS-Denied Onboard — graceful shutdown of the service stack. # # Refuses to stop the airborne stack when /run/azaion/in-flight is set # (deployment_procedures.md § Deployment Strategy — ground-only safety # gate). Saves the current image SHAs to .previous-tags.env so deploy.sh # --rollback can restore them. # # Honours the 30 s stop_grace_period declared in docker-compose.yml so # C13 can emit flight_footer{clean_shutdown=true} per # deployment_procedures.md § Graceful Shutdown. # # Usage: # scripts/stop-services.sh [--target dev|airborne|operator-workstation] # [--compose-file ] [--force] [--help] # # Defaults: # target = dev # compose-file = docker-compose.yml # # Exit codes: # 0 stack stopped cleanly (or already stopped) # 64 missing prerequisite (docker / compose file) # 65 SSH unreachable when DEPLOY_HOST is set # 68 refused — /run/azaion/in-flight is set and --force was not passed set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" usage() { cat <<'EOF' GPS-Denied Onboard — graceful shutdown of the service stack. Refuses to stop the airborne stack when /run/azaion/in-flight is set (deployment_procedures.md § Deployment Strategy — ground-only safety gate). Saves the current image SHAs to .previous-tags.env so deploy.sh --rollback can restore them. Honours the 30 s stop_grace_period declared in docker-compose.yml so C13 can emit flight_footer{clean_shutdown=true} per deployment_procedures.md § Graceful Shutdown. Usage: scripts/stop-services.sh [--target dev|airborne|operator-workstation] [--compose-file ] [--force] [--help] Defaults: target = dev compose-file = docker-compose.yml Exit codes: 0 stack stopped cleanly (or already stopped) 64 missing prerequisite (docker / compose file) 65 SSH unreachable when DEPLOY_HOST is set 68 refused — /run/azaion/in-flight is set and --force was not passed EOF exit 0 } TARGET="dev" COMPOSE_FILE="" FORCE=0 while [ $# -gt 0 ]; do case "$1" in --target) TARGET="$2"; shift 2 ;; --compose-file) COMPOSE_FILE="$2"; shift 2 ;; --force) FORCE=1; shift ;; --help|-h) usage ;; *) echo "ERROR: unknown argument: $1" >&2; usage ;; esac done if [ -z "${COMPOSE_FILE}" ]; then case "${TARGET}" in dev) COMPOSE_FILE="docker-compose.yml" ;; airborne) COMPOSE_FILE="${AIRBORNE_COMPOSE_FILE:-/etc/gps-denied/docker-compose.airborne.yml}" ;; operator-workstation) COMPOSE_FILE="docker-compose.yml" ;; *) echo "ERROR: invalid --target: ${TARGET}" >&2; exit 64 ;; esac fi remote_exec="" if [ -n "${DEPLOY_HOST:-}" ]; then if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "${DEPLOY_HOST}" true 2>/dev/null; then echo "ERROR: cannot reach 'ssh ${DEPLOY_HOST}' non-interactively." >&2 exit 65 fi remote_exec="ssh ${DEPLOY_HOST}" fi if [ "${TARGET}" = "airborne" ]; then if ${remote_exec} test -e /run/azaion/in-flight 2>/dev/null; then if [ "${FORCE}" = "1" ]; then echo "WARNING: /run/azaion/in-flight is set but --force was passed —" >&2 echo " continuing. This will abort a live flight pipeline." >&2 else echo "ERROR: /run/azaion/in-flight is set on the target host." >&2 echo " Refusing to stop the airborne stack mid-flight." >&2 echo " Wait for the flight to end, or pass --force to override." >&2 exit 68 fi fi fi if ! ${remote_exec} command -v docker >/dev/null 2>&1; then echo "ERROR: docker not found on $( [ -n "${remote_exec}" ] && echo "remote" || echo "local" ) host" >&2 exit 64 fi echo "[stop-services] target: ${TARGET}" echo "[stop-services] compose-file: ${COMPOSE_FILE}" PREV_TAGS_FILE="${REPO_ROOT}/.previous-tags.env" echo "[stop-services] saving current image digests to ${PREV_TAGS_FILE}" { echo "# Saved by scripts/stop-services.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "# Used by deploy.sh --rollback to restore the previous image set." echo "# Service tag layout: PREV__IMAGE=@" ${remote_exec} docker compose -f "${COMPOSE_FILE}" config --services 2>/dev/null \ | while read -r service; do image=$(${remote_exec} docker compose -f "${COMPOSE_FILE}" \ images --format '{{.Repository}}@{{.ID}}' "${service}" 2>/dev/null \ | head -n1 || true) [ -n "${image}" ] || continue key=$(echo "PREV_${service}_IMAGE" | tr '[:lower:]-' '[:upper:]_') echo "${key}=${image}" done } > "${PREV_TAGS_FILE}" echo "[stop-services] docker compose down (graceful, 30s grace per compose stop_grace_period)" ${remote_exec} docker compose -f "${COMPOSE_FILE}" down --remove-orphans echo "[stop-services] OK"