From 587a44617baab30a97c58f33bd71d4a51db5649f Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:57:52 +0000 Subject: [PATCH 1/5] feat: add registry-backed machine roles --- commands/bootstrap.sh | 29 +++++++++- commands/lib/templates.sh | 114 ++++++++++++++++++++++++++++++++++---- 2 files changed, 130 insertions(+), 13 deletions(-) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 04f63ac..2b040cb 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -12,6 +12,8 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" . "$HERE/lib/users-config.sh" # parse_users_file — the --users PRE-FLIGHT only # shellcheck source=SCRIPTDIR/lib/manifest.sh . "$HERE/lib/manifest.sh" # manifest_stamp — provenance, written beside the marker +# shellcheck source=SCRIPTDIR/lib/templates.sh +. "$HERE/lib/templates.sh" # registry-backed machine-role definitions # The users lib is sourced for validation, never for convergence: `users apply` # stays the single owner of what a users file DOES to a box (#51). Bootstrap # borrows the parser so a typo'd users file is caught in the same breath as a @@ -119,6 +121,7 @@ EOF # --- args (validated before the root check, so errors are testable) --------- ROLE="${1:-}" +MACHINE_TEMPLATE_DIR="" case "$ROLE" in --undo) shift @@ -136,7 +139,19 @@ case "$ROLE" in exec "$HERE/bootstrap-tenant.sh" "$@" ;; -h|--help) usage; exit 0 ;; "") usage >&2; die "role required (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a '-box' tenant role from the template registry, e.g. claude-box)" 2 ;; - *) die "unknown role: $ROLE (want control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a '-box' tenant role from the template registry, e.g. claude-box)" 2 ;; + *) + shift + templates_resolve || exit 2 + trap '[ -n "$TEMPLATES_TMP" ] && rm -rf "$TEMPLATES_TMP"' EXIT + MACHINE_TEMPLATE_DIR="$REGISTRY_DIR/$ROLE" + if [ "$(template_family "$ROLE" 2>/dev/null || true)" != "machine" ] \ + || [ ! -f "$MACHINE_TEMPLATE_DIR/template.env" ]; then + MACHINE_ROLES="$(templates_machine_roles "$REGISTRY_DIR" | paste -sd'|' -)" + [ -n "$MACHINE_ROLES" ] || MACHINE_ROLES="none" + die "unknown role: $ROLE (want control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom; machine roles from $(templates_source_desc): $MACHINE_ROLES; or a '-box' tenant role)" 2 + fi + machine_template_parse_env "$MACHINE_TEMPLATE_DIR/template.env" \ + || die "invalid machine role $ROLE from $(templates_source_desc)" 2 ;; esac # Role→traits map — the single place a role's shape is declared (issue #26). @@ -156,6 +171,7 @@ case "$ROLE" in dev-server) ROOT_DOOR=closed HOST=yes JOIN=authkey ;; workstation) ROOT_DOOR=closed HOST=yes JOIN=login ;; custom) ;; + *) ROOT_DOOR="$TPL_ROOT_DOOR" HOST="$TPL_HOST" JOIN="$TPL_JOIN" ;; esac # custom has no hostname default: a made-up name on a made-up shape helps nobody. @@ -784,6 +800,17 @@ if [ -n "$USERS_FILE" ]; then "$HERE/users-apply.sh" --file "$USERS_FILE" fi +# A registry machine's optional install is the final convergence phase: after +# join, host setup, the marker prerequisites, and operators. It inherits the +# caller environment, adds only the selected role, and runs from its definition +# directory. Definitions own idempotence, like bootstrap itself. +if [ -n "$MACHINE_TEMPLATE_DIR" ] && [ -e "$MACHINE_TEMPLATE_DIR/install.sh" ]; then + log "running install hook for ${ROLE} from $(templates_source_desc)" + if ! (cd "$MACHINE_TEMPLATE_DIR" && RIG_ROLE="$ROLE" ./install.sh); then + die "install hook failed for role $ROLE from $(templates_source_desc)" + fi +fi + log "done — role ${ROLE}, hostname ${TS_HOSTNAME}" if [ "$ROLE" = "control-plane-server" ]; then log "next: rig coolify install --version " diff --git a/commands/lib/templates.sh b/commands/lib/templates.sh index 688019c..18acbd3 100644 --- a/commands/lib/templates.sh +++ b/commands/lib/templates.sh @@ -43,6 +43,7 @@ RIG_TEMPLATES_PIN=be749f7fd1ff8dd7c2359bbce7fd6abd3f403eb0 # KEY="value" — nothing else. Parsed by regex, never sourced. TEMPLATE_KEYS_REQUIRED=(USER CONTEXT_PATH CLI_NAME PATH_LINE) TEMPLATE_KEYS_OPTIONAL=(CLI_SRC NEEDS_NODE APT_EXTRAS) +MACHINE_KEYS_REQUIRED=(ROOT_DOOR HOST JOIN) # templates_source_desc — where the resolved registry came from, for error # messages and logs: a misconfigured RIG_TEMPLATES_REPO must be visible in @@ -128,6 +129,26 @@ templates_roles() { done } +# template_family — directory names are the registry's family tag. +# workstation is the one intentional suffix-less machine role (#152 / epic D5). +template_family() { + case "$1" in + *-box) printf 'tenant\n' ;; + *-server|workstation) printf 'machine\n' ;; + *) return 1 ;; + esac +} + +# templates_machine_roles — only machine definitions, for the +# machine bootstrap's unknown-role refusal. +templates_machine_roles() { + local role + while IFS= read -r role; do + [ "$(template_family "$role" 2>/dev/null || true)" = "machine" ] || continue + printf '%s\n' "$role" + done < <(templates_roles "$1") +} + # template_parse_env — parse against the allowlist. Sets # TPL_USER, TPL_CONTEXT_PATH, TPL_CLI_NAME, TPL_CLI_SRC, TPL_PATH_LINE, # TPL_NEEDS_NODE (default no), TPL_APT_EXTRAS. Every refusal names the @@ -206,6 +227,62 @@ template_parse_env() { done } +# machine_template_parse_env — the fleet-machine traits schema. +# The globals match bootstrap's table columns so a definition becomes a table +# row without changing any downstream trait behavior. +# shellcheck disable=SC2034 +machine_template_parse_env() { + local file="$1" line key val n=0 seen=" " k ok + TPL_ROOT_DOOR="" TPL_HOST="" TPL_JOIN="" + [ -f "$file" ] || { printf 'template.env missing: %s\n' "$file" >&2; return 1; } + while IFS= read -r line || [ -n "$line" ]; do + n=$((n+1)) + case "$line" in ''|'#'*) continue ;; esac + if [[ ! "$line" =~ ^([A-Z_]+)=\"(.*)\"$ ]]; then + printf 'template.env:%d: not KEY="value": %s\n' "$n" "$line" >&2 + return 1 + fi + key="${BASH_REMATCH[1]}" val="${BASH_REMATCH[2]}" + ok="" + for k in "${MACHINE_KEYS_REQUIRED[@]}"; do + [ "$key" = "$k" ] && ok=1 + done + [ -n "$ok" ] || { + printf 'template.env:%d: unknown key: %s (allowed: %s)\n' \ + "$n" "$key" "${MACHINE_KEYS_REQUIRED[*]}" >&2 + return 1 + } + case "$seen" in *" $key "*) + printf 'template.env:%d: duplicate key: %s\n' "$n" "$key" >&2 + return 1 ;; + esac + seen="$seen$key " + case "$key" in + ROOT_DOOR) TPL_ROOT_DOOR="$val" ;; + HOST) TPL_HOST="$val" ;; + JOIN) TPL_JOIN="$val" ;; + esac + done < "$file" + for k in "${MACHINE_KEYS_REQUIRED[@]}"; do + case "$seen" in *" $k "*) ;; *) + printf 'template.env: missing required key: %s\n' "$k" >&2 + return 1 ;; + esac + done + case "$TPL_ROOT_DOOR" in + open|closed) ;; + *) printf 'template.env: ROOT_DOOR: want open or closed, got: %s\n' "$TPL_ROOT_DOOR" >&2; return 1 ;; + esac + case "$TPL_HOST" in + yes|no) ;; + *) printf 'template.env: HOST: want yes or no, got: %s\n' "$TPL_HOST" >&2; return 1 ;; + esac + case "$TPL_JOIN" in + authkey|login) ;; + *) printf 'template.env: JOIN: want authkey or login, got: %s\n' "$TPL_JOIN" >&2; return 1 ;; + esac +} + # render_tenant_context — the agent-context file's # content, on stdout: the one file every agent reads before touching # anything. The skeleton is MECHANISM and lives here once — the box#80 guard @@ -248,19 +325,32 @@ EOF # protects the registry, the mint-time parse protects a mint served through # RIG_TEMPLATES_REPO/_DIR that CI never saw. template_lint() { - local dir="${1%/}" role + local dir="${1%/}" role family role="$(basename "$dir")" [ -d "$dir" ] || { printf '%s: not a directory\n' "$dir" >&2; return 1; } - case "$role" in - *-box|*-server) ;; - *) printf '%s: role directories carry a family suffix (-box for box tenants, -server for fleet machines — rig#76)\n' "$role" >&2; return 1 ;; - esac - template_parse_env "$dir/template.env" || return 1 - [ -s "$dir/install.sh" ] \ - || { printf '%s: install.sh missing or empty\n' "$role" >&2; return 1; } - head -n1 "$dir/install.sh" | grep -q '^#!' \ - || { printf '%s: install.sh has no shebang\n' "$role" >&2; return 1; } - grep -q '[^[:space:]]' "$dir/creds.md" 2>/dev/null \ - || { printf '%s: creds.md missing or blank (the context renderer splices it in — a blank paragraph would ship a context file with a hole)\n' "$role" >&2; return 1; } + family="$(template_family "$role" 2>/dev/null || true)" + [ -n "$family" ] || { + printf '%s: role directories carry a family suffix (-box for box tenants, -server for fleet machines — rig#76; workstation is #152 machine carve-out)\n' "$role" >&2 + return 1 + } + if [ "$family" = "tenant" ]; then + template_parse_env "$dir/template.env" || return 1 + [ -s "$dir/install.sh" ] \ + || { printf '%s: install.sh missing or empty\n' "$role" >&2; return 1; } + head -n1 "$dir/install.sh" | grep -q '^#!' \ + || { printf '%s: install.sh has no shebang\n' "$role" >&2; return 1; } + grep -q '[^[:space:]]' "$dir/creds.md" 2>/dev/null \ + || { printf '%s: creds.md missing or blank (the context renderer splices it in — a blank paragraph would ship a context file with a hole)\n' "$role" >&2; return 1; } + else + machine_template_parse_env "$dir/template.env" || return 1 + [ ! -e "$dir/creds.md" ] \ + || { printf '%s: creds.md is not allowed for machine roles (machines render no tenant context)\n' "$role" >&2; return 1; } + if [ -e "$dir/install.sh" ]; then + [ -s "$dir/install.sh" ] \ + || { printf '%s: install.sh is empty\n' "$role" >&2; return 1; } + head -n1 "$dir/install.sh" | grep -q '^#!' \ + || { printf '%s: install.sh has no shebang\n' "$role" >&2; return 1; } + fi + fi return 0 } From 88e59b6ec8b378dbb379bedc7dc3f4a4df17a3a3 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:00:21 +0000 Subject: [PATCH 2/5] test: cover machine template registry --- README.md | 28 +++++++++++++++++ changelog.d/152.md | 1 + commands/bootstrap.sh | 3 +- commands/template-lint.sh | 17 +++++----- test/cli.sh | 65 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 changelog.d/152.md diff --git a/README.md b/README.md index b13d346..17e2732 100644 --- a/README.md +++ b/README.md @@ -401,6 +401,34 @@ unattended VM-host appliance) — and `workstation` is the machine at the keyboa end of all the SSH connections: `root-door=closed`, `join=login`, entering the tailnet as *your* device rather than the fleet's. +### Machine-role templates + +Machine presets can also live in the +[heavy-duty/rig-templates](https://github.com/heavy-duty/rig-templates) +registry. A `*-server` directory is a fleet-machine definition; the exact +name `workstation` is the deliberate suffix-less exception. Its +`template.env` contains exactly the three traits bootstrap's built-in table +uses: + +```dotenv +ROOT_DOOR="open" # open|closed +HOST="no" # yes|no +JOIN="authkey" # authkey|login +``` + +An `install.sh` is optional. When present, bootstrap runs it as root, +non-interactively, from the definition directory with `RIG_ROLE` set, after +the tailnet join, host setup, role marker prerequisites, and operator +convergence. A nonzero exit fails bootstrap and names the role and registry +source. The definition owns idempotence, just as bootstrap does. + +Built-in roles and `custom` take precedence over registry names. Any other +non-tenant role is looked up in the resolved registry; the same three source +knobs below apply, including `RIG_TEMPLATES_DIR` for an offline local +definition. Pin reviewed registry content into rig's tree before using it on +fleet machines: an optional machine `install.sh` executes as root on metal, +and an override is the operator explicitly choosing a different trust root. + ### `rig bootstrap -box` — the box tenants Run as root, **inside** a [box](https://github.com/heavy-duty/box)-minted diff --git a/changelog.d/152.md b/changelog.d/152.md new file mode 100644 index 0000000..7238dcc --- /dev/null +++ b/changelog.d/152.md @@ -0,0 +1 @@ +Machine-role templates can declare bootstrap traits and an optional final root install hook (#152) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 2b040cb..bf19ba6 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -144,7 +144,8 @@ case "$ROLE" in templates_resolve || exit 2 trap '[ -n "$TEMPLATES_TMP" ] && rm -rf "$TEMPLATES_TMP"' EXIT MACHINE_TEMPLATE_DIR="$REGISTRY_DIR/$ROLE" - if [ "$(template_family "$ROLE" 2>/dev/null || true)" != "machine" ] \ + if { [ "$ROLE" != "workstation" ] && [[ ! "$ROLE" =~ ^[a-z][a-z0-9-]*-server$ ]]; } \ + || [ "$(template_family "$ROLE" 2>/dev/null || true)" != "machine" ] \ || [ ! -f "$MACHINE_TEMPLATE_DIR/template.env" ]; then MACHINE_ROLES="$(templates_machine_roles "$REGISTRY_DIR" | paste -sd'|' -)" [ -n "$MACHINE_ROLES" ] || MACHINE_ROLES="none" diff --git a/commands/template-lint.sh b/commands/template-lint.sh index 3350130..5f570df 100755 --- a/commands/template-lint.sh +++ b/commands/template-lint.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# rig template-lint ... — is this a valid tenant-role definition? +# rig template-lint ... — is this a valid role definition? # # rig defines what a valid template is (the schema lives in # lib/templates.sh, beside the mint-time parser that enforces it); the @@ -23,12 +23,15 @@ usage() { cat <<'EOF' usage: rig template-lint ... -Validate tenant-role definitions (the heavy-duty/rig-templates shape): -each must carry a family-suffixed name (rig#76), a template.env -that parses against the allowlist (KEY="value" only — the file is data, -never sourced), an install.sh with a shebang, and a non-blank creds.md. -Every refusal names the failing key or file. Exits non-zero if any -definition fails; nothing is written. +Validate role definitions (the heavy-duty/rig-templates shape). + +Tenant roles use a *-box directory, tenant template.env schema, a shebang +install.sh, and non-blank creds.md. Machine roles use a *-server directory +(or exact name workstation), the ROOT_DOOR/HOST/JOIN schema, no creds.md, +and an optional install.sh which must be non-empty and carry a shebang. +template.env is parsed as KEY="value" data and never sourced. Every refusal +names the failing key or file. Exits non-zero if any definition fails; +nothing is written. EOF } diff --git a/test/cli.sh b/test/cli.sh index 450f064..e4955ae 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -613,6 +613,25 @@ mkdir -p "$TPL_FIX/badnode-box" printf 'USER="x"\nCONTEXT_PATH=".x/A.md"\nCLI_NAME="x"\nPATH_LINE="p"\nNEEDS_NODE="maybe"\n' > "$TPL_FIX/badnode-box/template.env" mkdir -p "$TPL_FIX/badapt-box" printf 'USER="x"\nCONTEXT_PATH=".x/A.md"\nCLI_NAME="x"\nPATH_LINE="p"\nAPT_EXTRAS="zsh -o"\n' > "$TPL_FIX/badapt-box/template.env" +mkdir -p "$TPL_FIX/scratch-server" +printf 'ROOT_DOOR="closed"\nHOST="no"\nJOIN="login"\n' > "$TPL_FIX/scratch-server/template.env" +mkdir -p "$TPL_FIX/workstation" +printf 'ROOT_DOOR="closed"\nHOST="yes"\nJOIN="login"\n' > "$TPL_FIX/workstation/template.env" +mkdir -p "$TPL_FIX/hooked-server" +printf 'ROOT_DOOR="open"\nHOST="no"\nJOIN="authkey"\n' > "$TPL_FIX/hooked-server/template.env" +printf '#!/usr/bin/env bash\nexit 1\n' > "$TPL_FIX/hooked-server/install.sh" +mkdir -p "$TPL_FIX/baddoor-server" +printf 'ROOT_DOOR="ajar"\nHOST="no"\nJOIN="authkey"\n' > "$TPL_FIX/baddoor-server/template.env" +mkdir -p "$TPL_FIX/tenantkeys-server" +cp "$TPL_FIX/scratch-box/template.env" "$TPL_FIX/tenantkeys-server/template.env" +mkdir -p "$TPL_FIX/machinekeys-box" +cp "$TPL_FIX/scratch-server/template.env" "$TPL_FIX/machinekeys-box/template.env" +mkdir -p "$TPL_FIX/creds-server" +cp "$TPL_FIX/scratch-server/template.env" "$TPL_FIX/creds-server/template.env" +printf 'not used\n' > "$TPL_FIX/creds-server/creds.md" +mkdir -p "$TPL_FIX/noshebang-server" +cp "$TPL_FIX/scratch-server/template.env" "$TPL_FIX/noshebang-server/template.env" +printf 'exit 0\n' > "$TPL_FIX/noshebang-server/install.sh" # THE HARD CUT, tenant half (#76). The pre-rename names are gone and must fail # as UNKNOWN — asserted per name, because an alias left in for one tenant is the @@ -653,6 +672,28 @@ check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "Box TENANT role check "bootstrap: an unheard-of '-box' role still dispatches (zero code changes)" 0 "Box TENANT roles" \ "$ROOT/commands/bootstrap.sh" scratch-box --help +# Machine roles use the same resolved registry but remain table-compatible: +# loading happens before flag parsing, so an explicit flag overrides the +# definition exactly as it overrides a built-in row. +check "machine template: traits load from the local registry" 2 "join=login" \ + env RIG_TEMPLATES_DIR="$TPL_FIX" TS_AUTHKEY=x \ + "$ROOT/commands/bootstrap.sh" scratch-server --no-users +if [ "$(id -u)" -ne 0 ]; then + check "machine template: a flag overrides the loaded trait" 1 "must run as root" \ + env RIG_TEMPLATES_DIR="$TPL_FIX" TS_AUTHKEY=x \ + "$ROOT/commands/bootstrap.sh" scratch-server --no-users --join authkey +fi +check "machine template: invalid ROOT_DOOR is refused by key" 2 "ROOT_DOOR" \ + env RIG_TEMPLATES_DIR="$TPL_FIX" \ + "$ROOT/commands/bootstrap.sh" baddoor-server --no-users +check "machine template: unknown role lists machine definitions" 2 "scratch-server" \ + env RIG_TEMPLATES_DIR="$TPL_FIX" "$ROOT/commands/bootstrap.sh" absent-server +check "machine template: unknown role names the resolved source" 2 "RIG_TEMPLATES_DIR" \ + env RIG_TEMPLATES_DIR="$TPL_FIX" "$ROOT/commands/bootstrap.sh" absent-server +check "machine template: a registry role cannot shadow a built-in" 2 "unset TS_AUTHKEY" \ + env RIG_TEMPLATES_DIR="$TPL_FIX" TS_AUTHKEY=x \ + "$ROOT/commands/bootstrap.sh" workstation --no-users + # The tenant marker guard (#83), against marker FIXTURES (never the harness # machine's real /etc/rig/role): converging a tenant onto a machine-role box or a # VM host (host=yes) refuses for every tenant — and names the staging PAIR, @@ -844,6 +885,30 @@ cp "$TPL_FIX/scratch-box/template.env" "$TPL_FIX/scratch-box/creds.md" "$TPL_FIX printf 'exit 0\n' > "$TPL_FIX/noshebang-box/install.sh" check "template-lint: an install.sh without a shebang is refused" 1 "no shebang" \ "$ROOT/commands/template-lint.sh" "$TPL_FIX/noshebang-box" +check "template-lint: a traits-only machine definition passes" 0 "OK: " \ + "$ROOT/commands/template-lint.sh" "$TPL_FIX/scratch-server" +check "template-lint: workstation is the machine-family carve-out" 0 "OK: " \ + "$ROOT/commands/template-lint.sh" "$TPL_FIX/workstation" +check "template-lint: machine roles refuse tenant keys" 1 "unknown key: USER" \ + "$ROOT/commands/template-lint.sh" "$TPL_FIX/tenantkeys-server" +check "template-lint: tenant roles refuse machine keys" 1 "unknown key: ROOT_DOOR" \ + "$ROOT/commands/template-lint.sh" "$TPL_FIX/machinekeys-box" +check "template-lint: machine roles refuse creds.md" 1 "creds.md is not allowed" \ + "$ROOT/commands/template-lint.sh" "$TPL_FIX/creds-server" +check "template-lint: machine install.sh requires a shebang" 1 "no shebang" \ + "$ROOT/commands/template-lint.sh" "$TPL_FIX/noshebang-server" +# The install is deliberately after the users phase and its wrapper names both +# role and source. Dynamic execution belongs to the root integration path; the +# non-root offline harness pins the safety ordering and failure contract. +machine_hook_at="$(grep -n 'running install hook for' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)" +check "machine template: install hook is bootstrap's last convergence phase" 0 "" \ + test "${users_apply_at:-999999}" -lt "${machine_hook_at:-0}" +# shellcheck disable=SC2016 +check "machine template: install failure names role and source" 0 "" \ + grep -qF 'install hook failed for role $ROLE from $(templates_source_desc)' "$ROOT/commands/bootstrap.sh" +# shellcheck disable=SC2016 +check "machine template: install runs from its definition with RIG_ROLE" 0 "" \ + grep -qF 'cd "$MACHINE_TEMPLATE_DIR" && RIG_ROLE="$ROLE" ./install.sh' "$ROOT/commands/bootstrap.sh" rm -rf "$TPL_FIX" "$TPL_WORK" # Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal From 597e1105f4c1c418d72bd372f971b0043be75770 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:02:27 +0000 Subject: [PATCH 3/5] fix: format changelog fragment as entry --- changelog.d/152.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/152.md b/changelog.d/152.md index 7238dcc..0a50dae 100644 --- a/changelog.d/152.md +++ b/changelog.d/152.md @@ -1 +1 @@ -Machine-role templates can declare bootstrap traits and an optional final root install hook (#152) +- Machine-role templates can declare bootstrap traits and an optional final root install hook (#152) From 86bf52d4a5361637a7c44447a123586d1329cabd Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:07:01 +0000 Subject: [PATCH 4/5] fix: group machine role changelog entry --- changelog.d/152.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.d/152.md b/changelog.d/152.md index 0a50dae..24a1d3b 100644 --- a/changelog.d/152.md +++ b/changelog.d/152.md @@ -1 +1,3 @@ +### Added + - Machine-role templates can declare bootstrap traits and an optional final root install hook (#152) From c08bcc6622f5cc6f15b2b25b00e3dfbd1dd730ae Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:07:53 +0000 Subject: [PATCH 5/5] fix: align machine role boundaries --- commands/bootstrap.sh | 4 ++-- test/cli.sh | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index bf19ba6..c8cae9e 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -144,7 +144,7 @@ case "$ROLE" in templates_resolve || exit 2 trap '[ -n "$TEMPLATES_TMP" ] && rm -rf "$TEMPLATES_TMP"' EXIT MACHINE_TEMPLATE_DIR="$REGISTRY_DIR/$ROLE" - if { [ "$ROLE" != "workstation" ] && [[ ! "$ROLE" =~ ^[a-z][a-z0-9-]*-server$ ]]; } \ + if [[ ! "$ROLE" =~ ^[a-z][a-z0-9-]*-server$ ]] \ || [ "$(template_family "$ROLE" 2>/dev/null || true)" != "machine" ] \ || [ ! -f "$MACHINE_TEMPLATE_DIR/template.env" ]; then MACHINE_ROLES="$(templates_machine_roles "$REGISTRY_DIR" | paste -sd'|' -)" @@ -807,7 +807,7 @@ fi # directory. Definitions own idempotence, like bootstrap itself. if [ -n "$MACHINE_TEMPLATE_DIR" ] && [ -e "$MACHINE_TEMPLATE_DIR/install.sh" ]; then log "running install hook for ${ROLE} from $(templates_source_desc)" - if ! (cd "$MACHINE_TEMPLATE_DIR" && RIG_ROLE="$ROLE" ./install.sh); then + if ! (cd "$MACHINE_TEMPLATE_DIR" && RIG_ROLE="$ROLE" bash ./install.sh); then die "install hook failed for role $ROLE from $(templates_source_desc)" fi fi diff --git a/test/cli.sh b/test/cli.sh index e4955ae..fff0f8c 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -616,10 +616,7 @@ printf 'USER="x"\nCONTEXT_PATH=".x/A.md"\nCLI_NAME="x"\nPATH_LINE="p"\nAPT_EXTRA mkdir -p "$TPL_FIX/scratch-server" printf 'ROOT_DOOR="closed"\nHOST="no"\nJOIN="login"\n' > "$TPL_FIX/scratch-server/template.env" mkdir -p "$TPL_FIX/workstation" -printf 'ROOT_DOOR="closed"\nHOST="yes"\nJOIN="login"\n' > "$TPL_FIX/workstation/template.env" -mkdir -p "$TPL_FIX/hooked-server" -printf 'ROOT_DOOR="open"\nHOST="no"\nJOIN="authkey"\n' > "$TPL_FIX/hooked-server/template.env" -printf '#!/usr/bin/env bash\nexit 1\n' > "$TPL_FIX/hooked-server/install.sh" +printf 'ROOT_DOOR="closed"\nHOST="yes"\nJOIN="authkey"\n' > "$TPL_FIX/workstation/template.env" mkdir -p "$TPL_FIX/baddoor-server" printf 'ROOT_DOOR="ajar"\nHOST="no"\nJOIN="authkey"\n' > "$TPL_FIX/baddoor-server/template.env" mkdir -p "$TPL_FIX/tenantkeys-server" @@ -908,7 +905,7 @@ check "machine template: install failure names role and source" 0 "" \ grep -qF 'install hook failed for role $ROLE from $(templates_source_desc)' "$ROOT/commands/bootstrap.sh" # shellcheck disable=SC2016 check "machine template: install runs from its definition with RIG_ROLE" 0 "" \ - grep -qF 'cd "$MACHINE_TEMPLATE_DIR" && RIG_ROLE="$ROLE" ./install.sh' "$ROOT/commands/bootstrap.sh" + grep -qF 'cd "$MACHINE_TEMPLATE_DIR" && RIG_ROLE="$ROLE" bash ./install.sh' "$ROOT/commands/bootstrap.sh" rm -rf "$TPL_FIX" "$TPL_WORK" # Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal