fix: [nomad-step-2] S2-fix-C — make tools/vault-import.sh --sops optional (spec regression) (#921)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/secret-scan Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful

This commit is contained in:
dev-qwen2 2026-04-16 21:15:54 +00:00
parent 3e29a9a61d
commit 6971d5e2ff
3 changed files with 144 additions and 42 deletions

17
tests/fixtures/dot-env-for-env-only vendored Normal file
View file

@ -0,0 +1,17 @@
# Test fixture .env file for env-only vault-import.sh test
# Contains minimal keys for testing env-only import (no sops)
# Generic forge creds
FORGE_TOKEN=generic-forge-token
FORGE_PASS=generic-forge-pass
FORGE_ADMIN_TOKEN=generic-admin-token
# Bot tokens (review only for minimal test)
FORGE_REVIEW_TOKEN=review-token
FORGE_REVIEW_PASS=review-pass
# Woodpecker secrets
WOODPECKER_AGENT_SECRET=wp-agent-secret
# Chat secrets
FORWARD_AUTH_SECRET=forward-auth-secret

View file

@ -309,12 +309,12 @@ setup() {
echo "$output" | grep -q "Missing required argument"
}
@test "fails with missing --sops argument" {
@test "succeeds with --env only (no --sops required)" {
# Issue #921: --sops is now optional
run "$IMPORT_SCRIPT" \
--env "$FIXTURES_DIR/dot-env-complete" \
--age-key "$FIXTURES_DIR/age-keys.txt"
[ "$status" -ne 0 ]
echo "$output" | grep -q "Missing required argument"
--env "$FIXTURES_DIR/dot-env-for-env-only"
[ "$status" -eq 0 ]
echo "$output" | grep -q "Starting Vault import"
}
@test "fails with missing --age-key argument" {
@ -351,3 +351,68 @@ setup() {
[ "$status" -ne 0 ]
echo "$output" | grep -q "not found"
}
# --- Optional --sops argument tests (issue #921) ─────────────────────────────────
@test "env-only import succeeds (no --sops)" {
run "$IMPORT_SCRIPT" \
--env "$FIXTURES_DIR/dot-env-for-env-only"
[ "$status" -eq 0 ]
echo "$output" | grep -q "Starting Vault import"
# Verify forge path was written
run curl -sf -H "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/secret/data/disinto/shared/forge"
[ "$status" -eq 0 ]
echo "$output" | grep -q "generic-forge-token"
echo "$output" | grep -q "generic-admin-token"
}
@test "env-only import warns about age-key without sops" {
run "$IMPORT_SCRIPT" \
--env "$FIXTURES_DIR/dot-env-for-env-only" \
--age-key "$FIXTURES_DIR/age-keys.txt"
[ "$status" -eq 0 ]
echo "$output" | grep -q "WARNING.*--age-key given without --import-sops"
}
@test "sops-only import succeeds (no --env)" {
run "$IMPORT_SCRIPT" \
--sops "$FIXTURES_DIR/.env.vault.enc" \
--age-key "$FIXTURES_DIR/age-keys.txt"
[ "$status" -eq 0 ]
echo "$output" | grep -q "Starting Vault import"
# Verify runner path was written (from sops)
run curl -sf -H "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/secret/data/disinto/runner/GITHUB_TOKEN"
[ "$status" -eq 0 ]
echo "$output" | jq -e '.data.data.value == "github-test-token-abc123"'
}
@test "sops without --age-key errors" {
run "$IMPORT_SCRIPT" \
--sops "$FIXTURES_DIR/.env.vault.enc"
[ "$status" -ne 0 ]
echo "$output" | grep -q "requires --age-key"
}
@test "no arguments errors" {
run "$IMPORT_SCRIPT"
[ "$status" -ne 0 ]
echo "$output" | grep -q "must provide --import-env and/or --import-sops"
}
@test "env-only import with dry-run works" {
run "$IMPORT_SCRIPT" \
--env "$FIXTURES_DIR/dot-env-for-env-only" \
--dry-run
[ "$status" -eq 0 ]
echo "$output" | grep -q "DRY-RUN"
echo "$output" | grep -q "Import plan"
# Verify nothing was written to Vault
run curl -sf -H "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/secret/data/disinto/shared/forge"
[ "$status" -ne 0 ]
}

View file

@ -240,10 +240,13 @@ Usage:
--age-key /path/to/age/keys.txt \
[--dry-run]
Note: --env and --sops are optional but at least one must be provided.
--age-key is only required when using --sops.
Options:
--env Path to .env file (required)
--sops Path to sops-encrypted .env.vault.enc file (required)
--age-key Path to age keys file (required)
--env Path to .env file (optional, use with --sops)
--sops Path to sops-encrypted .env.vault.enc file (optional, use with --env)
--age-key Path to age keys file (required when using --sops)
--dry-run Print import plan without writing to Vault (optional)
--help Show this help message
@ -272,30 +275,36 @@ EOF
esac
done
# Validate required arguments
if [ -z "$env_file" ]; then
_die "Missing required argument: --env"
# --import-sops requires --age-key (can't decrypt without key)
if [ -n "${sops_file:-}" ] && [ -z "${age_key_file:-}" ]; then
_die "ERROR: --import-sops requires --age-key"
fi
if [ -z "$sops_file" ]; then
_die "Missing required argument: --sops"
# --age-key without --import-sops is a no-op — accept it but warn
if [ -z "${sops_file:-}" ] && [ -n "${age_key_file:-}" ]; then
echo "WARNING: --age-key given without --import-sops; ignoring" >&2
fi
if [ -z "$age_key_file" ]; then
_die "Missing required argument: --age-key"
# At least one of --import-env / --import-sops must be provided
if [ -z "${env_file:-}" ] && [ -z "${sops_file:-}" ]; then
_die "ERROR: must provide --import-env and/or --import-sops"
fi
# Validate files exist
if [ ! -f "$env_file" ]; then
if [ -n "$env_file" ] && [ ! -f "$env_file" ]; then
_die "Environment file not found: $env_file"
fi
if [ ! -f "$sops_file" ]; then
if [ -n "$sops_file" ] && [ ! -f "$sops_file" ]; then
_die "Sops file not found: $sops_file"
fi
if [ ! -f "$age_key_file" ]; then
if [ -n "$age_key_file" ] && [ ! -f "$age_key_file" ]; then
_die "Age key file not found: $age_key_file"
fi
# Security check: age key permissions
_validate_age_key_perms "$age_key_file"
# Security check: age key permissions (only if sops is being used)
if [ -n "$age_key_file" ]; then
_validate_age_key_perms "$age_key_file"
fi
# Security check: VAULT_ADDR must be localhost
_check_vault_addr
@ -303,16 +312,20 @@ EOF
# Source the Vault helpers
source "$(dirname "$0")/../lib/hvault.sh"
# Load .env file
_log "Loading environment from: $env_file"
_load_env_file "$env_file"
# Load .env file (if provided)
if [ -n "$env_file" ]; then
_log "Loading environment from: $env_file"
_load_env_file "$env_file"
fi
# Decrypt sops file
_log "Decrypting sops file: $sops_file"
local sops_env
sops_env="$(_decrypt_sops "$sops_file" "$age_key_file")"
# shellcheck disable=SC2086
eval "$sops_env"
# Decrypt sops file (if provided)
if [ -n "$sops_file" ]; then
_log "Decrypting sops file: $sops_file"
local sops_env
sops_env="$(_decrypt_sops "$sops_file" "$age_key_file")"
# shellcheck disable=SC2086
eval "$sops_env"
fi
# Collect all import operations
declare -a operations=()
@ -383,15 +396,17 @@ EOF
fi
done
# --- From sops-decrypted .env.vault.enc ---
# --- From sops-decrypted .env.vault.enc (if provided) ---
# Runner tokens
for token_name in "${RUNNER_TOKENS[@]}"; do
local token_val="${!token_name:-}"
if [ -n "$token_val" ]; then
operations+=("runner|$token_name|$sops_file|$token_name")
fi
done
if [ -n "$sops_file" ]; then
# Runner tokens
for token_name in "${RUNNER_TOKENS[@]}"; do
local token_val="${!token_name:-}"
if [ -n "$token_val" ]; then
operations+=("runner|$token_name|$sops_file|$token_name")
fi
done
fi
# If dry-run, just print the plan
if $dry_run; then
@ -454,12 +469,17 @@ EOF
local vault_key="$subkey"
local source_value=""
if [ "$file" = "$env_file" ]; then
# Source from environment file (envvar contains the variable name)
source_value="${!envvar:-}"
if [ -n "$sops_file" ]; then
if [ "$file" = "$env_file" ]; then
# Source from environment file (envvar contains the variable name)
source_value="${!envvar:-}"
else
# Source from sops-decrypted env (envvar contains the variable name)
source_value="$(printf '%s' "$sops_env" | grep "^${envvar}=" | sed "s/^${envvar}=//" || true)"
fi
else
# Source from sops-decrypted env (envvar contains the variable name)
source_value="$(printf '%s' "$sops_env" | grep "^${envvar}=" | sed "s/^${envvar}=//" || true)"
# No sops file - source from environment file only
source_value="${!envvar:-}"
fi
case "$category" in