Module: Pangea::CLI::TofuEvents
- Defined in:
- lib/pangea/cli/tofu_events.rb
Overview
Parses and renders the NDJSON event stream from ‘tofu plan -json` / `tofu apply -json` / `tofu destroy -json`.
OpenTofu’s -json flag emits one JSON object per line. Each event has ‘@level`, `@message`, `@timestamp`, and `type` fields, plus type-specific payload. Parsing the stream structurally (instead of grep’ing human-readable output) lets us:
-
Filter deprecation warnings (inline_policy, etc.) by matching on diagnostic.summary, not by string regex over arbitrary output.
-
Classify errors as transient (eventual consistency, throttling, service unavailable) vs permanent, by inspecting diagnostic.detail.
-
Render a clean, Nord-themed human summary across plan / apply / destroy with per-resource change glyphs and elapsed timings.
-
Feed the event stream into higher-level orchestration (audit logs, retry decisions, telemetry) as a typed data structure.
When ‘PANGEA_VERBOSE=1` is set, operations.rb bypasses this module and runs tofu in pass-through mode for full debug output.
Defined Under Namespace
Constant Summary collapse
- TRANSIENT_ERROR_PATTERNS =
Diagnostic message patterns that indicate a transient failure —safe to retry. We match against both ‘summary` and `detail`.
[ /NoSuchEntity/, # IAM eventual consistency (role not yet visible) /InvalidClientTokenId/, # IAM creds not yet propagated /Throttling/, # API rate limit /RequestLimitExceeded/, # API rate limit /RequestTimeout/, # Transient timeout /InternalFailure/, # AWS internal /ServiceUnavailable/, # Service outage ].freeze
- DROPPED_WARNING_PATTERNS =
Warning summaries to drop from human output. These are known, understood, and not actionable at the user level.
[ /inline_policy is deprecated/, /Argument is deprecated/, ].freeze
Class Method Summary collapse
-
.parse_line(line) ⇒ Object
Parse one NDJSON line.
-
.render_human(event) ⇒ Object
Render a single event as a one-line human-readable string, or nil if the event is not user-facing (dropped warnings, internal logs).
-
.stream(cmd, args) ⇒ Object
Run ‘cmd` with args, streaming -json output.
-
.yield_raw(line) ⇒ Object
Helper for yield_raw — overridable in specs.
Class Method Details
.parse_line(line) ⇒ Object
Parse one NDJSON line. Returns an Event, or nil if the line is not valid JSON (tofu sometimes emits non-JSON prelude lines — notably the “Initializing the backend…” text during ‘tofu init` which doesn’t support -json).
207 208 209 210 211 212 |
# File 'lib/pangea/cli/tofu_events.rb', line 207 def parse_line(line) return nil if line.nil? || line.empty? Event.new(JSON.parse(line)) rescue JSON::ParserError nil end |
.render_human(event) ⇒ Object
Render a single event as a one-line human-readable string, or nil if the event is not user-facing (dropped warnings, internal logs). Output is Nord-themed via Pangea::CLI::Theme.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/pangea/cli/tofu_events.rb', line 152 def render_human(event) return nil if event.dropped_warning? case event.type when 'version', 'log', 'change_summary', 'refresh_start', 'refresh_complete', 'apply_progress', 'outputs' # Omitted from output. `outputs` (the type event, not state # outputs) is redundant with the change_summary. nil when 'planned_change' action = event.change&.dig('action') addr = event.resource_address return nil unless action && addr " #{Theme.action_glyph(action)} #{Theme.color(:resource, addr)}" when 'apply_start' addr = event.resource_address addr ? " #{Theme.progress_glyph} #{Theme.color(:resource, addr)}" : nil when 'apply_complete' addr = event.resource_address addr ? " #{Theme.success_glyph} #{Theme.color(:resource, addr)}" : nil when 'apply_errored' addr = event.resource_address err = event.hook&.dig('error') body = err ? "#{Theme.color(:resource, addr)}: #{Theme.color(:error, err)}" \ : Theme.color(:resource, addr.to_s) " #{Theme.error_glyph} #{body}" when 'diagnostic' d = event.diagnostic return nil if d.nil? severity = d['severity'].to_s role = case severity when 'error' then :error when 'warning' then :warning else :info end prefix = Theme.color(role, "[#{severity.upcase}]") lines = ["#{prefix} #{Theme.color(:resource, d['summary'])}"] detail = d['detail'].to_s unless detail.empty? # Indent the detail with a subtle left-bar for visual grouping. detail.lines.each do |dl| lines << " #{Theme.color(:divider, '│')} #{Theme.color(role, dl.chomp)}" end end lines.join("\n") else event. if event.level != 'info' end end |
.stream(cmd, args) ⇒ Object
Run ‘cmd` with args, streaming -json output. Yields each Event to the block if given, otherwise collects silently. Returns the Collector regardless. Exit status is available via $?.
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/pangea/cli/tofu_events.rb', line 217 def stream(cmd, args) collector = Collector.new IO.popen([cmd, *args], err: %i[child out]) do |io| io.each_line do |line| line = line.chomp event = parse_line(line) if event.nil? # Non-JSON passthrough (rare, but tofu init etc. may emit # human-readable text before the JSON stream begins). yield_raw(line) if block_given? next end collector.consume(event) yield event if block_given? end end collector end |
.yield_raw(line) ⇒ Object
Helper for yield_raw — overridable in specs. Default: echo to stderr.
237 238 239 |
# File 'lib/pangea/cli/tofu_events.rb', line 237 def yield_raw(line) $stderr.puts line unless line.strip.empty? end |