Module: Pangea::Backend

Defined in:
lib/pangea/backend.rb

Overview

Backend selection + auto-discovery layer.

Per theory/MAGMA.md §II.11, Pangea supports two execution backends —tofu (historical default) and magma (pleme-io’s Rust-native executor). The selected backend consumes Pangea-rendered Terraform JSON and drives plan/apply/destroy.

Resolution priority:

1. --backend CLI flag
2. PANGEA_BACKEND env var
3. pangea.yml `backend:` field
4. default: tofu

Auto-discovery: each backend’s binary is probed via ‘<bin> capabilities` (magma) / `<bin> version -json` (tofu) on first use; the resulting capabilities manifest gates feature use with typed BackendIncompatible errors.

Defined Under Namespace

Classes: BackendIncompatible, BackendUnavailable, Capabilities

Class Method Summary collapse

Class Method Details

.capabilities(name) ⇒ Capabilities

Probe a backend’s capabilities by spawning ‘<bin> capabilities` (magma) or `<bin> version -json` (tofu). Cached per-process.

Parameters:

  • name (String)

    ‘tofu’ or ‘magma’

Returns:

Raises:



87
88
89
90
# File 'lib/pangea/backend.rb', line 87

def self.capabilities(name)
  @capabilities ||= {}
  @capabilities[name] ||= probe(name)
end

.probe(name) ⇒ Object



92
93
94
95
96
97
98
99
# File 'lib/pangea/backend.rb', line 92

def self.probe(name)
  case name
  when 'magma'  then probe_magma
  when 'tofu'   then probe_tofu
  else
    raise ArgumentError, "unknown backend: #{name}"
  end
end

.probe_magmaObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/pangea/backend.rb', line 101

def self.probe_magma
  out, status = Open3.capture2('magma', 'capabilities')
  raise BackendUnavailable, "magma binary not on PATH" unless status.success?

  m = JSON.parse(out)
  Capabilities.new(
    name:                          m['tool'],
    version:                       m['version'],
    supported_protocols:           Array(m['supported_protocols']),
    input_formats:                 Array(m['input_formats']),
    subcommands:                   Array(m['subcommands']),
    supports_in_memory_pipeline:   m['in_memory_pipeline_supported'] == true,
    supports_workspace_chains:     m['workspace_chain_supported'] == true,
    raw:                           m,
  )
rescue Errno::ENOENT
  raise BackendUnavailable, "magma binary not on PATH"
end

.probe_tofuObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/pangea/backend.rb', line 120

def self.probe_tofu
  out, status = Open3.capture2('tofu', 'version', '-json')
  raise BackendUnavailable, "tofu binary not on PATH" unless status.success?

  m = JSON.parse(out)
  # tofu's version -json is minimal; we synthesize a Capabilities by
  # composing version + a compile-time table of "tofu has X
  # subcommand" plus capability-flags fixed to tofu's known shape.
  Capabilities.new(
    name:                          'tofu',
    version:                       m['terraform_version'] || m['version'] || 'unknown',
    supported_protocols:           %w[tfplugin5 tfplugin6],
    input_formats:                 %w[hcl2 terraform-json],
    subcommands:                   %w[init plan apply destroy state import workspace
                                      output show refresh taint force-unlock get
                                      fmt validate console],
    supports_in_memory_pipeline:   false,
    supports_workspace_chains:     false,
    raw:                           m,
  )
rescue Errno::ENOENT
  raise BackendUnavailable, "tofu binary not on PATH"
end

.resolve(explicit: nil, yml_config: nil) ⇒ String

Resolve backend choice from explicit > env > config > default.

Parameters:

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

    the –backend flag value

  • yml_config (Hash, nil) (defaults to: nil)

    the parsed pangea.yml

Returns:

  • (String)

    one of ‘tofu’ or ‘magma’



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/pangea/backend.rb', line 67

def self.resolve(explicit: nil, yml_config: nil)
  value = explicit ||
          ENV['PANGEA_BACKEND'] ||
          yml_config&.dig('backend') ||
          yml_config&.dig(:backend) ||
          'tofu'
  value = value.to_s
  unless %w[tofu magma].include?(value)
    raise ArgumentError,
          "unknown backend #{value.inspect}; expected 'tofu' or 'magma'"
  end
  value
end

.verify_compatible!(backend, requires) ⇒ Object

Verify a workspace’s requirements against the selected backend. Raises BackendIncompatible with hints when something doesn’t match.

Parameters:

  • backend (String)

    ‘tofu’ or ‘magma’

  • requires (Hash)

    e.g. { input_format: ‘pangea-ruby-inprocess’, feature: :in_memory_pipeline }



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pangea/backend.rb', line 150

def self.verify_compatible!(backend, requires)
  caps = capabilities(backend)
  if (fmt = requires[:input_format])
    unless caps.input_formats.include?(fmt.to_s)
      raise BackendIncompatible.new(
        backend:      backend,
        feature:      "input format '#{fmt}'",
        alternatives: %w[tofu magma].reject { |b| b == backend },
        hint:         "Set `backend: magma` in pangea.yml if you need #{fmt}.",
      )
    end
  end
  if requires[:feature] == :in_memory_pipeline && !caps.supports_in_memory_pipeline
    raise BackendIncompatible.new(
      backend:      backend,
      feature:      'in-memory workspace chains',
      alternatives: ['magma'],
      hint:         "Switch to backend=magma to use §II.9 in-memory chains.",
    )
  end
end