#!/usr/bin/env bash
# vault-kv-health.sh -- proactive Vault AppRole health for every vault-kv consumer (D-068 item 3).
# Read-only except one real AppRole login per consumer (60s-TTL token; charm token_ttl=60s).
# Checks: C0 vault unsealed | C1 BUNDLEFIX-007 live (external binding == access binding)
#         C2 relation-data vault_url host inside metal-internal | C3 conf render == relation data
#         C4 AppRole login HTTP 200 from the consumer principal's authentic source
# Consumer discovery is DYNAMIC (juju relations of vault:secrets). Consumers without a conf-path
# mapping are a COVERAGE GAP: reported loudly, exit 2. Update CONSUMER_MAP when adding consumers.
# Exit: 0 all pass | 1 any check failed | 2 warnings only (coverage gap / unparsed)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib-net.sh
. "$SCRIPT_DIR/lib-net.sh"   # provides METAL_INTERNAL_CIDR (single source of truth)
M="${MODEL:-openstack}"
FAIL=0; WARN=0
fail(){ echo "FAIL: $*"; FAIL=$((FAIL+1)); }
warn(){ echo "WARN: $*"; WARN=$((WARN+1)); }
pass(){ echo "PASS: $*"; }
PROBE="$SCRIPT_DIR/vault-kv-inner-probe.sh"
[ -s "$PROBE" ] || { echo "FATAL: missing $PROBE (sibling probe source)"; exit 1; }
INNER_B64=$(base64 -w0 "$PROBE")
# consumer app -> "principal_unit:conf_path"
declare -A CONSUMER_MAP=( ["barbican-vault"]="barbican/0:/etc/barbican/barbican.conf" )

echo "=== C0: vault unsealed ==="
VS=$(juju ssh -m "$M" vault/0 -- 'VAULT_ADDR=http://127.0.0.1:8200 vault status 2>&1' </dev/null | tr -d '\r' || true)
if printf '%s\n' "$VS" | grep -qE '^Sealed[[:space:]]+false'; then pass "vault unsealed"
elif printf '%s\n' "$VS" | grep -qE '^Sealed[[:space:]]+true'; then fail "vault SEALED"
else warn "vault status unparsed: $(printf '%s' "$VS" | head -1)"; fi

echo "=== C1: BUNDLEFIX-007 live invariant (external binding == access) ==="
B=$(juju show-application vault -m "$M" --format yaml 2>&1 || true)
ACC=$(printf '%s\n' "$B" | awk '/^[[:space:]]+access:/{print $2; exit}')
EXT=$(printf '%s\n' "$B" | awk '/^[[:space:]]+external:/{print $2; exit}')
echo "  access=$ACC external=$EXT"
if [ -n "$ACC" ] && [ "$ACC" = "$EXT" ]; then pass "external == access ($ACC)"
elif [ -z "$ACC" ] || [ -z "$EXT" ]; then warn "bindings unparsed"
else fail "external ($EXT) != access ($ACC) -- D-067 clobber condition LIVE"; fi

echo "=== consumer discovery (relations of vault:secrets) ==="
CONS=$(juju status vault -m "$M" --format json 2>/dev/null | python3 -c "
import sys,json
try:
    d=json.load(sys.stdin); r=d['applications']['vault'].get('relations',{}).get('secrets',[])
    for x in r: print(x['related-application'] if isinstance(x,dict) else x)
except Exception as e: print('PARSE-ERROR', e)")
printf '%s\n' "${CONS:-<none>}" | sed 's/^/  /'
case "$CONS" in PARSE-ERROR*|'') warn "consumer discovery failed"; CONS="";; esac

for APP in $CONS; do
  echo "=== consumer: $APP ==="
  MAPV="${CONSUMER_MAP[$APP]:-}"
  if [ -z "$MAPV" ]; then warn "$APP: no conf mapping -- COVERAGE GAP, update CONSUMER_MAP"; continue; fi
  PUNIT="${MAPV%%:*}"; CPATH="${MAPV#*:}"
  # Capture-then-test (DOCFIX-116): the old extract-then-check pipeline merged
  # stderr and misreported a juju failure as "no vault_url" (C2 FAIL). A juju
  # failure is a HOLD (cannot verify), never a verdict on the relation data.
  SUERR=$(mktemp)
  if ! SUOUT=$(juju show-unit "$APP/0" -m "$M" --format yaml 2>"$SUERR"); then
    warn "$APP: juju show-unit failed -- relation data UNVERIFIED (not a C2 verdict): $(head -1 "$SUERR")"
    rm -f "$SUERR"; continue
  fi
  rm -f "$SUERR"
  # URL class is IPv4-only (v1); extend for bracketed IPv6 at v2
  RURL=$(printf '%s\n' "$SUOUT" | grep -E 'vault_url' | head -1 | grep -oE 'https?://[0-9A-Za-z.:]+' || true)
  echo "  relation vault_url: ${RURL:-<none>}"
  RHOST=${RURL#*//}; RHOST=${RHOST%%:*}
  if [ -z "$RURL" ]; then fail "$APP: no vault_url in relation data"
  elif python3 -c "import ipaddress,sys; sys.exit(0 if ipaddress.ip_address('$RHOST') in ipaddress.ip_network('$METAL_INTERNAL_CIDR') else 1)" 2>/dev/null; then
    pass "$APP: C2 relation vault_url on metal-internal ($METAL_INTERNAL_CIDR)"
  else fail "$APP: C2 relation vault_url $RHOST NOT in $METAL_INTERNAL_CIDR"; fi
  P=$(printf '%s' "$INNER_B64" | juju ssh -m "$M" "$PUNIT" -- "base64 -d | sudo bash -s $CPATH" 2>&1 | tr -d '\r' || true)
  printf '%s\n' "$P" | sed 's/^/  /'
  CURL2=$(printf '%s\n' "$P" | awk -F= '/^conf_vault_url=/{print $2; exit}')
  if [ -n "$RURL" ] && [ "$CURL2" = "$RURL" ]; then pass "$APP: C3 conf render matches relation data"
  else fail "$APP: C3 conf ($CURL2) != relation ($RURL)"; fi
  if printf '%s\n' "$P" | grep -q '^PROBE-PASS$'; then pass "$APP: C4 AppRole login 200 from $PUNIT"
  else fail "$APP: C4 AppRole login FAILED from $PUNIT"; fi
done

echo
echo "Summary: FAIL=$FAIL WARN=$WARN"
[ "$FAIL" -gt 0 ] && exit 1
[ "$WARN" -gt 0 ] && exit 2
exit 0
