#!/usr/bin/env bash # GPS-Denied Onboard — verify the running service stack is healthy. # # The companion has no inbound HTTP `/health/*` endpoint (NFT-SEC-05 — # in-flight egress lockdown). Health is read from the Docker # HEALTHCHECK status (exec-based `python3 -m gps_denied_onboard.healthcheck` # per deployment_procedures.md § Health Checks). # # Usage: # scripts/health-check.sh [--target dev|airborne|operator-workstation] # [--compose-file ] [--help] # # Defaults: # target = dev # compose-file = docker-compose.yml # # Exit codes: # 0 all services healthy (or running with no declared healthcheck) # 1 at least one service is unhealthy # 2 at least one service is missing (compose up was not run, or container exited) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" usage() { cat <<'EOF' GPS-Denied Onboard — verify the running service stack is healthy. The companion has no inbound HTTP `/health/*` endpoint (NFT-SEC-05 — in-flight egress lockdown). Health is read from the Docker HEALTHCHECK status (exec-based `python3 -m gps_denied_onboard.healthcheck` per deployment_procedures.md § Health Checks). Usage: scripts/health-check.sh [--target dev|airborne|operator-workstation] [--compose-file ] [--help] Defaults: target = dev compose-file = docker-compose.yml Exit codes: 0 all services healthy (or running with no declared healthcheck) 1 at least one service is unhealthy 2 at least one service is missing (compose up was not run, or container exited) EOF exit 0 } TARGET="dev" COMPOSE_FILE="" while [ $# -gt 0 ]; do case "$1" in --target) TARGET="$2"; shift 2 ;; --compose-file) COMPOSE_FILE="$2"; shift 2 ;; --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 64 fi remote_exec="ssh ${DEPLOY_HOST}" fi echo "[health-check] target: ${TARGET}" echo "[health-check] compose-file: ${COMPOSE_FILE}" # Format: \t\t status_lines=$(${remote_exec} docker compose -f "${COMPOSE_FILE}" ps -a \ --format '{{.Service}}{{"\t"}}{{.State}}{{"\t"}}{{.Health}}' 2>/dev/null \ || true) if [ -z "${status_lines}" ]; then echo "ERROR: no containers found for ${COMPOSE_FILE}" >&2 echo " Did you run scripts/start-services.sh?" >&2 exit 2 fi rc=0 printf '%-30s %-12s %-12s\n' SERVICE STATE HEALTH printf '%-30s %-12s %-12s\n' "------------------------------" "------------" "------------" while IFS=$'\t' read -r service state health; do [ -z "${service}" ] && continue if [ -z "${health}" ]; then health_display="(none)" else health_display="${health}" fi printf '%-30s %-12s %-12s\n' "${service}" "${state}" "${health_display}" if [ "${state}" != "running" ]; then rc=2 elif [ -n "${health}" ] && [ "${health}" != "healthy" ]; then rc=1 fi done <<< "${status_lines}" case "${rc}" in 0) echo "[health-check] OK" ;; 1) echo "[health-check] UNHEALTHY — at least one service failed HEALTHCHECK" >&2 ;; 2) echo "[health-check] MISSING — at least one service is not running" >&2 ;; esac exit "${rc}"