#!/usr/bin/env bash # preflight_kubecontext.sh # # Shared helper to detect the "wrong shell context for the install mode" # class of bug. Sourced by install.sh and deploy.sh. # # Exposes two functions: # # verify_kubecontext_matches_config # Strict gate. Reads APP_CLUSTER_KUBECONTEXT from the config's [Global] # section and refuses to proceed if `kubectl config current-context` # doesn't match. Used by deploy.sh (unattended path: silent mismatches # are dangerous). # # Returns 0 on match (or when the config has no APP_CLUSTER_KUBECONTEXT, # e.g. fresh k3d setup). Exits 1 with a clear error on mismatch. # Set KNOE_SKIP_KUBECONTEXT_GUARD=true to bypass (escape hatch for # deliberate cross-cluster maintenance). # # print_kubecontext_notice # Informational. Prints the current kubectl context (or "(none)") so # the user sees what install.sh is about to inherit before the TUI # launches. Never exits or fails. Used by install.sh. # # History: this guard was filed in response to the 2026-04-28 14:00 UTC # outage. An install.sh run in k3d mode with the shell pointed at GKE # overwrote the GKE-cluster ObjectStore + ScheduledBackup with k3d-mode # defaults, and CNPG backups silently failed for hours until the next # manual check. See CLAUDE.md §"Env-contamination warning" and # docs/TODO.md queue item #1 (drift R4). # Read APP_CLUSTER_KUBECONTEXT from the [Global] section of an INI-style # config. Empty string if absent. Quote-stripping is best-effort. _kubectx_from_config() { local cfg="$1" [[ -f "$cfg" ]] || { echo ""; return 0; } # awk: print value when we're in [Global] and key matches. # Strips surrounding whitespace and quotes. awk ' /^\[/{section=$0; next} section=="[Global]" && /^[[:space:]]*APP_CLUSTER_KUBECONTEXT[[:space:]]*=/ { sub(/^[^=]*=[[:space:]]*/, "", $0) sub(/^"/, "", $0); sub(/"$/, "", $0) sub(/^'\''/, "", $0); sub(/'\''$/, "", $0) print $0 exit } ' "$cfg" } # Read the live current-context, or empty if kubectl/config unavailable. _kubectx_current() { command -v kubectl >/dev/null 2>&1 || { echo ""; return 0; } kubectl config current-context 2>/dev/null || true } # verify_kubecontext_matches_config # Strict gate. Exits 1 on mismatch unless KNOE_SKIP_KUBECONTEXT_GUARD=true. verify_kubecontext_matches_config() { local cfg_path="${1:-}" if [[ -z "$cfg_path" ]]; then echo "preflight_kubecontext: usage: verify_kubecontext_matches_config " >&2 return 2 fi if [[ "${KNOE_SKIP_KUBECONTEXT_GUARD:-false}" == "true" ]]; then echo "preflight_kubecontext: KNOE_SKIP_KUBECONTEXT_GUARD=true — skipping check (escape hatch)." >&2 return 0 fi local expected expected="$(_kubectx_from_config "$cfg_path")" # No baked context in the config (e.g. fresh k3d.cfg) → nothing to check. if [[ -z "$expected" ]]; then return 0 fi local actual actual="$(_kubectx_current)" # No live current-context → user hasn't selected one; the config is # authoritative and downstream code will pass --context explicitly. if [[ -z "$actual" ]]; then return 0 fi if [[ "$expected" == "$actual" ]]; then return 0 fi cat >&2 < kubectl current-context: (none set)" else echo "==> kubectl current-context: $actual" fi echo " The mode you select must target this cluster, OR you must switch" echo " context (kubectl config use-context …) before proceeding." }