Compare commits
3 commits
main
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a958efab7b | ||
|
|
cc1e914a0c | ||
|
|
518e58dfea |
3 changed files with 201 additions and 26 deletions
127
bin/disinto
127
bin/disinto
|
|
@ -89,6 +89,9 @@ Init options:
|
|||
--yes Skip confirmation prompts
|
||||
--rotate-tokens Force regeneration of all bot tokens/passwords (idempotent by default)
|
||||
--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:
|
||||
--formula <path> Path to role formula TOML (default: formulas/<role>.toml)
|
||||
|
|
@ -664,8 +667,12 @@ prompt_admin_password() {
|
|||
# `sudo disinto init ...` directly.
|
||||
_disinto_init_nomad() {
|
||||
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 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
|
||||
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)"
|
||||
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
|
||||
echo ""
|
||||
echo "── Cluster-up dry-run ─────────────────────────────────"
|
||||
|
|
@ -694,6 +701,32 @@ _disinto_init_nomad() {
|
|||
"${cmd[@]}" || true
|
||||
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
|
||||
echo "── Deploy services dry-run ────────────────────────────"
|
||||
echo "[deploy] services to deploy: ${with_services}"
|
||||
|
|
@ -721,7 +754,7 @@ _disinto_init_nomad() {
|
|||
exit 0
|
||||
fi
|
||||
|
||||
# Real run: cluster-up + deploy services
|
||||
# Real run: cluster-up + import + deploy services
|
||||
local -a cluster_cmd=("$cluster_up")
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"${cluster_cmd[@]}" || exit $?
|
||||
|
|
@ -733,6 +766,61 @@ _disinto_init_nomad() {
|
|||
sudo -n -- "${cluster_cmd[@]}" || exit $?
|
||||
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
|
||||
if [ -n "$with_services" ]; then
|
||||
echo ""
|
||||
|
|
@ -777,6 +865,11 @@ _disinto_init_nomad() {
|
|||
echo ""
|
||||
echo "── Summary ────────────────────────────────────────────"
|
||||
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}"
|
||||
if echo "$with_services" | grep -q "forgejo"; then
|
||||
echo "Ports: forgejo: 3000"
|
||||
|
|
@ -802,7 +895,7 @@ disinto_init() {
|
|||
fi
|
||||
|
||||
# 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
|
||||
case "$1" in
|
||||
--branch) branch="$2"; shift 2 ;;
|
||||
|
|
@ -819,6 +912,9 @@ disinto_init() {
|
|||
--yes) auto_yes=true; shift ;;
|
||||
--rotate-tokens) rotate_tokens=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 ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -859,11 +955,32 @@ disinto_init() {
|
|||
exit 1
|
||||
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
|
||||
# (S0.4). The default and --empty variants are identical today; Step 1
|
||||
# will branch on $empty to add job deployment to the default path.
|
||||
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;
|
||||
# `return` is defensive against future refactors.
|
||||
return
|
||||
|
|
@ -1017,7 +1134,7 @@ p.write_text(text)
|
|||
echo "[ensure] CLAUDE_CONFIG_DIR"
|
||||
echo "[ensure] state files (.dev-active, .reviewer-active, .gardener-active)"
|
||||
echo ""
|
||||
echo "Dry run complete — no changes made."
|
||||
echo "Dry run complete - no changes made."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ EOF
|
|||
→ export VAULT_ADDR=${VAULT_ADDR_DEFAULT}
|
||||
→ export NOMAD_ADDR=${NOMAD_ADDR_DEFAULT}
|
||||
|
||||
Dry run complete — no changes made.
|
||||
Dry run complete - no changes made.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
#!/usr/bin/env bats
|
||||
# =============================================================================
|
||||
# tests/disinto-init-nomad.bats — Regression guard for `disinto init`
|
||||
# tests/disinto-init-nomad.bats - Regression guard for `disinto init`
|
||||
# backend dispatch (S0.5, issue #825).
|
||||
#
|
||||
# Exercises the three CLI paths the Nomad+Vault migration cares about:
|
||||
# 1. --backend=nomad --dry-run → cluster-up step list
|
||||
# 2. --backend=nomad --empty --dry-run → same, with "--empty" banner
|
||||
# 3. --backend=docker --dry-run → docker path unaffected
|
||||
# 1. --backend=nomad --dry-run -> cluster-up step list
|
||||
# 2. --backend=nomad --empty --dry-run -> same, with "--empty" banner
|
||||
# 3. --backend=docker --dry-run -> docker path unaffected
|
||||
#
|
||||
# A throw-away `placeholder/repo` slug satisfies the CLI's positional-arg
|
||||
# requirement (the nomad dispatcher never touches it). --dry-run on both
|
||||
# backends short-circuits before any network/filesystem mutation, so the
|
||||
# suite is hermetic — no Forgejo, no sudo, no real cluster.
|
||||
# suite is hermetic - no Forgejo, no sudo, no real cluster.
|
||||
# =============================================================================
|
||||
|
||||
setup_file() {
|
||||
|
|
@ -24,7 +24,7 @@ setup_file() {
|
|||
}
|
||||
}
|
||||
|
||||
# ── --backend=nomad --dry-run ────────────────────────────────────────────────
|
||||
# -- --backend=nomad --dry-run ------------------------------------------------
|
||||
|
||||
@test "disinto init --backend=nomad --dry-run exits 0 and prints the step list" {
|
||||
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --dry-run
|
||||
|
|
@ -41,27 +41,27 @@ setup_file() {
|
|||
[[ "$output" == *"[dry-run] Step 5/9: install /etc/nomad.d/server.hcl + client.hcl from repo"* ]]
|
||||
[[ "$output" == *"[dry-run] Step 6/9: first-run vault init + persist unseal.key + root.token"* ]]
|
||||
[[ "$output" == *"[dry-run] Step 7/9: systemctl start vault + poll until unsealed"* ]]
|
||||
[[ "$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 complete — no changes made."* ]]
|
||||
[[ "$output" == *"Dry run complete - no changes made."* ]]
|
||||
}
|
||||
|
||||
# ── --backend=nomad --empty --dry-run ────────────────────────────────────────
|
||||
# -- --backend=nomad --empty --dry-run ----------------------------------------
|
||||
|
||||
@test "disinto init --backend=nomad --empty --dry-run prints the --empty banner + step list" {
|
||||
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --empty --dry-run
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# --empty changes the dispatcher banner but not the step list — Step 1
|
||||
# --empty changes the dispatcher banner but not the step list - Step 1
|
||||
# of the migration will branch on $empty to gate job deployment; today
|
||||
# both modes invoke the same cluster-up dry-run.
|
||||
[[ "$output" == *"nomad backend: --empty (cluster-up only, no jobs)"* ]]
|
||||
[[ "$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) --------------------------------------
|
||||
|
||||
@test "disinto init --backend=docker does NOT dispatch to the nomad path" {
|
||||
run "$DISINTO_BIN" init placeholder/repo --backend=docker --dry-run
|
||||
|
|
@ -71,14 +71,14 @@ setup_file() {
|
|||
[[ "$output" != *"nomad backend:"* ]]
|
||||
[[ "$output" != *"[dry-run] Step 1/9: install nomad + vault binaries + docker daemon"* ]]
|
||||
|
||||
# Positive assertion: docker-path output still appears — the existing
|
||||
# Positive assertion: docker-path output still appears - the existing
|
||||
# docker dry-run printed "=== disinto init ===" before listing the
|
||||
# intended forge/compose actions.
|
||||
[[ "$output" == *"=== disinto init ==="* ]]
|
||||
[[ "$output" == *"── Dry-run: intended actions ────"* ]]
|
||||
[[ "$output" == *"-- Dry-run: intended actions ----"* ]]
|
||||
}
|
||||
|
||||
# ── Flag syntax: --flag=value vs --flag value ────────────────────────────────
|
||||
# -- Flag syntax: --flag=value vs --flag value --------------------------------
|
||||
|
||||
# Both forms must work. The bin/disinto flag loop has separate cases for
|
||||
# `--backend value` and `--backend=value`; a regression in either would
|
||||
|
|
@ -91,7 +91,7 @@ setup_file() {
|
|||
[[ "$output" == *"[dry-run] Step 1/9: install nomad + vault binaries + docker daemon"* ]]
|
||||
}
|
||||
|
||||
# ── Flag validation ──────────────────────────────────────────────────────────
|
||||
# -- Flag validation ----------------------------------------------------------
|
||||
|
||||
@test "--backend=bogus is rejected with a clear error" {
|
||||
run "$DISINTO_BIN" init placeholder/repo --backend=bogus --dry-run
|
||||
|
|
@ -105,11 +105,11 @@ setup_file() {
|
|||
[[ "$output" == *"--empty is only valid with --backend=nomad"* ]]
|
||||
}
|
||||
|
||||
# ── Positional vs flag-first invocation (#835) ───────────────────────────────
|
||||
# -- Positional vs flag-first invocation (#835) -------------------------------
|
||||
#
|
||||
# Before the #835 fix, disinto_init eagerly consumed $1 as repo_url *before*
|
||||
# argparse ran. That swallowed `--backend=nomad` as a repo_url and then
|
||||
# complained that `--empty` required a nomad backend — the nonsense error
|
||||
# complained that `--empty` required a nomad backend - the nonsense error
|
||||
# flagged during S0.1 end-to-end verification. The cases below pin the CLI
|
||||
# to the post-fix contract: the nomad path accepts flag-first invocation,
|
||||
# the docker path still errors helpfully on a missing repo_url.
|
||||
|
|
@ -119,7 +119,7 @@ setup_file() {
|
|||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"nomad backend: --empty (cluster-up only, no jobs)"* ]]
|
||||
[[ "$output" == *"[dry-run] Step 1/9: install nomad + vault binaries + docker daemon"* ]]
|
||||
# The bug symptom must be absent — backend was misdetected as docker
|
||||
# The bug symptom must be absent - backend was misdetected as docker
|
||||
# when --backend=nomad got swallowed as repo_url.
|
||||
[[ "$output" != *"--empty is only valid with --backend=nomad"* ]]
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ setup_file() {
|
|||
[[ "$output" != *"Unknown option"* ]]
|
||||
}
|
||||
|
||||
# ── --with flag tests ─────────────────────────────────────────────────────────
|
||||
# -- --with flag tests ---------------------------------------------------------
|
||||
|
||||
@test "disinto init --backend=nomad --with forgejo --dry-run prints deploy plan" {
|
||||
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --with forgejo --dry-run
|
||||
|
|
@ -191,3 +191,61 @@ setup_file() {
|
|||
[ "$status" -ne 0 ]
|
||||
[[ "$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"* ]]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue