fix: [nomad-step-2] S2.5 — bin/disinto init --import-env / --import-sops / --age-key wire-up (#883)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/nomad-validate Pipeline failed
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/nomad-validate Pipeline failed
ci/woodpecker/pr/smoke-init Pipeline failed

This commit is contained in:
dev-qwen2 2026-04-16 18:13:26 +00:00
parent 6bdbeb5bd2
commit 28228e31d0
2 changed files with 182 additions and 7 deletions

View file

@ -89,6 +89,9 @@ Init options:
--yes Skip confirmation prompts --yes Skip confirmation prompts
--rotate-tokens Force regeneration of all bot tokens/passwords (idempotent by default) --rotate-tokens Force regeneration of all bot tokens/passwords (idempotent by default)
--dry-run Print every intended action without executing --dry-run Print every intended action without executing
--import-env <path> (nomad) Path to .env file for import into Vault KV
--import-sops <path> (nomad) Path to sops-encrypted .env.vault.enc for import
--age-key <path> (nomad) Path to age keyfile (required with --import-sops)
Hire an agent options: Hire an agent options:
--formula <path> Path to role formula TOML (default: formulas/<role>.toml) --formula <path> Path to role formula TOML (default: formulas/<role>.toml)
@ -664,8 +667,12 @@ prompt_admin_password() {
# `sudo disinto init ...` directly. # `sudo disinto init ...` directly.
_disinto_init_nomad() { _disinto_init_nomad() {
local dry_run="${1:-false}" empty="${2:-false}" with_services="${3:-}" local dry_run="${1:-false}" empty="${2:-false}" with_services="${3:-}"
local import_env="${4:-}" import_sops="${5:-}" age_key="${6:-}"
local cluster_up="${FACTORY_ROOT}/lib/init/nomad/cluster-up.sh" local cluster_up="${FACTORY_ROOT}/lib/init/nomad/cluster-up.sh"
local deploy_sh="${FACTORY_ROOT}/lib/init/nomad/deploy.sh" local deploy_sh="${FACTORY_ROOT}/lib/init/nomad/deploy.sh"
local vault_import_sh="${FACTORY_ROOT}/tools/vault-import.sh"
local vault_auth_sh="${FACTORY_ROOT}/lib/init/nomad/vault-nomad-auth.sh"
local vault_policies_sh="${FACTORY_ROOT}/tools/vault-apply-policies.sh"
if [ ! -x "$cluster_up" ]; then if [ ! -x "$cluster_up" ]; then
echo "Error: ${cluster_up} not found or not executable" >&2 echo "Error: ${cluster_up} not found or not executable" >&2
@ -686,7 +693,7 @@ _disinto_init_nomad() {
echo "nomad backend: default (cluster-up; jobs deferred to Step 1)" echo "nomad backend: default (cluster-up; jobs deferred to Step 1)"
fi fi
# Dry-run: print cluster-up plan + deploy.sh plan # Dry-run: print cluster-up plan + import plan + deploy.sh plan
if [ "$dry_run" = "true" ]; then if [ "$dry_run" = "true" ]; then
echo "" echo ""
echo "── Cluster-up dry-run ─────────────────────────────────" echo "── Cluster-up dry-run ─────────────────────────────────"
@ -694,6 +701,32 @@ _disinto_init_nomad() {
"${cmd[@]}" || true "${cmd[@]}" || true
echo "" echo ""
# Import plan if any import flags are set
if [ -n "$import_env" ] || [ -n "$import_sops" ] || [ -n "$age_key" ]; then
echo "── Vault import dry-run ───────────────────────────────"
if [ -n "$import_env" ]; then
echo "[import] env file: ${import_env}"
fi
if [ -n "$import_sops" ]; then
echo "[import] sops file: ${import_sops}"
fi
if [ -n "$age_key" ]; then
echo "[import] age key: ${age_key}"
fi
echo "[import] [dry-run] ${vault_import_sh} --dry-run"
echo "[import] [dry-run] vault import plan printed above"
echo ""
echo "── Vault policies dry-run ─────────────────────────────"
echo "[policies] [dry-run] ${vault_policies_sh} --dry-run"
echo ""
echo "── Vault auth dry-run ─────────────────────────────────"
echo "[auth] [dry-run] ${vault_auth_sh}"
echo ""
else
echo "[import] no --import-env/--import-sops - skipping; set them or seed kv/disinto/* manually before deploying secret-dependent services"
echo ""
fi
if [ -n "$with_services" ]; then if [ -n "$with_services" ]; then
echo "── Deploy services dry-run ────────────────────────────" echo "── Deploy services dry-run ────────────────────────────"
echo "[deploy] services to deploy: ${with_services}" echo "[deploy] services to deploy: ${with_services}"
@ -721,7 +754,7 @@ _disinto_init_nomad() {
exit 0 exit 0
fi fi
# Real run: cluster-up + deploy services # Real run: cluster-up + import + deploy services
local -a cluster_cmd=("$cluster_up") local -a cluster_cmd=("$cluster_up")
if [ "$(id -u)" -eq 0 ]; then if [ "$(id -u)" -eq 0 ]; then
"${cluster_cmd[@]}" || exit $? "${cluster_cmd[@]}" || exit $?
@ -733,6 +766,61 @@ _disinto_init_nomad() {
sudo -n -- "${cluster_cmd[@]}" || exit $? sudo -n -- "${cluster_cmd[@]}" || exit $?
fi fi
# Apply Vault policies (S2.1)
echo ""
echo "── Applying Vault policies ─────────────────────────────"
if [ "$(id -u)" -eq 0 ]; then
"${vault_policies_sh}" || exit $?
else
if ! command -v sudo >/dev/null 2>&1; then
echo "Error: vault-apply-policies.sh must run as root and sudo is not installed" >&2
exit 1
fi
sudo -n -- "${vault_policies_sh}" || exit $?
fi
# Configure Vault JWT auth (S2.3)
echo ""
echo "── Configuring Vault JWT auth ──────────────────────────"
if [ "$(id -u)" -eq 0 ]; then
"${vault_auth_sh}" || exit $?
else
if ! command -v sudo >/dev/null 2>&1; then
echo "Error: vault-nomad-auth.sh must run as root and sudo is not installed" >&2
exit 1
fi
sudo -n -- "${vault_auth_sh}" || exit $?
fi
# Import secrets if import flags are set (S2.2)
if [ -n "$import_env" ] || [ -n "$import_sops" ] || [ -n "$age_key" ]; then
echo ""
echo "── Importing secrets into Vault ────────────────────────"
local -a import_cmd=("$vault_import_sh")
if [ -n "$import_env" ]; then
import_cmd+=("--env" "$import_env")
fi
if [ -n "$import_sops" ]; then
import_cmd+=("--sops" "$import_sops")
fi
if [ -n "$age_key" ]; then
import_cmd+=("--age-key" "$age_key")
fi
if [ "$(id -u)" -eq 0 ]; then
"${import_cmd[@]}" || exit $?
else
if ! command -v sudo >/dev/null 2>&1; then
echo "Error: vault-import.sh must run as root and sudo is not installed" >&2
exit 1
fi
sudo -n -- "${import_cmd[@]}" || exit $?
fi
else
echo "[import] no --import-env/--import-sops - skipping; set them or seed kv/disinto/* manually before deploying secret-dependent services"
fi
# Deploy services if requested # Deploy services if requested
if [ -n "$with_services" ]; then if [ -n "$with_services" ]; then
echo "" echo ""
@ -777,6 +865,11 @@ _disinto_init_nomad() {
echo "" echo ""
echo "── Summary ────────────────────────────────────────────" echo "── Summary ────────────────────────────────────────────"
echo "Cluster: Nomad+Vault cluster is up" echo "Cluster: Nomad+Vault cluster is up"
if [ -n "$import_env" ] || [ -n "$import_sops" ]; then
echo "Imported: secrets from ${import_env:+$import_env }${import_sops:+${import_sops} }"
else
echo "Imported: (none — secrets must be seeded manually)"
fi
echo "Deployed: ${with_services}" echo "Deployed: ${with_services}"
if echo "$with_services" | grep -q "forgejo"; then if echo "$with_services" | grep -q "forgejo"; then
echo "Ports: forgejo: 3000" echo "Ports: forgejo: 3000"
@ -802,7 +895,7 @@ disinto_init() {
fi fi
# Parse flags # Parse flags
local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false rotate_tokens=false use_build=false dry_run=false backend="docker" empty=false with_services="" local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false rotate_tokens=false use_build=false dry_run=false backend="docker" empty=false with_services="" import_env="" import_sops="" age_key=""
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--branch) branch="$2"; shift 2 ;; --branch) branch="$2"; shift 2 ;;
@ -819,6 +912,9 @@ disinto_init() {
--yes) auto_yes=true; shift ;; --yes) auto_yes=true; shift ;;
--rotate-tokens) rotate_tokens=true; shift ;; --rotate-tokens) rotate_tokens=true; shift ;;
--dry-run) dry_run=true; shift ;; --dry-run) dry_run=true; shift ;;
--import-env) import_env="$2"; shift 2 ;;
--import-sops) import_sops="$2"; shift 2 ;;
--age-key) age_key="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;; *) echo "Unknown option: $1" >&2; exit 1 ;;
esac esac
done done
@ -859,11 +955,32 @@ disinto_init() {
exit 1 exit 1
fi fi
# Import flags validation
# --import-sops requires --age-key
if [ -n "$import_sops" ] && [ -z "$age_key" ]; then
echo "Error: --import-sops requires --age-key" >&2
exit 1
fi
# --age-key requires --import-sops
if [ -n "$age_key" ] && [ -z "$import_sops" ]; then
echo "Error: --age-key requires --import-sops" >&2
exit 1
fi
# --import-* flags require --backend=nomad
if [ -n "$import_env" ] || [ -n "$import_sops" ] || [ -n "$age_key" ]; then
if [ "$backend" != "nomad" ]; then
echo "Error: --import-env, --import-sops, and --age-key require --backend=nomad" >&2
exit 1
fi
fi
# Dispatch on backend — the nomad path runs lib/init/nomad/cluster-up.sh # Dispatch on backend — the nomad path runs lib/init/nomad/cluster-up.sh
# (S0.4). The default and --empty variants are identical today; Step 1 # (S0.4). The default and --empty variants are identical today; Step 1
# will branch on $empty to add job deployment to the default path. # will branch on $empty to add job deployment to the default path.
if [ "$backend" = "nomad" ]; then if [ "$backend" = "nomad" ]; then
_disinto_init_nomad "$dry_run" "$empty" "$with_services" _disinto_init_nomad "$dry_run" "$empty" "$with_services" "$import_env" "$import_sops" "$age_key"
# shellcheck disable=SC2317 # _disinto_init_nomad always exits today; # shellcheck disable=SC2317 # _disinto_init_nomad always exits today;
# `return` is defensive against future refactors. # `return` is defensive against future refactors.
return return
@ -1017,7 +1134,7 @@ p.write_text(text)
echo "[ensure] CLAUDE_CONFIG_DIR" echo "[ensure] CLAUDE_CONFIG_DIR"
echo "[ensure] state files (.dev-active, .reviewer-active, .gardener-active)" echo "[ensure] state files (.dev-active, .reviewer-active, .gardener-active)"
echo "" echo ""
echo "Dry run complete no changes made." echo "Dry run complete - no changes made."
exit 0 exit 0
fi fi

View file

@ -44,7 +44,7 @@ setup_file() {
[[ "$output" == *"[dry-run] Step 8/9: systemctl start nomad + poll until ≥1 node ready"* ]] [[ "$output" == *"[dry-run] Step 8/9: systemctl start nomad + poll until ≥1 node ready"* ]]
[[ "$output" == *"[dry-run] Step 9/9: write /etc/profile.d/disinto-nomad.sh"* ]] [[ "$output" == *"[dry-run] Step 9/9: write /etc/profile.d/disinto-nomad.sh"* ]]
[[ "$output" == *"Dry run complete no changes made."* ]] [[ "$output" == *"Dry run complete - no changes made."* ]]
} }
# ── --backend=nomad --empty --dry-run ──────────────────────────────────────── # ── --backend=nomad --empty --dry-run ────────────────────────────────────────
@ -58,7 +58,7 @@ setup_file() {
# both modes invoke the same cluster-up dry-run. # both modes invoke the same cluster-up dry-run.
[[ "$output" == *"nomad backend: --empty (cluster-up only, no jobs)"* ]] [[ "$output" == *"nomad backend: --empty (cluster-up only, no jobs)"* ]]
[[ "$output" == *"[dry-run] Step 1/9: install nomad + vault binaries + docker daemon"* ]] [[ "$output" == *"[dry-run] Step 1/9: install nomad + vault binaries + docker daemon"* ]]
[[ "$output" == *"Dry run complete no changes made."* ]] [[ "$output" == *"Dry run complete - no changes made."* ]]
} }
# ── --backend=docker (regression guard) ────────────────────────────────────── # ── --backend=docker (regression guard) ──────────────────────────────────────
@ -191,3 +191,61 @@ setup_file() {
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
[[ "$output" == *"--empty and --with are mutually exclusive"* ]] [[ "$output" == *"--empty and --with are mutually exclusive"* ]]
} }
# ── Import flag validation ────────────────────────────────────────────────────
@test "disinto init --backend=nomad --import-env only is accepted" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --import-env /tmp/.env --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"--import-env"* ]]
}
@test "disinto init --backend=nomad --import-sops without --age-key errors" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --import-sops /tmp/.env.vault.enc --dry-run
[ "$status" -ne 0 ]
[[ "$output" == *"--import-sops requires --age-key"* ]]
}
@test "disinto init --backend=nomad --age-key without --import-sops errors" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --age-key /tmp/keys.txt --dry-run
[ "$status" -ne 0 ]
[[ "$output" == *"--age-key requires --import-sops"* ]]
}
@test "disinto init --backend=docker --import-env errors with backend requirement" {
run "$DISINTO_BIN" init placeholder/repo --backend=docker --import-env /tmp/.env
[ "$status" -ne 0 ]
[[ "$output" == *"--import-env, --import-sops, and --age-key require --backend=nomad"* ]]
}
@test "disinto init --backend=nomad --import-sops --age-key --dry-run shows import plan" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --import-sops /tmp/.env.vault.enc --age-key /tmp/keys.txt --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"Vault import dry-run"* ]]
[[ "$output" == *"--import-sops"* ]]
[[ "$output" == *"--age-key"* ]]
}
@test "disinto init --backend=nomad --import-env --import-sops --age-key --dry-run shows full import plan" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --import-env /tmp/.env --import-sops /tmp/.env.vault.enc --age-key /tmp/keys.txt --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"Vault import dry-run"* ]]
[[ "$output" == *"env file: /tmp/.env"* ]]
[[ "$output" == *"sops file: /tmp/.env.vault.enc"* ]]
[[ "$output" == *"age key: /tmp/keys.txt"* ]]
}
@test "disinto init --backend=nomad without import flags shows skip message" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"no --import-env/--import-sops - skipping"* ]]
}
@test "disinto init --backend=nomad --import-env --import-sops --age-key --with forgejo --dry-run shows all plans" {
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --import-env /tmp/.env --import-sops /tmp/.env.vault.enc --age-key /tmp/keys.txt --with forgejo --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"Vault import dry-run"* ]]
[[ "$output" == *"Vault policies dry-run"* ]]
[[ "$output" == *"Vault auth dry-run"* ]]
[[ "$output" == *"Deploy services dry-run"* ]]
}