Module: Pangea::Secrets

Defined in:
lib/pangea/secrets.rb

Overview

Unified secret resolution for Pangea templates.

Single interface for all secret access — templates never know (or care) which backend provides the value. Resolution chain is tried in order until one succeeds.

Resolution chain (first match wins):

1. Environment variable (CI, manual override)
2. sops-nix pre-decrypted file (darwin-rebuild / nixos-rebuild)
3. SOPS CLI extraction (fallback — requires sops + age key)

Usage:

Pangea::Secrets.configure(
  sops_file: '/path/to/secrets.yaml',
  sops_nix_dir: '~/.config/sops-nix/secrets',
)

api_key = Pangea::Secrets.resolve('porkbun/api-key')
# Tries: ENV['PORKBUN_API_KEY'] → ~/.config/sops-nix/secrets/porkbun/api-key → sops -d

# Or with explicit env var name:
api_key = Pangea::Secrets.resolve('porkbun/api-key', env: 'MY_CUSTOM_VAR')

Defined Under Namespace

Classes: ResolutionError

Class Method Summary collapse

Class Method Details

.configure(sops_file: nil, sops_nix_dir: nil) ⇒ Object

Configure default paths for secret resolution.

Parameters:

  • sops_file (String) (defaults to: nil)

    Path to SOPS-encrypted secrets file

  • sops_nix_dir (String) (defaults to: nil)

    Directory where sops-nix decrypts files



35
36
37
38
# File 'lib/pangea/secrets.rb', line 35

def configure(sops_file: nil, sops_nix_dir: nil)
  @sops_file = sops_file || default_sops_file
  @sops_nix_dir = sops_nix_dir || default_sops_nix_dir
end

.exists?(path) ⇒ Boolean

Check if a secret exists without retrieving its value.

Parameters:

  • path (String)

    Secret path

Returns:

  • (Boolean)


90
91
92
# File 'lib/pangea/secrets.rb', line 90

def exists?(path)
  !resolve_optional(path).nil?
end

.reset!Object

Reset configuration (for testing).



95
96
97
98
# File 'lib/pangea/secrets.rb', line 95

def reset!
  @sops_file = nil
  @sops_nix_dir = nil
end

.resolve(path, env: nil, required: true) ⇒ String

Resolve a secret by path.

Parameters:

  • path (String)

    Secret path using forward-slash convention (e.g., “porkbun/api-key”)

  • env (String, nil) (defaults to: nil)

    Override environment variable name (default: derived from path)

  • required (Boolean) (defaults to: true)

    Raise if not found (default: true)

Returns:

  • (String)

    The secret value

Raises:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/pangea/secrets.rb', line 47

def resolve(path, env: nil, required: true)
  env_var = env || path_to_env_var(path)
  sops_extract = path_to_sops_extract(path)
  nix_file = File.join(sops_nix_dir, path)

  # 1. Environment variable
  val = ENV[env_var]
  return val if val && !val.empty?

  # 2. sops-nix pre-decrypted file
  if File.exist?(nix_file)
    val = File.read(nix_file).strip
    return val unless val.empty?
  end

  # 3. SOPS CLI extraction
  if File.exist?(sops_file)
    result = `sops --decrypt --extract '#{sops_extract}' #{sops_file} 2>/dev/null`
    return result.strip if $?.success? && !result.strip.empty?
  end

  # Not found
  if required
    raise ResolutionError,
      "secret '#{path}' not found in: ENV[#{env_var}], #{nix_file}, SOPS[#{sops_extract}]"
  end

  nil
end

.resolve_optional(path, env: nil) ⇒ String?

Resolve a secret, returning nil instead of raising.

Parameters:

  • path (String)

    Secret path

  • env (String, nil) (defaults to: nil)

    Override environment variable name

Returns:

  • (String, nil)

    The secret value or nil



82
83
84
# File 'lib/pangea/secrets.rb', line 82

def resolve_optional(path, env: nil)
  resolve(path, env: env, required: false)
end