mux_tf 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MuxTf
4
- class PlanFormatter # rubocop:disable Metrics/ClassLength
4
+ class PlanFormatter
5
5
  extend TerraformHelpers
6
6
  extend PiotrbCliUtils::Util
7
7
  include Coloring
8
8
 
9
- class << self # rubocop:disable Metrics/ClassLength
10
- def log_unhandled_line(state, line, reason: nil)
11
- p [state, pastel.strip(line), reason]
12
- end
9
+ extend ErrorHandlingMethods
10
+ extend FormatterCommon
13
11
 
12
+ class << self
14
13
  def pretty_plan(filename, targets: [])
15
14
  if ENV["JSON_PLAN"]
16
15
  pretty_plan_v2(filename, targets: targets)
@@ -19,117 +18,12 @@ module MuxTf
19
18
  end
20
19
  end
21
20
 
22
- def parse_non_json_plan_line(raw_line)
23
- result = {}
24
-
25
- if raw_line.match(/^time=(?<timestamp>[^ ]+) level=(?<level>[^ ]+) msg=(?<message>.+?)(?: prefix=\[(?<prefix>.+?)\])?\s*$/)
26
- result.merge!($LAST_MATCH_INFO.named_captures.symbolize_keys)
27
- result[:module] = "terragrunt"
28
- result.delete(:prefix) unless result[:prefix]
29
- result[:prefix] = Pathname.new(result[:prefix]).relative_path_from(Dir.getwd).to_s if result[:prefix]
30
-
31
- result[:merge_up] = true if result[:message].match(/^\d+ errors? occurred:$/)
32
- elsif raw_line.strip == ""
33
- result[:blank] = true
34
- else
35
- result[:message] = raw_line
36
- result[:merge_up] = true
37
- end
38
-
39
- # time=2023-08-25T11:44:41-07:00 level=error msg=Terraform invocation failed in /Users/piotr/Work/janepods/.terragrunt-cache/BM86IAj5tW4bZga2lXeYT8tdOKI/V0IEypKSfyl-kHfCnRNAqyX02V8/modules/event-bus prefix=[/Users/piotr/Work/janepods/accounts/eks-dev/admin/apps/kube-system-event-bus]
40
- # time=2023-08-25T11:44:41-07:00 level=error msg=1 error occurred:
41
- # * [/Users/piotr/Work/janepods/.terragrunt-cache/BM86IAj5tW4bZga2lXeYT8tdOKI/V0IEypKSfyl-kHfCnRNAqyX02V8/modules/event-bus] exit status 2
42
- #
43
- #
44
- result
45
- end
46
-
47
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
48
21
  def tf_plan_json(out:, targets: [], &block)
49
- emit_line = proc { |result|
50
- result[:level] ||= result[:stream] == :stderr ? "error" : "info"
51
- result[:module] ||= result[:stream]
52
- result[:type] ||= "unknown"
53
-
54
- if result[:message].match(/^Terraform invocation failed in (.+)/)
55
- result[:type] = "tf_failed"
56
-
57
- lines = result[:message].split("\n")
58
- result[:diagnostic] = {
59
- "summary" => "Terraform invocation failed",
60
- "detail" => result[:message],
61
- roots: [],
62
- extra: []
63
- }
64
-
65
- lines.each do |line|
66
- if line.match(/^\s+\* \[(.+)\] exit status (\d+)$/)
67
- result[:diagnostic][:roots] << {
68
- path: $LAST_MATCH_INFO[1],
69
- status: $LAST_MATCH_INFO[2].to_i
70
- }
71
- elsif line.match(/^\d+ errors? occurred$/)
72
- # noop
73
- else
74
- result[:diagnostic][:extra] << line
75
- end
76
- end
77
-
78
- result[:message] = "Terraform invocation failed"
79
- end
80
-
81
- block.call(result)
82
- }
83
- last_stderr_line = nil
84
- status = tf_plan(out: out, detailed_exitcode: true, color: true, compact_warnings: false, json: true, input: false,
85
- targets: targets) { |(stream, raw_line)|
86
- case stream
87
- # when :command
88
- # puts raw_line
89
- when :stdout
90
- parsed_line = JSON.parse(raw_line)
91
- parsed_line.keys.each do |key| # rubocop:disable Style/HashEachMethods -- intentional, allow adding keys to hash while iterating
92
- if key[0] == "@"
93
- parsed_line[key[1..]] = parsed_line[key]
94
- parsed_line.delete(key)
95
- end
96
- end
97
- parsed_line.symbolize_keys!
98
- parsed_line[:stream] = stream
99
- if last_stderr_line
100
- emit_line.call(last_stderr_line)
101
- last_stderr_line = nil
102
- end
103
- emit_line.call(parsed_line)
104
- when :stderr
105
- parsed_line = parse_non_json_plan_line(raw_line)
106
- parsed_line[:stream] = stream
107
-
108
- if parsed_line[:blank]
109
- if last_stderr_line
110
- emit_line.call(last_stderr_line)
111
- last_stderr_line = nil
112
- end
113
- elsif parsed_line[:merge_up]
114
- if last_stderr_line
115
- last_stderr_line[:message] += "\n#{parsed_line[:message]}"
116
- else
117
- # this is just a standalone message then
118
- parsed_line.delete(:merge_up)
119
- last_stderr_line = parsed_line
120
- end
121
- elsif last_stderr_line
122
- emit_line.call(last_stderr_line)
123
- last_stderr_line = parsed_line
124
- else
125
- last_stderr_line = parsed_line
126
- end
127
- end
128
- }
129
- emit_line.call(last_stderr_line) if last_stderr_line
130
- status
22
+ tf_cmd_json(proc { |handler|
23
+ tf_plan(out: out, detailed_exitcode: true, color: true, compact_warnings: false, json: true, input: false,
24
+ targets: targets, &handler)
25
+ }, &block)
131
26
  end
132
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
133
27
 
134
28
  def parse_lock_info(detail)
135
29
  # Lock Info:
@@ -148,7 +42,7 @@ module MuxTf
148
42
  result
149
43
  end
150
44
 
151
- def print_plan_line(parsed_line, without: [])
45
+ def print_plan_line(parsed_line, without: [], from: nil)
152
46
  default_without = [
153
47
  :level,
154
48
  :module,
@@ -160,8 +54,9 @@ module MuxTf
160
54
  :ui
161
55
  ]
162
56
  extra = parsed_line.without(*default_without, *without)
163
- data = parsed_line.merge(extra: extra)
57
+ data = parsed_line.merge(extra: extra).merge(from: from)
164
58
  log_line = [
59
+ "%<from>s",
165
60
  "%<level>-6s",
166
61
  "%<module>-12s",
167
62
  "%<type>-10s",
@@ -174,7 +69,143 @@ module MuxTf
174
69
  log log_line
175
70
  end
176
71
 
177
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
72
+ def parse_tf_ui_line(parsed_line, meta, seen, skip_plan_summary: false)
73
+ # p(parsed_line)
74
+ case parsed_line[:type]
75
+ when "version"
76
+ meta[:terraform_version] = parsed_line[:terraform]
77
+ meta[:terraform_ui_version] = parsed_line[:ui]
78
+ when "apply_start", "refresh_start"
79
+ first_in_group = !seen.call(parsed_line[:module], "apply_start") &&
80
+ !seen.call(parsed_line[:module], "refresh_start")
81
+ log "Refreshing ", depth: 1, newline: false if first_in_group
82
+ # {
83
+ # :hook=>{
84
+ # "resource"=>{
85
+ # "addr"=>"data.aws_eks_cluster_auth.this",
86
+ # "module"=>"",
87
+ # "resource"=>"data.aws_eks_cluster_auth.this",
88
+ # "implied_provider"=>"aws",
89
+ # "resource_type"=>"aws_eks_cluster_auth",
90
+ # "resource_name"=>"this",
91
+ # "resource_key"=>nil
92
+ # },
93
+ # "action"=>"read"
94
+ # }
95
+ # }
96
+ log ".", newline: false
97
+ when "apply_complete", "refresh_complete"
98
+ # {
99
+ # :hook=>{
100
+ # "resource"=>{
101
+ # "addr"=>"data.aws_eks_cluster_auth.this",
102
+ # "module"=>"",
103
+ # "resource"=>"data.aws_eks_cluster_auth.this",
104
+ # "implied_provider"=>"aws",
105
+ # "resource_type"=>"aws_eks_cluster_auth",
106
+ # "resource_name"=>"this",
107
+ # "resource_key"=>nil
108
+ # },
109
+ # "action"=>"read",
110
+ # "id_key"=>"id",
111
+ # "id_value"=>"admin",
112
+ # "elapsed_seconds"=>0
113
+ # }
114
+ # }
115
+ # noop
116
+ when "resource_drift"
117
+ first_in_group = !seen.call(parsed_line[:module], "resource_drift") &&
118
+ !seen.call(parsed_line[:module], "planned_change")
119
+ # {
120
+ # :change=>{
121
+ # "resource"=>{"addr"=>"module.application.kubectl_manifest.application", "module"=>"module.application", "resource"=>"kubectl_manifest.application", "implied_provider"=>"kubectl", "resource_type"=>"kubectl_manifest", "resource_name"=>"application", "resource_key"=>nil},
122
+ # "action"=>"update"
123
+ # }
124
+ # }
125
+ if first_in_group
126
+ log ""
127
+ log ""
128
+ log "Planned Changes:"
129
+ end
130
+ # {
131
+ # :change=>{
132
+ # "resource"=>{"addr"=>"aws_iam_policy.crossplane_aws_ecr[0]", "module"=>"", "resource"=>"aws_iam_policy.crossplane_aws_ecr[0]", "implied_provider"=>"aws", "resource_type"=>"aws_iam_policy", "resource_name"=>"crossplane_aws_ecr", "resource_key"=>0},
133
+ # "action"=>"update"
134
+ # },
135
+ # :type=>"resource_drift",
136
+ # :level=>"info",
137
+ # :message=>"aws_iam_policy.crossplane_aws_ecr[0]: Drift detected (update)",
138
+ # :module=>"terraform.ui",
139
+ # :timestamp=>"2023-09-26T17:11:46.340117-07:00",
140
+ # :stream=>:stdout
141
+ # }
142
+
143
+ log format("[%<action>s] %<addr>s - Drift Detected (%<change_action>s)",
144
+ action: PlanSummaryHandler.format_action(parsed_line[:change]["action"]),
145
+ addr: PlanSummaryHandler.format_address(parsed_line[:change]["resource"]["addr"]),
146
+ change_action: parsed_line[:change]["action"]), depth: 1
147
+ when "planned_change"
148
+ if skip_plan_summary
149
+ log "" if first_in_group
150
+ else
151
+ first_in_group = !seen.call(parsed_line[:module], "resource_drift") &&
152
+ !seen.call(parsed_line[:module], "planned_change")
153
+ # {
154
+ # :change=>
155
+ # {"resource"=>
156
+ # {"addr"=>"module.application.kubectl_manifest.application",
157
+ # "module"=>"module.application",
158
+ # "resource"=>"kubectl_manifest.application",
159
+ # "implied_provider"=>"kubectl",
160
+ # "resource_type"=>"kubectl_manifest",
161
+ # "resource_name"=>"application",
162
+ # "resource_key"=>nil},
163
+ # "action"=>"create"},
164
+ # :type=>"planned_change",
165
+ # :level=>"info",
166
+ # :message=>"module.application.kubectl_manifest.application: Plan to create",
167
+ # :module=>"terraform.ui",
168
+ # :timestamp=>"2023-08-25T14:48:46.005185-07:00",
169
+ # }
170
+ if first_in_group
171
+ log ""
172
+ log ""
173
+ log "Planned Changes:"
174
+ end
175
+ log format("[%<action>s] %<addr>s",
176
+ action: PlanSummaryHandler.format_action(parsed_line[:change]["action"]),
177
+ addr: PlanSummaryHandler.format_address(parsed_line[:change]["resource"]["addr"])), depth: 1
178
+ end
179
+ when "change_summary"
180
+ if skip_plan_summary
181
+ log ""
182
+ else
183
+ # {
184
+ # :changes=>{"add"=>1, "change"=>0, "import"=>0, "remove"=>0, "operation"=>"plan"},
185
+ # :type=>"change_summary",
186
+ # :level=>"info",
187
+ # :message=>"Plan: 1 to add, 0 to change, 0 to destroy.",
188
+ # :module=>"terraform.ui",
189
+ # :timestamp=>"2023-08-25T14:48:46.005211-07:00",
190
+ # :stream=>:stdout
191
+ # }
192
+ log ""
193
+ # puts parsed_line[:message]
194
+ log "#{parsed_line[:changes]['operation'].capitalize} summary: " + parsed_line[:changes].without("operation").map { |k, v|
195
+ color = PlanSummaryHandler.color_for_action(k)
196
+ "#{pastel.yellow(v)} to #{pastel.decorate(k, color)}" if v.positive?
197
+ }.compact.join(" ")
198
+ end
199
+
200
+ when "output"
201
+ when "outputs"
202
+ # json plan output summary
203
+ false # handled when reading the plan json
204
+ else
205
+ print_plan_line(parsed_line, from: "parse_tf_ui_line,else")
206
+ end
207
+ end
208
+
178
209
  def pretty_plan_v2(filename, targets: [])
179
210
  meta = {}
180
211
  meta[:seen] = {
@@ -189,141 +220,25 @@ module MuxTf
189
220
  when "info"
190
221
  case parsed_line[:module]
191
222
  when "terraform.ui"
192
- case parsed_line[:type]
193
- when "version"
194
- meta[:terraform_version] = parsed_line[:terraform]
195
- meta[:terraform_ui_version] = parsed_line[:ui]
196
- when "apply_start", "refresh_start"
197
- first_in_group = !seen.call(parsed_line[:module], "apply_start") &&
198
- !seen.call(parsed_line[:module], "refresh_start")
199
- log "Refreshing ", depth: 1, newline: false if first_in_group
200
- # {
201
- # :hook=>{
202
- # "resource"=>{
203
- # "addr"=>"data.aws_eks_cluster_auth.this",
204
- # "module"=>"",
205
- # "resource"=>"data.aws_eks_cluster_auth.this",
206
- # "implied_provider"=>"aws",
207
- # "resource_type"=>"aws_eks_cluster_auth",
208
- # "resource_name"=>"this",
209
- # "resource_key"=>nil
210
- # },
211
- # "action"=>"read"
212
- # }
213
- # }
214
- log ".", newline: false
215
- when "apply_complete", "refresh_complete"
216
- # {
217
- # :hook=>{
218
- # "resource"=>{
219
- # "addr"=>"data.aws_eks_cluster_auth.this",
220
- # "module"=>"",
221
- # "resource"=>"data.aws_eks_cluster_auth.this",
222
- # "implied_provider"=>"aws",
223
- # "resource_type"=>"aws_eks_cluster_auth",
224
- # "resource_name"=>"this",
225
- # "resource_key"=>nil
226
- # },
227
- # "action"=>"read",
228
- # "id_key"=>"id",
229
- # "id_value"=>"admin",
230
- # "elapsed_seconds"=>0
231
- # }
232
- # }
233
- # noop
234
- when "resource_drift"
235
- first_in_group = !seen.call(parsed_line[:module], "resource_drift") &&
236
- !seen.call(parsed_line[:module], "planned_change")
237
- # {
238
- # :change=>{
239
- # "resource"=>{"addr"=>"module.application.kubectl_manifest.application", "module"=>"module.application", "resource"=>"kubectl_manifest.application", "implied_provider"=>"kubectl", "resource_type"=>"kubectl_manifest", "resource_name"=>"application", "resource_key"=>nil},
240
- # "action"=>"update"
241
- # }
242
- # }
243
- if first_in_group
244
- log ""
245
- log ""
246
- log "Planned Changes:"
247
- end
248
- # {
249
- # :change=>{
250
- # "resource"=>{"addr"=>"aws_iam_policy.crossplane_aws_ecr[0]", "module"=>"", "resource"=>"aws_iam_policy.crossplane_aws_ecr[0]", "implied_provider"=>"aws", "resource_type"=>"aws_iam_policy", "resource_name"=>"crossplane_aws_ecr", "resource_key"=>0},
251
- # "action"=>"update"
252
- # },
253
- # :type=>"resource_drift",
254
- # :level=>"info",
255
- # :message=>"aws_iam_policy.crossplane_aws_ecr[0]: Drift detected (update)",
256
- # :module=>"terraform.ui",
257
- # :timestamp=>"2023-09-26T17:11:46.340117-07:00",
258
- # :stream=>:stdout
259
- # }
260
-
261
- log format("[%<action>s] %<addr>s - Drift Detected (%<change_action>s)",
262
- action: PlanSummaryHandler.format_action(parsed_line[:change]["action"]),
263
- addr: PlanSummaryHandler.format_address(parsed_line[:change]["resource"]["addr"]),
264
- change_action: parsed_line[:change]["action"]), depth: 1
265
- when "planned_change"
266
- first_in_group = !seen.call(parsed_line[:module], "resource_drift") &&
267
- !seen.call(parsed_line[:module], "planned_change")
268
- # {
269
- # :change=>
270
- # {"resource"=>
271
- # {"addr"=>"module.application.kubectl_manifest.application",
272
- # "module"=>"module.application",
273
- # "resource"=>"kubectl_manifest.application",
274
- # "implied_provider"=>"kubectl",
275
- # "resource_type"=>"kubectl_manifest",
276
- # "resource_name"=>"application",
277
- # "resource_key"=>nil},
278
- # "action"=>"create"},
279
- # :type=>"planned_change",
280
- # :level=>"info",
281
- # :message=>"module.application.kubectl_manifest.application: Plan to create",
282
- # :module=>"terraform.ui",
283
- # :timestamp=>"2023-08-25T14:48:46.005185-07:00",
284
- # }
285
- if first_in_group
286
- log ""
287
- log ""
288
- log "Planned Changes:"
289
- end
290
- log format("[%<action>s] %<addr>s",
291
- action: PlanSummaryHandler.format_action(parsed_line[:change]["action"]),
292
- addr: PlanSummaryHandler.format_address(parsed_line[:change]["resource"]["addr"])), depth: 1
293
- when "change_summary"
294
- # {
295
- # :changes=>{"add"=>1, "change"=>0, "import"=>0, "remove"=>0, "operation"=>"plan"},
296
- # :type=>"change_summary",
297
- # :level=>"info",
298
- # :message=>"Plan: 1 to add, 0 to change, 0 to destroy.",
299
- # :module=>"terraform.ui",
300
- # :timestamp=>"2023-08-25T14:48:46.005211-07:00",
301
- # :stream=>:stdout
302
- # }
303
- log ""
304
- # puts parsed_line[:message]
305
- log "#{parsed_line[:changes]['operation'].capitalize} summary: " + parsed_line[:changes].without("operation").map { |k, v|
306
- color = PlanSummaryHandler.color_for_action(k)
307
- "#{pastel.yellow(v)} to #{pastel.decorate(k, color)}" if v.positive?
308
- }.compact.join(" ")
309
-
310
- else
311
- print_plan_line(parsed_line)
312
- end
223
+ parse_tf_ui_line(parsed_line, meta, seen, skip_plan_summary: true)
224
+ when "tofu.ui" # rubocop:disable Lint/DuplicateBranch
225
+ parse_tf_ui_line(parsed_line, meta, seen, skip_plan_summary: true)
313
226
  else
314
- print_plan_line(parsed_line)
227
+ print_plan_line(parsed_line, from: "pretty_plan_v2,info,else")
315
228
  end
316
229
  when "error"
317
230
  if parsed_line[:diagnostic]
318
231
  handled_error = false
319
232
  muted_error = false
233
+ current_meta_error = {}
320
234
  unless parsed_line[:module] == "terragrunt" && parsed_line[:type] == "tf_failed"
321
235
  meta[:errors] ||= []
322
- meta[:errors] << {
236
+ current_meta_error = {
323
237
  type: :error,
324
238
  message: parsed_line[:diagnostic]["summary"],
325
239
  body: parsed_line[:diagnostic]["detail"].split("\n")
326
240
  }
241
+ meta[:errors] << current_meta_error
327
242
  end
328
243
 
329
244
  if parsed_line[:diagnostic]["summary"] == "Error acquiring the state lock"
@@ -336,13 +251,19 @@ module MuxTf
336
251
 
337
252
  unless muted_error
338
253
  if handled_error
339
- print_plan_line(parsed_line, without: [:diagnostic])
254
+ print_plan_line(parsed_line, without: [:diagnostic], from: "pretty_plan_v2,error,handled")
340
255
  else
341
- print_plan_line(parsed_line)
256
+ # print_plan_line(parsed_line, from: "pretty_plan_v2,error,unhandled_error")
257
+ print_unhandled_error_line(parsed_line)
258
+ current_meta_error[:printed] = true
342
259
  end
343
260
  end
261
+ elsif parsed_line[:module] == :stderr && parsed_line[:type] == "unknown" && parsed_line[:message][0] == "{"
262
+ # probably a TG error line ...
263
+ # sometimes this could have multiple lines of json ..
264
+ print_tg_error_line(parsed_line)
344
265
  else
345
- print_plan_line(parsed_line)
266
+ print_plan_line(parsed_line, from: "pretty_plan_v2,error,else")
346
267
  end
347
268
  end
348
269
 
@@ -350,339 +271,179 @@ module MuxTf
350
271
  }
351
272
  [status.status, meta]
352
273
  end
353
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
354
-
355
- def pretty_plan_v1(filename, targets: []) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
356
- meta = {}
357
274
 
358
- parser = StatefulParser.new(normalizer: pastel.method(:strip))
275
+ def setup_plan_v1_parser(parser)
359
276
  parser.state(:info, /^Acquiring state lock/)
360
277
  parser.state(:error, /(Error locking state|Error:)/, [:none, :blank, :info, :reading])
361
278
  parser.state(:reading, /: (Reading...|Read complete after)/, [:none, :info, :reading])
362
279
  parser.state(:none, /^$/, [:reading])
363
- parser.state(:refreshing, /^.+: Refreshing state... \[id=/, [:none, :info, :reading])
280
+ parser.state(:refreshing, /^.+: Refreshing state... \[id=/, [:none, :info, :reading, :import])
364
281
  parser.state(:refreshing, /Refreshing Terraform state in-memory prior to plan.../,
365
282
  [:none, :blank, :info, :reading])
366
283
  parser.state(:none, /^----------+$/, [:refreshing])
367
284
  parser.state(:none, /^$/, [:refreshing])
368
285
 
286
+ parser.state(:import, /".+: Preparing import... \[id=.+\]$/, [:none, :import])
287
+ parser.state(:none, /^$/, [:import])
288
+
369
289
  parser.state(:output_info, /^Changes to Outputs:$/, [:none])
370
290
  parser.state(:none, /^$/, [:output_info])
371
291
 
372
- parser.state(:plan_info, /Terraform will perform the following actions:/, [:none])
292
+ parser.state(:plan_info, /(?:Terraform|OpenTofu) will perform the following actions:/, [:none])
293
+ parser.state(:plan_info, /You can apply this plan to save these new output values to the (?:Terraform|OpenTofu) state/, [:none])
373
294
  parser.state(:plan_summary, /^Plan:/, [:plan_info])
374
295
 
375
- parser.state(:plan_legend, /^Terraform used the selected providers to generate the following execution$/)
296
+ parser.state(:plan_legend, /^(?:Terraform|OpenTofu) used the selected providers to generate the following execution$/)
376
297
  parser.state(:none, /^$/, [:plan_legend])
377
298
 
378
- parser.state(:plan_info, /Terraform planned the following actions, but then encountered a problem:/, [:none])
299
+ parser.state(:plan_info, /(?:Terraform|OpenTofu) planned the following actions, but then encountered a problem:/, [:none])
379
300
  parser.state(:plan_info, /No changes. Your infrastructure matches the configuration./, [:none])
380
301
 
381
- parser.state(:plan_error, /Planning failed. Terraform encountered an error while generating this plan./, [:refreshing])
302
+ parser.state(:plan_error, /Planning failed. (?:Terraform|OpenTofu) encountered an error while generating this plan./, [:refreshing, :none])
382
303
 
383
304
  # this extends the error block to include the lock info
384
305
  parser.state(:error_lock_info, /Lock Info/, [:error_block_error])
385
306
  parser.state(:after_error, /^╵/, [:error_lock_info])
386
-
387
- setup_error_handling(parser, from_states: [:plan_error, :none, :blank, :info, :reading, :plan_summary, :refreshing])
388
-
389
- last_state = nil
390
-
391
- status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true, targets: targets) { |raw_line|
392
- parser.parse(raw_line.rstrip) do |state, line|
393
- first_in_state = last_state != state
394
-
395
- case state
396
- when :none
397
- if line.blank?
398
- # nothing
399
- elsif raw_line.match(/Error when retrieving token from sso/) || raw_line.match(/Error loading SSO Token/)
400
- meta[:need_auth] = true
401
- log pastel.red("authentication problem"), depth: 2
402
- else
403
- log_unhandled_line(state, line, reason: "unexpected non blank line in :none state")
404
- end
405
- when :reading
406
- clean_line = pastel.strip(line)
407
- if clean_line.match(/^(.+): Reading...$/)
408
- log "Reading: #{$LAST_MATCH_INFO[1]} ...", depth: 2
409
- elsif clean_line.match(/^(.+): Read complete after ([^\[]+)(?: \[(.+)\])?$/)
410
- if $LAST_MATCH_INFO[3]
411
- log "Reading Complete: #{$LAST_MATCH_INFO[1]} after #{$LAST_MATCH_INFO[2]} [#{$LAST_MATCH_INFO[3]}]", depth: 3
412
- else
413
- log "Reading Complete: #{$LAST_MATCH_INFO[1]} after #{$LAST_MATCH_INFO[2]}", depth: 3
414
- end
415
- else
416
- log_unhandled_line(state, line, reason: "unexpected line in :reading state")
417
- end
418
- when :info
419
- if /Acquiring state lock. This may take a few moments.../.match?(line)
420
- log "Acquiring state lock ...", depth: 2
421
- else
422
- log_unhandled_line(state, line, reason: "unexpected line in :info state")
423
- end
424
- when :plan_error
425
- case pastel.strip(line)
426
- when ""
427
- # skip empty line
428
- when /Releasing state lock. This may take a few moments"/
429
- log line, depth: 2
430
- when /Planning failed./ # rubocop:disable Lint/DuplicateBranch
431
- log line, depth: 2
432
- else
433
- log_unhandled_line(state, line, reason: "unexpected line in :plan_error state")
434
- end
435
- when :error_lock_info
436
- meta["error"] = "lock"
437
- meta[$LAST_MATCH_INFO[1]] = $LAST_MATCH_INFO[2] if line =~ /([A-Z]+\S+)+:\s+(.+)$/
438
- clean_line = pastel.strip(line).gsub(/^│ /, "")
439
- if clean_line == ""
440
- meta[:current_error][:body] << clean_line if meta[:current_error][:body].last != ""
441
- else
442
- meta[:current_error][:body] << clean_line
443
- end
444
- when :refreshing
445
- if first_in_state
446
- log "Refreshing state ", depth: 2, newline: false
447
- else
448
- print "."
449
- end
450
- when :plan_legend
451
- puts if first_in_state
452
- log line, depth: 2
453
- when :refresh_done
454
- puts if first_in_state
455
- when :plan_info # rubocop:disable Lint/DuplicateBranch
456
- puts if first_in_state
457
- log line, depth: 2
458
- when :output_info # rubocop:disable Lint/DuplicateBranch
459
- puts if first_in_state
460
- log line, depth: 2
461
- when :plan_summary
462
- log line, depth: 2
463
- else
464
- log_unhandled_line(state, line, reason: "unexpected state") unless handle_error_states(meta, state, line)
465
- end
466
- last_state = state
467
- end
468
- }
469
- [status.status, meta]
470
307
  end
471
308
 
472
- def init_status_to_remedies(status, meta)
473
- remedies = Set.new
474
- if status != 0
475
- remedies << :reconfigure if meta[:need_reconfigure]
476
- remedies << :auth if meta[:need_auth]
477
- log "!! expected meta[:errors] to be set, how did we get here?" unless meta[:errors]
478
- if meta[:errors]
479
- meta[:errors].each do |error|
480
- remedies << :add_provider_constraint if error[:body].grep(/Could not retrieve the list of available versions for provider/)
309
+ def handle_plan_v1_line(state, line, meta, first_in_state:, stripped_line:)
310
+ case state
311
+ when :none
312
+ if line.blank?
313
+ # nothing
314
+ elsif stripped_line.match(/Error when retrieving token from sso/) || stripped_line.match(/Error loading SSO Token/)
315
+ meta[:need_auth] = true
316
+ log pastel.red("authentication problem"), depth: 2
317
+ else
318
+ log_unhandled_line(state, line, reason: "unexpected non blank line in :none state")
319
+ end
320
+ when :reading
321
+ if stripped_line.match(/^(.+): Reading...$/)
322
+ log "Reading: #{$LAST_MATCH_INFO[1]} ...", depth: 2
323
+ elsif stripped_line.match(/^(.+): Read complete after ([^\[]+)(?: \[(.+)\])?$/)
324
+ if $LAST_MATCH_INFO[3]
325
+ log "Reading Complete: #{$LAST_MATCH_INFO[1]} after #{$LAST_MATCH_INFO[2]} [#{$LAST_MATCH_INFO[3]}]", depth: 3
326
+ else
327
+ log "Reading Complete: #{$LAST_MATCH_INFO[1]} after #{$LAST_MATCH_INFO[2]}", depth: 3
481
328
  end
329
+ else
330
+ log_unhandled_line(state, line, reason: "unexpected line in :reading state")
482
331
  end
483
- if remedies.empty?
484
- log "!! don't know how to generate init remedies for this"
485
- log "!! Status: #{status}"
486
- log "!! Meta:"
487
- log meta.to_yaml.split("\n").map { |l| "!! #{l}" }.join("\n")
488
- remedies << :unknown
332
+ when :import
333
+ # github_repository_topics.this[\"tf-k8s-infra-modules\"]: Preparing import... [id=tf-k8s-infra-modules]
334
+ matches = stripped_line.match(/^(?<resource>.+): Preparing import... \[id=(?<id>.+)\]$/)
335
+ if matches
336
+ log "Importing #{pastel.cyan(matches[:resource])} (id=#{pastel.yellow(matches[:id])}) ...", depth: 2
337
+ else
338
+ p [:import, "couldn't parse line:", stripped_line]
489
339
  end
490
- end
491
- remedies
492
- end
493
-
494
- def setup_error_handling(parser, from_states:)
495
- parser.state(:error_block, /^╷/, from_states | [:after_error])
496
- parser.state(:error_block_error, /^│ Error: /, [:error_block])
497
- parser.state(:error_block_warning, /^│ Warning: /, [:error_block])
498
- parser.state(:after_error, /^╵/, [:error_block, :error_block_error, :error_block_warning])
499
- end
500
-
501
- def handle_error_states(meta, state, line) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
502
- case state
503
- when :error_block
504
- meta[:current_error] = {
505
- type: :unknown,
506
- body: []
507
- }
508
- when :error_block_error, :error_block_warning
509
- clean_line = pastel.strip(line).gsub(/^│ /, "")
510
- if clean_line =~ /^(Warning|Error): (.+)$/
511
- meta[:current_error][:type] = $LAST_MATCH_INFO[1].downcase.to_sym
512
- meta[:current_error][:message] = $LAST_MATCH_INFO[2]
513
- elsif clean_line == ""
514
- # skip double empty lines
515
- meta[:current_error][:body] << clean_line if meta[:current_error][:body].last != ""
340
+ when :info
341
+ if /Acquiring state lock. This may take a few moments.../.match?(line)
342
+ log "Acquiring state lock ...", depth: 2
516
343
  else
517
- meta[:current_error][:body] ||= []
518
- meta[:current_error][:body] << clean_line
344
+ log_unhandled_line(state, line, reason: "unexpected line in :info state")
519
345
  end
520
- when :after_error
346
+ when :plan_error
521
347
  case pastel.strip(line)
522
- when "" # closing of an error block
523
- if meta[:current_error][:type] == :error
524
- meta[:errors] ||= []
525
- meta[:errors] << meta[:current_error]
526
- end
527
- if meta[:current_error][:type] == :warning
528
- meta[:warnings] ||= []
529
- meta[:warnings] << meta[:current_error]
530
- end
531
- meta.delete(:current_error)
348
+ when ""
349
+ # skip empty line
350
+ when /Releasing state lock. This may take a few moments"/
351
+ log line, depth: 2
352
+ when /Planning failed./ # rubocop:disable Lint/DuplicateBranch
353
+ log line, depth: 2
354
+ else
355
+ log_unhandled_line(state, line, reason: "unexpected line in :plan_error state")
532
356
  end
357
+ when :error_lock_info
358
+ meta["error"] = "lock"
359
+ meta[$LAST_MATCH_INFO[1]] = $LAST_MATCH_INFO[2] if line =~ /([A-Z]+\S+)+:\s+(.+)$/
360
+ if stripped_line == ""
361
+ meta[:current_error][:body] << stripped_line if meta[:current_error][:body].last != ""
362
+ else
363
+ meta[:current_error][:body] << stripped_line
364
+ end
365
+ when :refreshing
366
+ if first_in_state
367
+ log "Refreshing state ", depth: 2, newline: false
368
+ else
369
+ print "."
370
+ end
371
+ when :plan_legend
372
+ puts if first_in_state
373
+ log line, depth: 2
374
+ when :refresh_done
375
+ puts if first_in_state
376
+ when :plan_info # rubocop:disable Lint/DuplicateBranch
377
+ puts if first_in_state
378
+ log line, depth: 2
379
+ when :output_info # rubocop:disable Lint/DuplicateBranch
380
+ puts if first_in_state
381
+ log line, depth: 2
382
+ when :plan_summary
383
+ log line, depth: 2
533
384
  else
534
385
  return false
535
386
  end
536
387
  true
537
388
  end
538
389
 
539
- def run_tf_init(upgrade: nil, reconfigure: nil) # rubocop:disable Metrics/MethodLength
540
- phase = :init
541
-
390
+ def pretty_plan_v1(filename, targets: [])
542
391
  meta = {}
392
+ init_phase = :none
543
393
 
544
394
  parser = StatefulParser.new(normalizer: pastel.method(:strip))
545
395
 
546
- parser.state(:modules_init, /^Initializing modules\.\.\./, [:none, :backend])
547
- parser.state(:modules_upgrade, /^Upgrading modules\.\.\./)
548
- parser.state(:backend, /^Initializing the backend\.\.\./, [:none, :modules_init, :modules_upgrade])
549
- parser.state(:plugins, /^Initializing provider plugins\.\.\./, [:backend, :modules_init])
396
+ InitFormatter.setup_init_parser(parser)
397
+ setup_plan_v1_parser(parser)
550
398
 
551
- parser.state(:backend_error, /Error when retrieving token from sso/, [:backend])
399
+ setup_error_handling(parser,
400
+ from_states: [:plan_error, :none, :blank, :info, :reading, :plan_summary, :refreshing] + [:plugins, :modules_init])
552
401
 
553
- parser.state(:plugin_warnings, /^$/, [:plugins])
554
- parser.state(:backend_error, /Error:/, [:backend])
555
-
556
- setup_error_handling(parser, from_states: [:plugins, :modules_init])
402
+ last_state = nil
557
403
 
558
- status = tf_init(upgrade: upgrade, reconfigure: reconfigure) { |raw_line|
559
- stripped_line = pastel.strip(raw_line.rstrip)
404
+ stderr_handler = StderrLineHandler.new(operation: :plan)
560
405
 
561
- parser.parse(raw_line.rstrip) do |state, line|
562
- case state
563
- when :modules_init
564
- if phase != state
565
- phase = state
566
- log "Initializing modules ", depth: 1
567
- next
568
- end
569
- case stripped_line
570
- when /^Downloading (?<repo>[^ ]+) (?<version>[^ ]+) for (?<module>[^ ]+)\.\.\./
571
- print "D"
572
- when /^Downloading (?<repo>[^ ]+) for (?<module>[^ ]+)\.\.\./ # rubocop:disable Lint/DuplicateBranch
573
- print "D"
574
- when /^- (?<module>[^ ]+) in (?<path>.+)$/
575
- print "."
576
- when ""
577
- puts
578
- else
579
- log_unhandled_line(state, line, reason: "unexpected line in :modules_init state")
580
- end
581
- when :modules_upgrade
582
- if phase != state
583
- # first line
584
- phase = state
585
- log "Upgrding modules ", depth: 1, newline: false
586
- next
587
- end
588
- case stripped_line
589
- when /^- (?<module>[^ ]+) in (?<path>.+)$/
590
- print "."
591
- when /^Downloading (?<repo>[^ ]+) (?<version>[^ ]+) for (?<module>[^ ]+)\.\.\./
592
- print "D"
593
- when /^Downloading (?<repo>[^ ]+) for (?<module>[^ ]+)\.\.\./ # rubocop:disable Lint/DuplicateBranch
594
- print "D"
595
- when ""
596
- puts
597
- else
598
- log_unhandled_line(state, line, reason: "unexpected line in :modules_upgrade state")
599
- end
600
- when :backend
601
- if phase != state
602
- # first line
603
- phase = state
604
- log "Initializing the backend ", depth: 1 # , newline: false
605
- next
606
- end
607
- case stripped_line
608
- when /^Successfully configured/
609
- log line, depth: 2
610
- when /unless the backend/ # rubocop:disable Lint/DuplicateBranch
611
- log line, depth: 2
612
- when ""
613
- puts
614
- else
615
- log_unhandled_line(state, line, reason: "unexpected line in :backend state")
616
- end
617
- when :backend_error
618
- if raw_line.match "terraform init -reconfigure"
619
- meta[:need_reconfigure] = true
620
- log pastel.red("module needs to be reconfigured"), depth: 2
621
- end
622
- if raw_line.match "Error when retrieving token from sso"
623
- meta[:need_auth] = true
624
- log pastel.red("authentication problem"), depth: 2
625
- end
626
- when :plugins
627
- if phase != state
628
- # first line
629
- phase = state
630
- log "Initializing provider plugins ...", depth: 1
631
- next
632
- end
633
- case stripped_line
634
- when /^- Reusing previous version of (?<module>.+) from the dependency lock file$/
635
- info = $LAST_MATCH_INFO.named_captures
636
- log "- [FROM-LOCK] #{info['module']}", depth: 2
637
- when /^- (?<module>.+) is built in to Terraform$/
638
- info = $LAST_MATCH_INFO.named_captures
639
- log "- [BUILTIN] #{info['module']}", depth: 2
640
- when /^- Finding (?<module>[^ ]+) versions matching "(?<version>.+)"\.\.\./
641
- info = $LAST_MATCH_INFO.named_captures
642
- log "- [FIND] #{info['module']} matching #{info['version'].inspect}", depth: 2
643
- when /^- Finding latest version of (?<module>.+)\.\.\.$/
644
- info = $LAST_MATCH_INFO.named_captures
645
- log "- [FIND] #{info['module']}", depth: 2
646
- when /^- Installing (?<module>[^ ]+) v(?<version>.+)\.\.\.$/
647
- info = $LAST_MATCH_INFO.named_captures
648
- log "- [INSTALLING] #{info['module']} v#{info['version']}", depth: 2
649
- when /^- Installed (?<module>[^ ]+) v(?<version>.+) \(signed by(?: a)? (?<signed>.+)\)$/
650
- info = $LAST_MATCH_INFO.named_captures
651
- log "- [INSTALLED] #{info['module']} v#{info['version']} (#{info['signed']})", depth: 2
652
- when /^- Using previously-installed (?<module>[^ ]+) v(?<version>.+)$/
653
- info = $LAST_MATCH_INFO.named_captures
654
- log "- [USING] #{info['module']} v#{info['version']}", depth: 2
655
- when /^- Downloading plugin for provider "(?<provider>[^"]+)" \((?<provider_path>[^)]+)\) (?<version>.+)\.\.\.$/
656
- info = $LAST_MATCH_INFO.named_captures
657
- log "- #{info['provider']} #{info['version']}", depth: 2
658
- when "- Checking for available provider plugins..."
659
- # noop
406
+ status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true, targets: targets, split_streams: true) { |(stream, raw_line)|
407
+ case stream
408
+ when :command
409
+ log "Running command: #{raw_line.strip} ...", depth: 2
410
+ when :stderr
411
+ stderr_handler.handle(raw_line)
412
+ when :stdout
413
+ stripped_line = pastel.strip(raw_line.rstrip)
414
+ parser.parse(raw_line.rstrip) do |state, line|
415
+ first_in_state = last_state != state
416
+
417
+ if (handled = handle_plan_v1_line(state, line, meta, first_in_state: first_in_state, stripped_line: stripped_line))
418
+ # great!
419
+ elsif (handled = InitFormatter.handle_init_line(state, line, meta, phase: init_phase, stripped_line: stripped_line))
420
+ init_phase = handled[:phase]
421
+ elsif handle_error_states(meta, state, line)
422
+ # no-op
660
423
  else
661
- log_unhandled_line(state, line, reason: "unexpected line in :plugins state")
662
- end
663
- when :plugin_warnings
664
- if phase != state
665
- # first line
666
- phase = state
667
- next
424
+ log_unhandled_line(state, line, reason: "unexpected state")
668
425
  end
669
426
 
670
- log pastel.yellow(line), depth: 1
671
- when :none
672
- next if line == ""
673
-
674
- log_unhandled_line(state, line, reason: "unexpected line in :none state")
675
- else
676
- log_unhandled_line(state, line, reason: "unexpected state") unless handle_error_states(meta, state, line)
427
+ last_state = state
677
428
  end
678
429
  end
679
430
  }
680
431
 
432
+ stderr_handler.flush
433
+ stderr_handler.merge_meta_into(meta)
434
+
435
+ meta[:errors]&.each do |error|
436
+ if error[:message] == "Error acquiring the state lock"
437
+ meta["error"] = "lock"
438
+ meta.merge!(parse_lock_info(error[:body].join("\n")))
439
+ end
440
+ end
441
+
681
442
  [status.status, meta]
682
443
  end
683
444
 
684
445
  def print_validation_errors(info)
685
- return unless (info["error_count"]).positive? || (info["warning_count"]).positive?
446
+ return unless info["error_count"].positive? || info["warning_count"].positive?
686
447
 
687
448
  log "Encountered #{pastel.red(info['error_count'])} Errors and #{pastel.yellow(info['warning_count'])} Warnings!", depth: 2
688
449
  info["diagnostics"].each do |dinfo|
@@ -693,11 +454,10 @@ module MuxTf
693
454
  end
694
455
  end
695
456
 
696
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
697
- def process_validation(info) # rubocop:disable Metrics/CyclomaticComplexity
457
+ def process_validation(info)
698
458
  remedies = Set.new
699
459
 
700
- if (info["error_count"]).positive? || (info["warning_count"]).positive?
460
+ if info["error_count"].positive? || info["warning_count"].positive?
701
461
  info["diagnostics"].each do |dinfo| # rubocop:disable Metrics/BlockLength
702
462
  item_handled = false
703
463
 
@@ -707,7 +467,8 @@ module MuxTf
707
467
  /Module not installed/,
708
468
  /Module source has changed/,
709
469
  /Required plugins are not installed/,
710
- /Module version requirements have changed/
470
+ /Module version requirements have changed/,
471
+ /to install all modules required by this configuration/
711
472
  remedies << :init
712
473
  item_handled = true
713
474
  when /Missing required argument/,
@@ -722,7 +483,7 @@ module MuxTf
722
483
  item_handled = true
723
484
  end
724
485
 
725
- if dinfo["severity"] == "error" && dinfo["snippet"]
486
+ if !item_handled && dinfo["severity"] == "error" && dinfo["snippet"]
726
487
  # trying something new .. assuming anything with a snippet is a user error
727
488
  remedies << :user_error
728
489
  item_handled = true
@@ -749,84 +510,6 @@ module MuxTf
749
510
 
750
511
  remedies
751
512
  end
752
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
753
-
754
- private
755
-
756
- def format_validation_range(dinfo, color) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
757
- range = dinfo["range"]
758
- # filename: "../../../modules/pods/jane_pod/main.tf"
759
- # start:
760
- # line: 151
761
- # column: 27
762
- # byte: 6632
763
- # end:
764
- # line: 151
765
- # column: 53
766
- # byte: 6658
767
-
768
- context_lines = 3
769
-
770
- lines = range["start"]["line"]..range["end"]["line"]
771
- columns = range["start"]["column"]..range["end"]["column"]
772
-
773
- # on ../../../modules/pods/jane_pod/main.tf line 151, in module "jane":
774
- # 151: jane_resources_preset = var.jane_resources_presetx
775
- output = []
776
- lines_info = if lines.size == 1
777
- "#{lines.first}:#{columns.first}"
778
- else
779
- "#{lines.first}:#{columns.first} to #{lines.last}:#{columns.last}"
780
- end
781
- output << "on: #{range['filename']} line#{lines.size > 1 ? 's' : ''}: #{lines_info}"
782
-
783
- # TODO: in terragrunt mode, we need to somehow figure out the path to the cache root, all the paths will end up being relative to that
784
- if File.exist?(range["filename"])
785
- file_lines = File.read(range["filename"]).split("\n")
786
- extract_range = (([lines.first - context_lines,
787
- 0].max)..([lines.last + context_lines, file_lines.length - 1].min))
788
- file_lines.each_with_index do |line, index|
789
- if extract_range.cover?(index + 1)
790
- if lines.cover?(index + 1)
791
- start_col = 1
792
- end_col = :max
793
- if index + 1 == lines.first
794
- start_col = columns.first
795
- elsif index + 1 == lines.last
796
- start_col = columns.last
797
- end
798
- painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
799
- output << "#{pastel.decorate('>', color)} #{index + 1}: #{painted_line}"
800
- else
801
- output << " #{index + 1}: #{line}"
802
- end
803
- end
804
- end
805
- elsif dinfo["snippet"]
806
- # {
807
- # "context"=>"locals",
808
- # "code"=>" aws_iam_policy.crossplane_aws_ecr.arn",
809
- # "start_line"=>72,
810
- # "highlight_start_offset"=>8,
811
- # "highlight_end_offset"=>41,
812
- # "values"=>[]
813
- # }
814
- output << "Code:"
815
- dinfo["snippet"]["code"].split("\n").each do |l|
816
- output << " > #{l}"
817
- end
818
- end
819
-
820
- output
821
- end
822
-
823
- def paint_line(line, *paint_options, start_col: 1, end_col: :max)
824
- end_col = line.length if end_col == :max
825
- prefix = line[0, start_col - 1]
826
- suffix = line[end_col..]
827
- middle = line[start_col - 1..end_col - 1]
828
- "#{prefix}#{pastel.decorate(middle, *paint_options)}#{suffix}"
829
- end
830
513
  end
831
514
  end
832
515
  end