diff --git a/changelog.d/153.md b/changelog.d/153.md new file mode 100644 index 0000000..5511116 --- /dev/null +++ b/changelog.d/153.md @@ -0,0 +1 @@ +- Pinned template registries install with rig and serve default converges offline (#153) diff --git a/commands/lib/templates.sh b/commands/lib/templates.sh index 18acbd3..15c26f9 100644 --- a/commands/lib/templates.sh +++ b/commands/lib/templates.sh @@ -18,7 +18,8 @@ # /creds.md the per-vendor creds-free paragraph the context # renderer splices in # -# THE SOURCE IS THREE KNOBS, precedence _DIR > _REF > pin: +# THE SOURCE IS THREE KNOBS plus the installed pin snapshot, precedence +# _DIR > _REF > snapshot > pin fetch: # RIG_TEMPLATES_DIR a local folder — bypasses the fetch entirely (the # offline-test path, and "try a template before it # exists anywhere") @@ -26,7 +27,8 @@ # bootstrap time (the same shape as the rig preinstall) # RIG_TEMPLATES_REPO which repo that ref lives in (default # heavy-duty/rig-templates) -# and, absent both overrides, the PIN below. +# and, absent both overrides, the snapshot installed beside this file when it +# matches the PIN below, then a live fetch of that pin as the fallback. # The default registry ref a mint converges — the BOX_RELEASE discipline # (#103): one line, bumped deliberately by ordinary rig PR after review, so a @@ -51,6 +53,10 @@ MACHINE_KEYS_REQUIRED=(ROOT_DOOR HOST JOIN) templates_source_desc() { if [ -n "${RIG_TEMPLATES_DIR:-}" ]; then printf 'local dir %s (RIG_TEMPLATES_DIR)' "$RIG_TEMPLATES_DIR" + elif [ -z "${RIG_TEMPLATES_REF:-}" ] && templates_snapshot_usable; then + printf '%s@%s (snapshot)' \ + "${RIG_TEMPLATES_REPO:-heavy-duty/rig-templates}" \ + "$RIG_TEMPLATES_PIN" else printf '%s@%s%s' \ "${RIG_TEMPLATES_REPO:-heavy-duty/rig-templates}" \ @@ -59,7 +65,26 @@ templates_source_desc() { fi } -# templates_resolve — resolve the three knobs to a LOCAL directory holding +# The snapshot path is derived from this library's installed tree. Its +# pin-bearing directory name is the staleness guard: an older snapshot is +# invisible after a pin bump. A usable registry has at least one definition; +# an empty directory means an interrupted extraction and falls through to the +# same live fetch as an absent snapshot. +templates_snapshot_dir() { + local lib_dir + lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + printf '%s/templates@%s' "$(cd "$lib_dir/../.." && pwd)" "$RIG_TEMPLATES_PIN" +} + +templates_snapshot_usable() { + local snapshot role_env + snapshot="$(templates_snapshot_dir)" + [ -d "$snapshot" ] || return 1 + role_env="$(find "$snapshot" -mindepth 2 -maxdepth 2 -type f -name template.env -print -quit 2>/dev/null)" + [ -n "$role_env" ] +} + +# templates_resolve — resolve the knobs to a LOCAL directory holding # the registry, left in the REGISTRY_DIR global (a global, not stdout: a # $(…) call site would run the fetch in a subshell and lose TEMPLATES_TMP, # the path the caller's cleanup trap must rm). RIG_TEMPLATES_DIR wins and is @@ -83,6 +108,10 @@ templates_resolve() { REGISTRY_DIR="$RIG_TEMPLATES_DIR" return 0 fi + if [ -z "${RIG_TEMPLATES_REF:-}" ] && templates_snapshot_usable; then + REGISTRY_DIR="$(templates_snapshot_dir)" + return 0 + fi repo="${RIG_TEMPLATES_REPO:-heavy-duty/rig-templates}" ref="${RIG_TEMPLATES_REF:-$RIG_TEMPLATES_PIN}" command -v curl >/dev/null 2>&1 || { printf 'curl is required to fetch the template registry\n' >&2; return 1; } diff --git a/test/cli.sh b/test/cli.sh index fff0f8c..25ed744 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -854,6 +854,45 @@ check "templates: the pin is one greppable line" 0 "1" \ check "templates: unset knobs fall back to the pin" 0 "the in-tree pin" \ bash -c '. "$1/commands/lib/templates.sh" && templates_source_desc' _ "$ROOT" +# The installed snapshot is found relative to templates.sh itself, so exercise +# it in a copied rig tree: no fixture-only path knob can accidentally make the +# production precedence pass. Poisoned curl makes any network attempt fatal. +mkdir -p "$TPL_WORK/rig/commands/lib" +cp "$ROOT/commands/lib/templates.sh" "$TPL_WORK/rig/commands/lib/templates.sh" +TPL_PIN="$(sed -n 's/^RIG_TEMPLATES_PIN=//p' "$ROOT/commands/lib/templates.sh")" +cp -r "$TPL_FIX" "$TPL_WORK/rig/templates@$TPL_PIN" +cat > "$TPL_WORK/bin/curl" <<'CURLEOF' +#!/usr/bin/env bash +echo "poisoned curl: snapshot resolution attempted network I/O" >&2 +exit 99 +CURLEOF +chmod +x "$TPL_WORK/bin/curl" +# shellcheck disable=SC2016 +snapshot_resolve='set -euo pipefail + . "$1/commands/lib/templates.sh" + templates_resolve + printf "%s\n%s\n" "$REGISTRY_DIR" "$(templates_source_desc)"' +check "templates: matching snapshot resolves with poisoned curl" 0 "(snapshot)" \ + env PATH="$TPL_WORK/bin:$PATH" bash -c "$snapshot_resolve" _ "$TPL_WORK/rig" + +# A stale directory and an empty current directory are both unusable. The +# poisoned fetch exit is folded into templates_resolve's normal loud refusal; +# the important assertion is that neither path answers as the registry. +mv "$TPL_WORK/rig/templates@$TPL_PIN" "$TPL_WORK/rig/templates@stale-pin" +mkdir "$TPL_WORK/rig/templates@$TPL_PIN" +check "templates: empty matching snapshot falls back to fetch" 1 "cannot fetch" \ + env PATH="$TPL_WORK/bin:$PATH" bash -c "$snapshot_resolve" _ "$TPL_WORK/rig" +rm -rf "$TPL_WORK/rig/templates@$TPL_PIN" +check "templates: stale snapshot is ignored" 1 "cannot fetch" \ + env PATH="$TPL_WORK/bin:$PATH" bash -c "$snapshot_resolve" _ "$TPL_WORK/rig" + +# An explicit ref always means a live fetch, even when the matching snapshot +# exists: restore it and prove the poison is reached. +mv "$TPL_WORK/rig/templates@stale-pin" "$TPL_WORK/rig/templates@$TPL_PIN" +check "templates: explicit REF never reads the snapshot" 1 "cannot fetch" \ + env PATH="$TPL_WORK/bin:$PATH" RIG_TEMPLATES_REF=operator-ref \ + bash -c "$snapshot_resolve" _ "$TPL_WORK/rig" + # rig template-lint — the registry repo's CI gate, same schema as the mint's # parser (rig defines validity; rig-templates CI enforces it on every PR). check "template-lint: --help exits 0" 0 "usage:" "$ROOT/commands/template-lint.sh" --help