mux_tf 0.13.0 → 0.14.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,14 +1,358 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MuxTf
4
- class PlanFormatter
4
+ class PlanFormatter # rubocop:disable Metrics/ClassLength
5
5
  extend TerraformHelpers
6
6
  extend PiotrbCliUtils::Util
7
+ include Coloring
7
8
 
8
- class << self
9
- def pretty_plan(filename, targets: []) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
10
- pastel = Pastel.new
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
13
+
14
+ def pretty_plan(filename, targets: [])
15
+ if ENV["JSON_PLAN"]
16
+ pretty_plan_v2(filename, targets: targets)
17
+ else
18
+ pretty_plan_v1(filename, targets: targets)
19
+ end
20
+ end
21
+
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
+ 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
131
+ end
132
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
133
+
134
+ def parse_lock_info(detail)
135
+ # Lock Info:
136
+ # ID: 4cc9c775-f0b7-3da7-25a4-94131afcef4d
137
+ # Path: jane-terraform-eks-dev/admin/apps/kube-system-event-bus/terraform.tfstate
138
+ # Operation: OperationTypePlan
139
+ # Who: piotr@Piotrs-Jane-MacBook-Pro.local
140
+ # Version: 1.5.4
141
+ # Created: 2023-08-25 19:03:38.821597 +0000 UTC
142
+ # Info:
143
+ result = {}
144
+ keys = %w[ID Path Operation Who Version Created]
145
+ keys.each do |key|
146
+ result[key] = detail.match(/^\s*#{key}:\s+(.+)$/)&.captures&.first
147
+ end
148
+ result
149
+ end
11
150
 
151
+ def print_plan_line(parsed_line, without: [])
152
+ default_without = [
153
+ :level,
154
+ :module,
155
+ :type,
156
+ :stream,
157
+ :message,
158
+ :timestamp,
159
+ :terraform,
160
+ :ui
161
+ ]
162
+ extra = parsed_line.without(*default_without, *without)
163
+ data = parsed_line.merge(extra: extra)
164
+ log_line = [
165
+ "%<level>-6s",
166
+ "%<module>-12s",
167
+ "%<type>-10s",
168
+ "%<message>s",
169
+ "%<extra>s"
170
+ ].map { |format_string|
171
+ field = format_string.match(/%<([^>]+)>/)[1].to_sym
172
+ data[field].present? ? format(format_string, data) : nil
173
+ }.compact.join(" | ")
174
+ log log_line
175
+ end
176
+
177
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
178
+ def pretty_plan_v2(filename, targets: [])
179
+ meta = {}
180
+ meta[:seen] = {
181
+ module_and_type: Set.new
182
+ }
183
+
184
+ status = tf_plan_json(out: filename, targets: targets) { |(parsed_line)|
185
+ seen = proc { |module_arg, type_arg| meta[:seen][:module_and_type].include?([module_arg, type_arg]) }
186
+ # first_in_state = !seen.call(parsed_line[:module], parsed_line[:type])
187
+
188
+ case parsed_line[:level]
189
+ when "info"
190
+ case parsed_line[:module]
191
+ 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
313
+ else
314
+ print_plan_line(parsed_line)
315
+ end
316
+ when "error"
317
+ if parsed_line[:diagnostic]
318
+ handled_error = false
319
+ muted_error = false
320
+ unless parsed_line[:module] == "terragrunt" && parsed_line[:type] == "tf_failed"
321
+ meta[:errors] ||= []
322
+ meta[:errors] << {
323
+ type: :error,
324
+ message: parsed_line[:diagnostic]["summary"],
325
+ body: parsed_line[:diagnostic]["detail"].split("\n")
326
+ }
327
+ end
328
+
329
+ if parsed_line[:diagnostic]["summary"] == "Error acquiring the state lock"
330
+ meta["error"] = "lock"
331
+ meta.merge!(parse_lock_info(parsed_line[:diagnostic]["detail"]))
332
+ handled_error = true
333
+ elsif parsed_line[:module] == "terragrunt" && parsed_line[:type] == "tf_failed"
334
+ muted_error = true
335
+ end
336
+
337
+ unless muted_error
338
+ if handled_error
339
+ print_plan_line(parsed_line, without: [:diagnostic])
340
+ else
341
+ print_plan_line(parsed_line)
342
+ end
343
+ end
344
+ else
345
+ print_plan_line(parsed_line)
346
+ end
347
+ end
348
+
349
+ meta[:seen][:module_and_type] << [parsed_line[:module], parsed_line[:type]]
350
+ }
351
+ [status.status, meta]
352
+ 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
12
356
  meta = {}
13
357
 
14
358
  parser = StatefulParser.new(normalizer: pastel.method(:strip))
@@ -19,25 +363,28 @@ module MuxTf
19
363
  parser.state(:refreshing, /^.+: Refreshing state... \[id=/, [:none, :info, :reading])
20
364
  parser.state(:refreshing, /Refreshing Terraform state in-memory prior to plan.../,
21
365
  [:none, :blank, :info, :reading])
22
- parser.state(:refresh_done, /^----------+$/, [:refreshing])
23
- parser.state(:refresh_done, /^$/, [:refreshing])
366
+ parser.state(:none, /^----------+$/, [:refreshing])
367
+ parser.state(:none, /^$/, [:refreshing])
24
368
 
25
- parser.state(:output_info, /^Changes to Outputs:$/, [:refresh_done])
26
- parser.state(:refresh_done, /^$/, [:output_info])
369
+ parser.state(:output_info, /^Changes to Outputs:$/, [:none])
370
+ parser.state(:none, /^$/, [:output_info])
27
371
 
28
- parser.state(:plan_info, /Terraform will perform the following actions:/, [:refresh_done, :none])
372
+ parser.state(:plan_info, /Terraform will perform the following actions:/, [:none])
29
373
  parser.state(:plan_summary, /^Plan:/, [:plan_info])
30
374
 
31
375
  parser.state(:plan_legend, /^Terraform used the selected providers to generate the following execution$/)
32
376
  parser.state(:none, /^$/, [:plan_legend])
33
377
 
34
- parser.state(:error_lock_info, /Lock Info/, [:plan_error_error])
378
+ parser.state(:plan_info, /Terraform planned the following actions, but then encountered a problem:/, [:none])
379
+ parser.state(:plan_info, /No changes. Your infrastructure matches the configuration./, [:none])
380
+
381
+ parser.state(:plan_error, /Planning failed. Terraform encountered an error while generating this plan./, [:refreshing])
382
+
383
+ # this extends the error block to include the lock info
384
+ parser.state(:error_lock_info, /Lock Info/, [:error_block_error])
385
+ parser.state(:after_error, /^╵/, [:error_lock_info])
35
386
 
36
- parser.state(:plan_error, /Planning failed. Terraform encountered an error while generating this plan./, [:refreshing, :refresh_done])
37
- parser.state(:plan_error_block, /^╷/, [:plan_error, :none, :blank, :info, :reading])
38
- parser.state(:plan_error_warning, /^│ Warning: /, [:plan_error_block])
39
- parser.state(:plan_error_error, /^│ Error: /, [:plan_error_block])
40
- parser.state(:plan_error, /^╵/, [:plan_error_warning, :plan_error_error, :plan_error_block, :error_lock_info])
387
+ setup_error_handling(parser, from_states: [:plan_error, :none, :blank, :info, :reading, :plan_summary, :refreshing])
41
388
 
42
389
  last_state = nil
43
390
 
@@ -50,7 +397,7 @@ module MuxTf
50
397
  if line.blank?
51
398
  # nothing
52
399
  else
53
- p [state, line]
400
+ log_unhandled_line(state, line, reason: "unexpected non blank line in :none state")
54
401
  end
55
402
  when :reading
56
403
  clean_line = pastel.strip(line)
@@ -63,43 +410,16 @@ module MuxTf
63
410
  log "Reading Complete: #{$LAST_MATCH_INFO[1]} after #{$LAST_MATCH_INFO[2]}", depth: 3
64
411
  end
65
412
  else
66
- p [state, line]
413
+ log_unhandled_line(state, line, reason: "unexpected line in :reading state")
67
414
  end
68
415
  when :info
69
416
  if /Acquiring state lock. This may take a few moments.../.match?(line)
70
417
  log "Acquiring state lock ...", depth: 2
71
418
  else
72
- p [state, line]
73
- end
74
- when :plan_error_block
75
- meta[:current_error] = {
76
- type: :unknown,
77
- body: []
78
- }
79
- when :plan_error_warning, :plan_error_error
80
- clean_line = pastel.strip(line).gsub(/^│ /, "")
81
- if clean_line =~ /^(Warning|Error): (.+)$/
82
- meta[:current_error][:type] = $LAST_MATCH_INFO[1].downcase.to_sym
83
- meta[:current_error][:message] = $LAST_MATCH_INFO[2]
84
- elsif clean_line == ""
85
- # skip double empty lines
86
- meta[:current_error][:body] << clean_line if meta[:current_error][:body].last != ""
87
- else
88
- meta[:current_error][:body] ||= []
89
- meta[:current_error][:body] << clean_line
419
+ log_unhandled_line(state, line, reason: "unexpected line in :info state")
90
420
  end
91
421
  when :plan_error
92
422
  case pastel.strip(line)
93
- when "╵" # closing of an error block
94
- if meta[:current_error][:type] == :error
95
- meta[:errors] ||= []
96
- meta[:errors] << meta[:current_error]
97
- end
98
- if meta[:current_error][:type] == :warning
99
- meta[:warnings] ||= []
100
- meta[:warnings] << meta[:current_error]
101
- end
102
- meta.delete(:current_error)
103
423
  when ""
104
424
  # skip empty line
105
425
  when /Releasing state lock. This may take a few moments"/
@@ -107,7 +427,7 @@ module MuxTf
107
427
  when /Planning failed./ # rubocop:disable Lint/DuplicateBranch
108
428
  log line, depth: 2
109
429
  else
110
- p [state, line]
430
+ log_unhandled_line(state, line, reason: "unexpected line in :plan_error state")
111
431
  end
112
432
  when :error_lock_info
113
433
  meta["error"] = "lock"
@@ -118,7 +438,6 @@ module MuxTf
118
438
  else
119
439
  meta[:current_error][:body] << clean_line
120
440
  end
121
- # log Paint[line, :red], depth: 2
122
441
  when :refreshing
123
442
  if first_in_state
124
443
  log "Refreshing state ", depth: 2, newline: false
@@ -139,7 +458,7 @@ module MuxTf
139
458
  when :plan_summary
140
459
  log line, depth: 2
141
460
  else
142
- p [state, pastel.strip(line)]
461
+ log_unhandled_line(state, line, reason: "unexpected state") unless handle_error_states(meta, state, line)
143
462
  end
144
463
  last_state = state
145
464
  end
@@ -150,19 +469,67 @@ module MuxTf
150
469
  def init_status_to_remedies(status, meta)
151
470
  remedies = Set.new
152
471
  if status != 0
153
- if meta[:need_reconfigure]
154
- remedies << :reconfigure
155
- else
156
- p [status, meta]
472
+ remedies << :reconfigure if meta[:need_reconfigure]
473
+ meta[:errors].each do |error|
474
+ remedies << :add_provider_constraint if error[:body].grep(/Could not retrieve the list of available versions for provider/)
475
+ end
476
+ if remedies.empty?
477
+ log "!! don't know how to generate init remedies for this"
478
+ log "!! Status: #{status}"
479
+ log "!! Meta:"
480
+ log meta.to_yaml.split("\n").map { |l| "!! #{l}" }.join("\n")
157
481
  remedies << :unknown
158
482
  end
159
483
  end
160
484
  remedies
161
485
  end
162
486
 
163
- def run_tf_init(upgrade: nil, reconfigure: nil) # rubocop:disable Metrics/MethodLength
164
- pastel = Pastel.new
487
+ def setup_error_handling(parser, from_states:)
488
+ parser.state(:error_block, /^╷/, from_states | [:after_error])
489
+ parser.state(:error_block_error, /^│ Error: /, [:error_block])
490
+ parser.state(:error_block_warning, /^│ Warning: /, [:error_block])
491
+ parser.state(:after_error, /^╵/, [:error_block, :error_block_error, :error_block_warning])
492
+ end
165
493
 
494
+ def handle_error_states(meta, state, line) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
495
+ case state
496
+ when :error_block
497
+ meta[:current_error] = {
498
+ type: :unknown,
499
+ body: []
500
+ }
501
+ when :error_block_error, :error_block_warning
502
+ clean_line = pastel.strip(line).gsub(/^│ /, "")
503
+ if clean_line =~ /^(Warning|Error): (.+)$/
504
+ meta[:current_error][:type] = $LAST_MATCH_INFO[1].downcase.to_sym
505
+ meta[:current_error][:message] = $LAST_MATCH_INFO[2]
506
+ elsif clean_line == ""
507
+ # skip double empty lines
508
+ meta[:current_error][:body] << clean_line if meta[:current_error][:body].last != ""
509
+ else
510
+ meta[:current_error][:body] ||= []
511
+ meta[:current_error][:body] << clean_line
512
+ end
513
+ when :after_error
514
+ case pastel.strip(line)
515
+ when "╵" # closing of an error block
516
+ if meta[:current_error][:type] == :error
517
+ meta[:errors] ||= []
518
+ meta[:errors] << meta[:current_error]
519
+ end
520
+ if meta[:current_error][:type] == :warning
521
+ meta[:warnings] ||= []
522
+ meta[:warnings] << meta[:current_error]
523
+ end
524
+ meta.delete(:current_error)
525
+ end
526
+ else
527
+ return false
528
+ end
529
+ true
530
+ end
531
+
532
+ def run_tf_init(upgrade: nil, reconfigure: nil) # rubocop:disable Metrics/MethodLength
166
533
  phase = :init
167
534
 
168
535
  meta = {}
@@ -177,6 +544,8 @@ module MuxTf
177
544
  parser.state(:plugin_warnings, /^$/, [:plugins])
178
545
  parser.state(:backend_error, /Error:/, [:backend])
179
546
 
547
+ setup_error_handling(parser, from_states: [:plugins])
548
+
180
549
  status = tf_init(upgrade: upgrade, reconfigure: reconfigure) { |raw_line|
181
550
  stripped_line = pastel.strip(raw_line.rstrip)
182
551
 
@@ -198,7 +567,7 @@ module MuxTf
198
567
  when ""
199
568
  puts
200
569
  else
201
- p [state, stripped_line]
570
+ log_unhandled_line(state, line, reason: "unexpected line in :modules_init state")
202
571
  end
203
572
  when :modules_upgrade
204
573
  if phase != state
@@ -217,13 +586,13 @@ module MuxTf
217
586
  when ""
218
587
  puts
219
588
  else
220
- p [state, stripped_line]
589
+ log_unhandled_line(state, line, reason: "unexpected line in :modules_upgrade state")
221
590
  end
222
591
  when :backend
223
592
  if phase != state
224
593
  # first line
225
594
  phase = state
226
- log "Initializing the backend ", depth: 1, newline: false
595
+ log "Initializing the backend ", depth: 1 # , newline: false
227
596
  next
228
597
  end
229
598
  case stripped_line
@@ -234,12 +603,12 @@ module MuxTf
234
603
  when ""
235
604
  puts
236
605
  else
237
- p [state, stripped_line]
606
+ log_unhandled_line(state, line, reason: "unexpected line in :backend state")
238
607
  end
239
608
  when :backend_error
240
609
  if raw_line.match "terraform init -reconfigure"
241
610
  meta[:need_reconfigure] = true
242
- log Paint["module needs to be reconfigured", :red], depth: 2
611
+ log pastel.red("module needs to be reconfigured"), depth: 2
243
612
  end
244
613
  when :plugins
245
614
  if phase != state
@@ -276,7 +645,7 @@ module MuxTf
276
645
  when "- Checking for available provider plugins..."
277
646
  # noop
278
647
  else
279
- p [state, line]
648
+ log_unhandled_line(state, line, reason: "unexpected line in :plugins state")
280
649
  end
281
650
  when :plugin_warnings
282
651
  if phase != state
@@ -285,13 +654,13 @@ module MuxTf
285
654
  next
286
655
  end
287
656
 
288
- log Paint[line, :yellow], depth: 1
657
+ log pastel.yellow(line), depth: 1
289
658
  when :none
290
659
  next if line == ""
291
660
 
292
- p [state, line]
661
+ log_unhandled_line(state, line, reason: "unexpected line in :none state")
293
662
  else
294
- p [state, line]
663
+ log_unhandled_line(state, line, reason: "unexpected state") unless handle_error_states(meta, state, line)
295
664
  end
296
665
  end
297
666
  }
@@ -299,35 +668,75 @@ module MuxTf
299
668
  [status.status, meta]
300
669
  end
301
670
 
302
- def process_validation(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
671
+ def print_validation_errors(info)
672
+ return unless (info["error_count"]).positive? || (info["warning_count"]).positive?
673
+
674
+ log "Encountered #{pastel.red(info['error_count'])} Errors and #{pastel.yellow(info['warning_count'])} Warnings!", depth: 2
675
+ info["diagnostics"].each do |dinfo|
676
+ color = dinfo["severity"] == "error" ? :red : :yellow
677
+ log "#{pastel.decorate(dinfo['severity'].capitalize, color)}: #{dinfo['summary']}", depth: 3
678
+ log dinfo["detail"].split("\n"), depth: 4 if dinfo["detail"]
679
+ log format_validation_range(dinfo, color), depth: 4 if dinfo["range"]
680
+ end
681
+ end
682
+
683
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
684
+ def process_validation(info) # rubocop:disable Metrics/CyclomaticComplexity
303
685
  remedies = Set.new
304
686
 
305
687
  if (info["error_count"]).positive? || (info["warning_count"]).positive?
306
- log "Encountered #{Paint[info['error_count'], :red]} Errors and #{Paint[info['warning_count'], :yellow]} Warnings!", depth: 2
307
- info["diagnostics"].each do |dinfo|
308
- color = dinfo["severity"] == "error" ? :red : :yellow
309
- log "#{Paint[dinfo['severity'].capitalize, color]}: #{dinfo['summary']}", depth: 3
310
- if dinfo["detail"]&.include?("terraform init")
311
- remedies << :init
312
- elsif /there is no package for .+ cached in/.match?(dinfo["summary"]) # rubocop:disable Lint/DuplicateBranch
313
- remedies << :init
314
- elsif dinfo["detail"]&.include?("timeout while waiting for plugin to start") # rubocop:disable Lint/DuplicateBranch
688
+ info["diagnostics"].each do |dinfo| # rubocop:disable Metrics/BlockLength
689
+ item_handled = false
690
+
691
+ case dinfo["summary"]
692
+ when /there is no package for .+ cached in/,
693
+ /Missing required provider/,
694
+ /Module not installed/,
695
+ /Module source has changed/,
696
+ /Required plugins are not installed/,
697
+ /Module version requirements have changed/
315
698
  remedies << :init
316
- else
317
- log dinfo["detail"], depth: 4 if dinfo["detail"]
318
- log format_validation_range(dinfo["range"], color), depth: 4 if dinfo["range"]
699
+ item_handled = true
700
+ when /Missing required argument/,
701
+ /Error in function call/,
702
+ /Invalid value for input variable/,
703
+ /Unsupported block type/,
704
+ /Reference to undeclared input variable/,
705
+ /Invalid reference/,
706
+ /Unsupported attribute/,
707
+ /Invalid depends_on reference/
708
+ remedies << :user_error
709
+ item_handled = true
710
+ end
319
711
 
320
- remedies << :unknown if dinfo["severity"] == "error"
712
+ if dinfo["severity"] == "error" && dinfo["snippet"]
713
+ # trying something new .. assuming anything with a snippet is a user error
714
+ remedies << :user_error
715
+ item_handled = true
321
716
  end
717
+
718
+ case dinfo["detail"]
719
+ when /timeout while waiting for plugin to start/
720
+ remedies << :init
721
+ item_handled = true
722
+ end
723
+
724
+ next if item_handled
725
+
726
+ puts "!! don't know how to handle this validation error"
727
+ puts dinfo.inspect
728
+ remedies << :unknown if dinfo["severity"] == "error"
322
729
  end
323
730
  end
324
731
 
325
732
  remedies
326
733
  end
734
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
327
735
 
328
736
  private
329
737
 
330
- def format_validation_range(range, color) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
738
+ def format_validation_range(dinfo, color) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
739
+ range = dinfo["range"]
331
740
  # filename: "../../../modules/pods/jane_pod/main.tf"
332
741
  # start:
333
742
  # line: 151
@@ -353,6 +762,7 @@ module MuxTf
353
762
  end
354
763
  output << "on: #{range['filename']} line#{lines.size > 1 ? 's' : ''}: #{lines_info}"
355
764
 
765
+ # 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
356
766
  if File.exist?(range["filename"])
357
767
  file_lines = File.read(range["filename"]).split("\n")
358
768
  extract_range = (([lines.first - context_lines,
@@ -368,12 +778,25 @@ module MuxTf
368
778
  start_col = columns.last
369
779
  end
370
780
  painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
371
- output << "#{Paint['>', color]} #{index + 1}: #{painted_line}"
781
+ output << "#{pastel.decorate('>', color)} #{index + 1}: #{painted_line}"
372
782
  else
373
783
  output << " #{index + 1}: #{line}"
374
784
  end
375
785
  end
376
786
  end
787
+ elsif dinfo["snippet"]
788
+ # {
789
+ # "context"=>"locals",
790
+ # "code"=>" aws_iam_policy.crossplane_aws_ecr.arn",
791
+ # "start_line"=>72,
792
+ # "highlight_start_offset"=>8,
793
+ # "highlight_end_offset"=>41,
794
+ # "values"=>[]
795
+ # }
796
+ output << "Code:"
797
+ dinfo["snippet"]["code"].split("\n").each do |l|
798
+ output << " > #{l}"
799
+ end
377
800
  end
378
801
 
379
802
  output
@@ -384,7 +807,7 @@ module MuxTf
384
807
  prefix = line[0, start_col - 1]
385
808
  suffix = line[end_col..]
386
809
  middle = line[start_col - 1..end_col - 1]
387
- "#{prefix}#{Paint[middle, *paint_options]}#{suffix}"
810
+ "#{prefix}#{pastel.decorate(middle, *paint_options)}#{suffix}"
388
811
  end
389
812
  end
390
813
  end