diff --git a/tests/fixtures/dot-env-for-env-only b/tests/fixtures/dot-env-for-env-only new file mode 100644 index 0000000..1ea4e68 --- /dev/null +++ b/tests/fixtures/dot-env-for-env-only @@ -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 diff --git a/tests/vault-import.bats b/tests/vault-import.bats index aa7ac7b..e636408 100644 --- a/tests/vault-import.bats +++ b/tests/vault-import.bats @@ -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 ] +} diff --git a/tools/vault-import.sh b/tools/vault-import.sh index e678d36..7111e30 100755 --- a/tools/vault-import.sh +++ b/tools/vault-import.sh @@ -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