feat(preflight): host/toolchain validation + VERSIONS pin-file — T01
- VERSIONS: 7 container images (CONTRACT_003 §3.2) + 13 host tools, KEY=value, source-able+greppable; images carry :PIN_DIGEST placeholders with a documented pin-digests procedure (D5 determinism — no real deploy until pinned). - preflight.sh: fails closed (non-zero on any required check), bash-3.2 safe, composable checks/ (versions,tools,env,docker) + gated (ssh,dns) that WARN-skip until the stack is configured. - env check honors D2 (passphrase presence only, never printed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
188e30e23e
commit
edc708b826
12 changed files with 763 additions and 0 deletions
58
preflight/checks/dns.sh
Executable file
58
preflight/checks/dns.sh
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# checks/dns.sh — OPTIONAL / GATED: DNS resolution of the configured hosts.*.
|
||||
# PLAN-002 §9.4: forge.olsitec.de (+ vault/s3) MUST resolve to the VM before
|
||||
# TLS/handover — but NOT necessarily at scaffold time. So: if the host names
|
||||
# are not configured in the Pulumi stack yet, SKIP with a WARNING. When they
|
||||
# ARE configured, attempt resolution; a miss is a WARNING (DNS may not be set
|
||||
# up this early), never a hard failure. This check never fails preflight alone.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
PF_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
# shellcheck source=../lib/common.sh
|
||||
. "$PF_DIR/lib/common.sh"
|
||||
BOOTSTRAP_DIR=$(cd "$PF_DIR/.." && pwd)/bootstrap
|
||||
|
||||
echo "[dns] (gated) DNS resolution of configured hosts.*"
|
||||
|
||||
pf_pulumi_config() {
|
||||
pf_have pulumi || { printf ''; return; }
|
||||
[ -d "$BOOTSTRAP_DIR" ] || { printf ''; return; }
|
||||
( cd "$BOOTSTRAP_DIR" 2>/dev/null && pulumi config get "$1" 2>/dev/null ) || printf ''
|
||||
}
|
||||
|
||||
# pf_resolve <name> : true if name resolves to at least one address.
|
||||
# Tries the tools likely present on macOS/Linux without requiring any one.
|
||||
pf_resolve() {
|
||||
n="$1"
|
||||
if pf_have getent; then getent hosts "$n" >/dev/null 2>&1 && return 0; fi
|
||||
if pf_have dscacheutil; then dscacheutil -q host -a name "$n" 2>/dev/null | grep -q ip_address && return 0; fi
|
||||
if pf_have host; then host "$n" >/dev/null 2>&1 && return 0; fi
|
||||
if pf_have dig; then [ -n "$(dig +short "$n" 2>/dev/null)" ] && return 0; fi
|
||||
if pf_have nslookup; then nslookup "$n" >/dev/null 2>&1 && return 0; fi
|
||||
if pf_have python3; then python3 -c "import socket,sys; socket.gethostbyname(sys.argv[1])" "$n" >/dev/null 2>&1 && return 0; fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! pf_have pulumi || [ ! -d "$BOOTSTRAP_DIR" ]; then
|
||||
pf_warn "no pulumi stack config available yet -> SKIPPING dns resolution (expected pre-Phase-0)"
|
||||
pf_summary "dns"; exit $?
|
||||
fi
|
||||
|
||||
any=0
|
||||
for key in foundation:hosts.forge foundation:hosts.vault foundation:hosts.s3; do
|
||||
name=$(pf_pulumi_config "$key")
|
||||
[ -n "$name" ] || continue
|
||||
any=1
|
||||
if pf_resolve "$name"; then
|
||||
pf_pass "DNS: $name resolves"
|
||||
else
|
||||
pf_warn "DNS: $name does NOT resolve yet (required before TLS/handover, PLAN-002 §9.4) — not failing"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$any" -eq 0 ]; then
|
||||
pf_warn "no hosts.* configured in stack yet -> SKIPPING dns resolution"
|
||||
fi
|
||||
|
||||
pf_summary "dns"
|
||||
29
preflight/checks/docker.sh
Executable file
29
preflight/checks/docker.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# checks/docker.sh — the Docker daemon is reachable.
|
||||
# `docker info` must succeed (CONTRACT_003: the whole egg is Docker containers;
|
||||
# the Pulumi @pulumi/docker provider needs a reachable daemon — locally for the
|
||||
# CLI sanity check here, over SSH at deploy time).
|
||||
# Exits non-zero if the docker CLI is missing or the daemon is unreachable.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
PF_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
# shellcheck source=../lib/common.sh
|
||||
. "$PF_DIR/lib/common.sh"
|
||||
|
||||
echo "[docker] docker daemon reachable"
|
||||
|
||||
if ! pf_have docker; then
|
||||
pf_fail "docker CLI not installed (see tools check)"
|
||||
pf_summary "docker"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if docker info >/dev/null 2>&1; then
|
||||
ctx=$(docker context show 2>/dev/null || echo "default")
|
||||
pf_pass "docker daemon reachable (context: $ctx)"
|
||||
else
|
||||
pf_fail "docker daemon NOT reachable ('docker info' failed) — is Docker Desktop / the engine running?"
|
||||
fi
|
||||
|
||||
pf_summary "docker"
|
||||
52
preflight/checks/env.sh
Executable file
52
preflight/checks/env.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# checks/env.sh — required environment for a `pulumi up` (CONTRACT_001 §1, §1.3).
|
||||
# * PULUMI_CONFIG_PASSPHRASE : set & non-empty (the single external secret, D2).
|
||||
# NEVER printed — only its presence is reported.
|
||||
# * SSH_PRIVATE_KEY_PATH : path to the VM key (default ~/.ssh/id_rsa) exists.
|
||||
# Exits non-zero if a required var is missing/empty or the key file is absent.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
PF_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
# shellcheck source=../lib/common.sh
|
||||
. "$PF_DIR/lib/common.sh"
|
||||
|
||||
echo "[env] required environment variables and secrets (CONTRACT_001 §1.3)"
|
||||
|
||||
# --- PULUMI_CONFIG_PASSPHRASE: presence only, value is sacred (D2) ---
|
||||
if [ -n "${PULUMI_CONFIG_PASSPHRASE:-}" ]; then
|
||||
pf_pass "PULUMI_CONFIG_PASSPHRASE is set (value not shown — D2)"
|
||||
elif [ -n "${PULUMI_CONFIG_PASSPHRASE_FILE:-}" ]; then
|
||||
if [ -f "${PULUMI_CONFIG_PASSPHRASE_FILE}" ]; then
|
||||
pf_pass "PULUMI_CONFIG_PASSPHRASE_FILE set and file exists (value not shown)"
|
||||
else
|
||||
pf_fail "PULUMI_CONFIG_PASSPHRASE_FILE='${PULUMI_CONFIG_PASSPHRASE_FILE}' does not exist"
|
||||
fi
|
||||
else
|
||||
pf_fail "PULUMI_CONFIG_PASSPHRASE is unset/empty (and no PULUMI_CONFIG_PASSPHRASE_FILE)"
|
||||
fi
|
||||
|
||||
# --- SSH_PRIVATE_KEY_PATH: file must exist (CONTRACT_001 default ~/.ssh/id_rsa) ---
|
||||
ssh_key="${SSH_PRIVATE_KEY_PATH:-$HOME/.ssh/id_rsa}"
|
||||
# Expand a leading ~ if the operator exported it literally.
|
||||
case "$ssh_key" in
|
||||
"~/"*) ssh_key="$HOME/${ssh_key#~/}" ;;
|
||||
"~") ssh_key="$HOME" ;;
|
||||
esac
|
||||
if [ -f "$ssh_key" ]; then
|
||||
if [ -z "${SSH_PRIVATE_KEY_PATH:-}" ]; then
|
||||
pf_pass "SSH private key found at default path: $ssh_key"
|
||||
else
|
||||
pf_pass "SSH private key found: $ssh_key"
|
||||
fi
|
||||
# Permission hygiene: warn (do not fail) on world/group-readable key.
|
||||
perms=$(ls -l "$ssh_key" 2>/dev/null | cut -c1-10)
|
||||
case "$perms" in
|
||||
*------) : ;; # owner-only-ish; fine
|
||||
*) pf_warn "SSH key $ssh_key permissions look loose ($perms); 'chmod 600' recommended" ;;
|
||||
esac
|
||||
else
|
||||
pf_fail "SSH private key not found at '$ssh_key' (set SSH_PRIVATE_KEY_PATH or create ~/.ssh/id_rsa)"
|
||||
fi
|
||||
|
||||
pf_summary "env"
|
||||
64
preflight/checks/ssh.sh
Executable file
64
preflight/checks/ssh.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# checks/ssh.sh — OPTIONAL / GATED: SSH reachability to the configured VM host.
|
||||
# Depends on Pulumi config (foundation:vm.host / foundation:vm.user) that may
|
||||
# not exist at scaffold time. If the stack/config is absent we SKIP with a
|
||||
# WARNING (never a failure). Only when a host IS configured do we attempt a
|
||||
# non-interactive SSH probe; a failed probe is a WARNING too, because the VM
|
||||
# may legitimately not exist yet during early bootstrap (PLAN-002 Phase 0/1).
|
||||
# This check therefore never causes preflight to exit non-zero on its own.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
PF_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
# shellcheck source=../lib/common.sh
|
||||
. "$PF_DIR/lib/common.sh"
|
||||
BOOTSTRAP_DIR=$(cd "$PF_DIR/.." && pwd)/bootstrap
|
||||
|
||||
echo "[ssh] (gated) SSH reachability to configured VM host"
|
||||
|
||||
# pf_pulumi_config <key> : echo a stack config value, or "" if unavailable.
|
||||
# Tolerates: pulumi not installed, no stack selected, key absent, no project.
|
||||
pf_pulumi_config() {
|
||||
pf_have pulumi || { printf ''; return; }
|
||||
[ -d "$BOOTSTRAP_DIR" ] || { printf ''; return; }
|
||||
( cd "$BOOTSTRAP_DIR" 2>/dev/null && pulumi config get "$1" 2>/dev/null ) || printf ''
|
||||
}
|
||||
|
||||
if ! pf_have pulumi; then
|
||||
pf_warn "pulumi not installed -> cannot read vm.host; SKIPPING ssh reachability"
|
||||
pf_summary "ssh"; exit $?
|
||||
fi
|
||||
if [ ! -d "$BOOTSTRAP_DIR" ]; then
|
||||
pf_warn "bootstrap/ not present yet -> no stack config; SKIPPING ssh reachability"
|
||||
pf_summary "ssh"; exit $?
|
||||
fi
|
||||
|
||||
vm_host=$(pf_pulumi_config "foundation:vm.host")
|
||||
vm_user=$(pf_pulumi_config "foundation:vm.user")
|
||||
[ -n "$vm_user" ] || vm_user="root"
|
||||
|
||||
if [ -z "$vm_host" ]; then
|
||||
pf_warn "foundation:vm.host not configured yet -> SKIPPING ssh reachability (expected pre-Phase-0)"
|
||||
pf_summary "ssh"; exit $?
|
||||
fi
|
||||
|
||||
ssh_key="${SSH_PRIVATE_KEY_PATH:-$HOME/.ssh/id_rsa}"
|
||||
case "$ssh_key" in "~/"*) ssh_key="$HOME/${ssh_key#~/}" ;; esac
|
||||
|
||||
pf_info "configured target: ${vm_user}@${vm_host} (key: $ssh_key)"
|
||||
|
||||
if ! pf_have ssh; then
|
||||
pf_warn "ssh client missing (see tools check) -> SKIPPING reachability probe"
|
||||
pf_summary "ssh"; exit $?
|
||||
fi
|
||||
|
||||
# Non-interactive, fast-failing probe. BatchMode avoids password prompts;
|
||||
# a 'true' command that succeeds proves auth + reachability.
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new \
|
||||
-i "$ssh_key" "${vm_user}@${vm_host}" true >/dev/null 2>&1; then
|
||||
pf_pass "SSH to ${vm_user}@${vm_host} succeeded"
|
||||
else
|
||||
pf_warn "SSH to ${vm_user}@${vm_host} did not succeed (VM may not exist yet / key not trusted) — not failing"
|
||||
fi
|
||||
|
||||
pf_summary "ssh"
|
||||
89
preflight/checks/tools.sh
Executable file
89
preflight/checks/tools.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# checks/tools.sh — every required host tool is present and >= its VERSIONS pin.
|
||||
# Reads TOOL_<X>_MIN from foundation/VERSIONS (PLAN-002 §9.1 host validation).
|
||||
# Exits non-zero if any required tool is missing or below its minimum.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
PF_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
# shellcheck source=../lib/common.sh
|
||||
. "$PF_DIR/lib/common.sh"
|
||||
|
||||
echo "[tools] required host tools present and >= VERSIONS pin"
|
||||
|
||||
# pf_get_version <tool> : echo a parseable version string for the tool, or "".
|
||||
# Kept in one place so the per-tool quirks are visible and testable.
|
||||
pf_get_version() {
|
||||
case "$1" in
|
||||
pulumi) pulumi version 2>/dev/null ;; # -> v3.243.0
|
||||
bun) bun --version 2>/dev/null ;; # -> 1.3.9
|
||||
node) node --version 2>/dev/null ;; # -> v24.10.0
|
||||
docker) docker --version 2>/dev/null | sed 's/.*version //' ;; # Docker version 29.5.3, build ...
|
||||
git) git --version 2>/dev/null ;; # git version 2.54.0
|
||||
age) age --version 2>/dev/null ;; # v1.1.1 (or "1.1.1")
|
||||
zstd) zstd --version 2>/dev/null | sed -n 's/.*[ vV]\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' ;; # *** Zstandard CLI (64-bit) v1.5.7, ...
|
||||
jq) jq --version 2>/dev/null ;; # jq-1.8.1
|
||||
vault) vault --version 2>/dev/null ;; # Vault v1.18.0 (...)
|
||||
psql) psql --version 2>/dev/null ;; # psql (PostgreSQL) 16.2
|
||||
pg_dump) pg_dump --version 2>/dev/null ;; # pg_dump (PostgreSQL) 16.2
|
||||
ssh) ssh -V 2>&1 ;; # OpenSSH_10.2p1, LibreSSL ...
|
||||
mc) mc --version 2>/dev/null | head -1 ;; # mc version RELEASE.2025-08-13T08-35-41Z
|
||||
*) printf '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# pf_check_tool <bin> <VERSIONS_KEY> <pretty-name>
|
||||
pf_check_tool() {
|
||||
bin="$1"; key="$2"; pretty="$3"
|
||||
min=$(pf_versions_get "$key" 2>/dev/null || true)
|
||||
if [ -z "$min" ]; then
|
||||
pf_fail "$pretty: no $key in VERSIONS (cannot determine minimum)"
|
||||
return
|
||||
fi
|
||||
if ! pf_have "$bin"; then
|
||||
pf_fail "$pretty ($bin): NOT INSTALLED (need >= $min)"
|
||||
return
|
||||
fi
|
||||
raw=$(pf_get_version "$bin" || true)
|
||||
# mc uses a date-stamped RELEASE.YYYY-MM-DD... tag; normalize to a dotted date.
|
||||
if [ "$bin" = "mc" ]; then
|
||||
norm=$(printf '%s' "$raw" | sed -n 's/.*RELEASE\.\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\).*/\1.\2.\3/p')
|
||||
minn=$(printf '%s' "$min" | tr '-' '.')
|
||||
if [ -z "$norm" ]; then
|
||||
pf_warn "$pretty ($bin): present but version unparseable ('$raw'); assuming OK"
|
||||
return
|
||||
fi
|
||||
if pf_ge "$norm" "$minn"; then
|
||||
pf_pass "$pretty ($bin) $norm >= $min"
|
||||
else
|
||||
pf_fail "$pretty ($bin) $norm < required $min"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
if [ -z "$raw" ]; then
|
||||
pf_warn "$pretty ($bin): present but version unparseable; assuming OK"
|
||||
return
|
||||
fi
|
||||
if pf_ge "$raw" "$min"; then
|
||||
pf_pass "$pretty ($bin) $(printf '%s' "$raw" | sed -e 's/^[^0-9]*//' -e 's/[^0-9.].*$//') >= $min"
|
||||
else
|
||||
pf_fail "$pretty ($bin) '$raw' < required $min"
|
||||
fi
|
||||
}
|
||||
|
||||
# bin VERSIONS key pretty name
|
||||
pf_check_tool pulumi TOOL_PULUMI_MIN "Pulumi"
|
||||
pf_check_tool bun TOOL_BUN_MIN "Bun"
|
||||
pf_check_tool node TOOL_NODE_MIN "Node.js"
|
||||
pf_check_tool docker TOOL_DOCKER_MIN "Docker CLI"
|
||||
pf_check_tool git TOOL_GIT_MIN "Git"
|
||||
pf_check_tool age TOOL_AGE_MIN "age"
|
||||
pf_check_tool zstd TOOL_ZSTD_MIN "zstd"
|
||||
pf_check_tool jq TOOL_JQ_MIN "jq"
|
||||
pf_check_tool vault TOOL_VAULT_MIN "Vault CLI"
|
||||
pf_check_tool psql TOOL_PSQL_MIN "PostgreSQL client (psql)"
|
||||
pf_check_tool pg_dump TOOL_PG_DUMP_MIN "PostgreSQL client (pg_dump)"
|
||||
pf_check_tool ssh TOOL_OPENSSH_MIN "OpenSSH client"
|
||||
pf_check_tool mc TOOL_MC_MIN "S3/RustFS client (mc)"
|
||||
|
||||
pf_summary "tools"
|
||||
56
preflight/checks/versions.sh
Executable file
56
preflight/checks/versions.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# checks/versions.sh — the VERSIONS pin-file is present, source-able, and lists
|
||||
# every CONTRACT_003 §3.2 image + every required tool (CONTRACT_001 §Validation:
|
||||
# "preflight asserts VERSIONS present and well-formed").
|
||||
# FAIL if missing/unparseable or a required key is absent.
|
||||
# WARN (not fail) on any image still carrying the PIN_DIGEST placeholder.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
PF_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
# shellcheck source=../lib/common.sh
|
||||
. "$PF_DIR/lib/common.sh"
|
||||
|
||||
echo "[versions] VERSIONS pin-file present and well-formed"
|
||||
|
||||
vf=$(pf_versions_file)
|
||||
if [ ! -f "$vf" ]; then
|
||||
pf_fail "VERSIONS not found at $vf"
|
||||
pf_summary "versions"; exit $?
|
||||
fi
|
||||
pf_pass "VERSIONS present: $vf"
|
||||
|
||||
# Source-able? (run in a subshell so a bad file can't poison this process).
|
||||
if ( set -a; . "$vf"; set +a ) >/dev/null 2>&1; then
|
||||
pf_pass "VERSIONS is source-able"
|
||||
else
|
||||
pf_fail "VERSIONS is NOT source-able (syntax error)"
|
||||
fi
|
||||
|
||||
# Required image keys (CONTRACT_003 §3.2).
|
||||
for k in IMAGE_CADDY IMAGE_FORGEJO IMAGE_POSTGRES IMAGE_VAULT IMAGE_RUSTFS IMAGE_ACT_RUNNER IMAGE_REGISTRY; do
|
||||
v=$(pf_versions_get "$k" 2>/dev/null || true)
|
||||
if [ -z "$v" ]; then
|
||||
pf_fail "missing required image key: $k"
|
||||
else
|
||||
case "$v" in
|
||||
*@sha256:PIN_DIGEST) pf_warn "$k not yet digest-pinned ($v) — run the pin-digests procedure" ;;
|
||||
*@sha256:*) pf_pass "$k pinned by digest" ;;
|
||||
*) pf_warn "$k has no '@sha256:' digest ($v) — floating tag (D5 wants a digest)" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
# Required tool-minimum keys.
|
||||
for k in TOOL_PULUMI_MIN TOOL_BUN_MIN TOOL_NODE_MIN TOOL_DOCKER_MIN TOOL_GIT_MIN \
|
||||
TOOL_AGE_MIN TOOL_ZSTD_MIN TOOL_JQ_MIN TOOL_VAULT_MIN TOOL_PSQL_MIN \
|
||||
TOOL_PG_DUMP_MIN TOOL_OPENSSH_MIN TOOL_MC_MIN; do
|
||||
v=$(pf_versions_get "$k" 2>/dev/null || true)
|
||||
if [ -z "$v" ]; then
|
||||
pf_fail "missing required tool-minimum key: $k"
|
||||
else
|
||||
pf_pass "$k = $v"
|
||||
fi
|
||||
done
|
||||
|
||||
pf_summary "versions"
|
||||
82
preflight/lib/common.sh
Normal file
82
preflight/lib/common.sh
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# shellcheck shell=bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# preflight/lib/common.sh — shared helpers for all preflight checks.
|
||||
# Sourced (not executed). POSIX-friendly, macOS bash 3.2 compatible:
|
||||
# no associative arrays, no `mapfile`, no `${var^^}`, no process substitution.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# ANSI colors (disabled if not a tty or NO_COLOR set).
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
PF_RED=$(printf '\033[31m'); PF_GRN=$(printf '\033[32m')
|
||||
PF_YEL=$(printf '\033[33m'); PF_RST=$(printf '\033[0m')
|
||||
else
|
||||
PF_RED=""; PF_GRN=""; PF_YEL=""; PF_RST=""
|
||||
fi
|
||||
|
||||
# Per-process result counters. Each check script reports into these and prints
|
||||
# its own lines; preflight.sh sums up by parsing the exit code + a summary line.
|
||||
PF_PASS=0
|
||||
PF_FAIL=0
|
||||
PF_WARN=0
|
||||
|
||||
pf_pass() { PF_PASS=$((PF_PASS + 1)); printf ' %sPASS%s %s\n' "$PF_GRN" "$PF_RST" "$*"; }
|
||||
pf_fail() { PF_FAIL=$((PF_FAIL + 1)); printf ' %sFAIL%s %s\n' "$PF_RED" "$PF_RST" "$*"; }
|
||||
pf_warn() { PF_WARN=$((PF_WARN + 1)); printf ' %sWARN%s %s\n' "$PF_YEL" "$PF_RST" "$*"; }
|
||||
pf_info() { printf ' %s----%s %s\n' "$PF_YEL" "$PF_RST" "$*"; }
|
||||
|
||||
# pf_summary <check-name> : print one machine-parseable line and return an exit
|
||||
# code: 0 if no failures, 1 if any failure. WARN never fails.
|
||||
pf_summary() {
|
||||
printf ' -> %s: %d pass, %d warn, %d fail\n' "$1" "$PF_PASS" "$PF_WARN" "$PF_FAIL"
|
||||
[ "$PF_FAIL" -eq 0 ]
|
||||
}
|
||||
|
||||
# pf_have <cmd> : true if command exists on PATH.
|
||||
pf_have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# pf_vercmp <a> <b> : numeric dotted-version compare. Echoes:
|
||||
# 0 if a == b, 1 if a > b, 2 if a < b. Missing components treated as 0.
|
||||
# Non-numeric leading tokens are stripped (e.g. "v3.243.0" -> "3.243.0").
|
||||
pf_vercmp() {
|
||||
a=$(printf '%s' "$1" | sed -e 's/^[^0-9]*//' -e 's/[^0-9.].*$//')
|
||||
b=$(printf '%s' "$2" | sed -e 's/^[^0-9]*//' -e 's/[^0-9.].*$//')
|
||||
# Normalize to dot-separated fields and compare field by field.
|
||||
IFS=.
|
||||
# shellcheck disable=SC2086
|
||||
set -- $a; a1=${1:-0}; a2=${2:-0}; a3=${3:-0}; a4=${4:-0}
|
||||
# shellcheck disable=SC2086
|
||||
set -- $b; b1=${1:-0}; b2=${2:-0}; b3=${3:-0}; b4=${4:-0}
|
||||
unset IFS
|
||||
i=1
|
||||
for pair in "$a1 $b1" "$a2 $b2" "$a3 $b3" "$a4 $b4"; do
|
||||
# shellcheck disable=SC2086
|
||||
set -- $pair
|
||||
av=$(printf '%s' "$1" | sed 's/[^0-9].*$//'); av=${av:-0}
|
||||
bv=$(printf '%s' "$2" | sed 's/[^0-9].*$//'); bv=${bv:-0}
|
||||
if [ "$av" -gt "$bv" ] 2>/dev/null; then echo 1; return 0; fi
|
||||
if [ "$av" -lt "$bv" ] 2>/dev/null; then echo 2; return 0; fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo 0
|
||||
}
|
||||
|
||||
# pf_ge <have> <min> : true (0) if version `have` >= `min`.
|
||||
pf_ge() {
|
||||
r=$(pf_vercmp "$1" "$2")
|
||||
[ "$r" = "0" ] || [ "$r" = "1" ]
|
||||
}
|
||||
|
||||
# pf_versions_file : absolute path to the VERSIONS pin-file (repo root).
|
||||
pf_versions_file() {
|
||||
# lib/ is preflight/lib, repo root is two levels up.
|
||||
printf '%s/VERSIONS' "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../.." && pwd)"
|
||||
}
|
||||
|
||||
# pf_versions_get <KEY> : echo the value of KEY from VERSIONS (greppable form).
|
||||
pf_versions_get() {
|
||||
vf=$(pf_versions_file)
|
||||
[ -f "$vf" ] || return 1
|
||||
line=$(grep "^$1=" "$vf" 2>/dev/null | head -1)
|
||||
[ -n "$line" ] || return 1
|
||||
printf '%s' "${line#*=}"
|
||||
}
|
||||
107
preflight/preflight.sh
Executable file
107
preflight/preflight.sh
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# preflight/preflight.sh — foundation host & toolchain pre-flight validation.
|
||||
#
|
||||
# PLAN-002 Phase 1 / §9.1: a freshly-cloned repo validates host + toolchain
|
||||
# BEFORE any `pulumi up`. Fails CLOSED: exits NON-ZERO if ANY required check
|
||||
# fails. Composable: one script per concern under checks/, aggregated here.
|
||||
#
|
||||
# Runs on the operator workstation AND in CI, on macOS (bash 3.2) and Linux.
|
||||
# No hard bashisms: no associative arrays, no mapfile, no ${var^^}, no
|
||||
# process substitution.
|
||||
#
|
||||
# USAGE
|
||||
# ./preflight/preflight.sh # run all checks
|
||||
# ./preflight/preflight.sh tools env # run only the named checks
|
||||
# NO_COLOR=1 ./preflight/preflight.sh # disable ANSI color
|
||||
#
|
||||
# EXIT CODES
|
||||
# 0 all REQUIRED checks passed (gated ssh/dns may WARN — that's fine)
|
||||
# 1 at least one required check failed (missing/old tool, missing ENV, etc.)
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
PF_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
CHECKS_DIR="$PF_DIR/checks"
|
||||
|
||||
# Colors for the top-level summary (lib/common.sh handles per-check coloring).
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
C_RED=$(printf '\033[31m'); C_GRN=$(printf '\033[32m')
|
||||
C_YEL=$(printf '\033[33m'); C_BLD=$(printf '\033[1m'); C_RST=$(printf '\033[0m')
|
||||
else
|
||||
C_RED=""; C_GRN=""; C_YEL=""; C_BLD=""; C_RST=""
|
||||
fi
|
||||
|
||||
# Ordered list of checks. REQUIRED checks gate the exit code; GATED checks
|
||||
# (ssh, dns) are allowed to be absent/unreachable and only warn — they are
|
||||
# listed so they run, but their result never flips the overall status.
|
||||
REQUIRED_CHECKS="versions tools env docker"
|
||||
GATED_CHECKS="ssh dns"
|
||||
|
||||
# Optional positional args restrict which checks run (any from either list).
|
||||
if [ "$#" -gt 0 ]; then
|
||||
REQUIRED_CHECKS=""
|
||||
GATED_CHECKS=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
ssh|dns) GATED_CHECKS="$GATED_CHECKS $arg" ;;
|
||||
*) REQUIRED_CHECKS="$REQUIRED_CHECKS $arg" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s== foundation preflight ==%s\n' "$C_BLD" "$C_RST"
|
||||
printf 'repo: %s\n' "$(cd "$PF_DIR/.." && pwd)"
|
||||
printf 'host: %s (%s)\n\n' "$(uname -s)" "$(uname -m)"
|
||||
|
||||
FAILED_CHECKS=""
|
||||
RAN_CHECKS=""
|
||||
|
||||
run_check() {
|
||||
name="$1"
|
||||
script="$CHECKS_DIR/$name.sh"
|
||||
if [ ! -f "$script" ]; then
|
||||
printf '%s[%s] no such check (%s) %s\n' "$C_RED" "$name" "$script" "$C_RST"
|
||||
FAILED_CHECKS="$FAILED_CHECKS $name"
|
||||
return
|
||||
fi
|
||||
RAN_CHECKS="$RAN_CHECKS $name"
|
||||
# Run in its own shell so `set -e` inside a check can't abort the orchestrator.
|
||||
if bash "$script"; then
|
||||
: # check returned 0
|
||||
else
|
||||
FAILED_CHECKS="$FAILED_CHECKS $name"
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# Required checks first (these gate exit status).
|
||||
for c in $REQUIRED_CHECKS; do
|
||||
[ -n "$c" ] && run_check "$c"
|
||||
done
|
||||
# Gated checks (run for visibility; their failure does NOT gate exit status,
|
||||
# but a non-zero from them would only happen on an internal error — we still
|
||||
# don't let it fail the run, matching the "skip with WARNING" requirement).
|
||||
for c in $GATED_CHECKS; do
|
||||
[ -n "$c" ] || continue
|
||||
script="$CHECKS_DIR/$c.sh"
|
||||
if [ -f "$script" ]; then
|
||||
RAN_CHECKS="$RAN_CHECKS $c"
|
||||
bash "$script" || true # gated: never gates the exit code
|
||||
printf '\n'
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- summary ----
|
||||
printf '%s== summary ==%s\n' "$C_BLD" "$C_RST"
|
||||
printf 'ran:%s\n' "$RAN_CHECKS"
|
||||
|
||||
if [ -n "$FAILED_CHECKS" ]; then
|
||||
printf '%sFAIL%s — failing checks:%s\n' "$C_RED" "$C_RST" "$FAILED_CHECKS"
|
||||
printf '%spreflight FAILED — fix the above before `pulumi up`.%s\n' "$C_RED" "$C_RST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%sPASS%s — all required checks passed.\n' "$C_GRN" "$C_RST"
|
||||
printf '%sNote:%s gated ssh/dns checks WARN-skip until the Pulumi stack is configured.\n' "$C_YEL" "$C_RST"
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue