mux_tf 0.12.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,40 +1,390 @@
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
11
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
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))
15
359
  parser.state(:info, /^Acquiring state lock/)
16
- parser.state(:error, /(╷|Error locking state|Error:)/, [:none, :blank, :info, :reading])
360
+ parser.state(:error, /(Error locking state|Error:)/, [:none, :blank, :info, :reading])
17
361
  parser.state(:reading, /: (Reading...|Read complete after)/, [:none, :info, :reading])
18
362
  parser.state(:none, /^$/, [:reading])
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/, [:error])
35
- parser.state(:error, /^$/, [:error_lock_info])
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])
36
380
 
37
- parser.state(:plan_error, /^╷|Error: /, [:refreshing, :refresh_done])
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])
386
+
387
+ setup_error_handling(parser, from_states: [:plan_error, :none, :blank, :info, :reading, :plan_summary, :refreshing])
38
388
 
39
389
  last_state = nil
40
390
 
@@ -47,7 +397,7 @@ module MuxTf
47
397
  if line.blank?
48
398
  # nothing
49
399
  else
50
- p [state, line]
400
+ log_unhandled_line(state, line, reason: "unexpected non blank line in :none state")
51
401
  end
52
402
  when :reading
53
403
  clean_line = pastel.strip(line)
@@ -60,24 +410,34 @@ module MuxTf
60
410
  log "Reading Complete: #{$LAST_MATCH_INFO[1]} after #{$LAST_MATCH_INFO[2]}", depth: 3
61
411
  end
62
412
  else
63
- p [state, line]
413
+ log_unhandled_line(state, line, reason: "unexpected line in :reading state")
64
414
  end
65
415
  when :info
66
416
  if /Acquiring state lock. This may take a few moments.../.match?(line)
67
417
  log "Acquiring state lock ...", depth: 2
68
418
  else
69
- p [state, line]
419
+ log_unhandled_line(state, line, reason: "unexpected line in :info state")
70
420
  end
71
- when :error
72
- meta["error"] = "lock"
73
- log Paint[line, :red], depth: 2
74
421
  when :plan_error
75
- puts if first_in_state
76
- meta["error"] = "refresh"
77
- log Paint[line, :red], depth: 2
422
+ case pastel.strip(line)
423
+ when ""
424
+ # skip empty line
425
+ when /Releasing state lock. This may take a few moments"/
426
+ log line, depth: 2
427
+ when /Planning failed./ # rubocop:disable Lint/DuplicateBranch
428
+ log line, depth: 2
429
+ else
430
+ log_unhandled_line(state, line, reason: "unexpected line in :plan_error state")
431
+ end
78
432
  when :error_lock_info
433
+ meta["error"] = "lock"
79
434
  meta[$LAST_MATCH_INFO[1]] = $LAST_MATCH_INFO[2] if line =~ /([A-Z]+\S+)+:\s+(.+)$/
80
- log Paint[line, :red], depth: 2
435
+ clean_line = pastel.strip(line).gsub(/^│ /, "")
436
+ if clean_line == ""
437
+ meta[:current_error][:body] << clean_line if meta[:current_error][:body].last != ""
438
+ else
439
+ meta[:current_error][:body] << clean_line
440
+ end
81
441
  when :refreshing
82
442
  if first_in_state
83
443
  log "Refreshing state ", depth: 2, newline: false
@@ -98,7 +458,7 @@ module MuxTf
98
458
  when :plan_summary
99
459
  log line, depth: 2
100
460
  else
101
- p [state, pastel.strip(line)]
461
+ log_unhandled_line(state, line, reason: "unexpected state") unless handle_error_states(meta, state, line)
102
462
  end
103
463
  last_state = state
104
464
  end
@@ -109,19 +469,67 @@ module MuxTf
109
469
  def init_status_to_remedies(status, meta)
110
470
  remedies = Set.new
111
471
  if status != 0
112
- if meta[:need_reconfigure]
113
- remedies << :reconfigure
114
- else
115
- 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")
116
481
  remedies << :unknown
117
482
  end
118
483
  end
119
484
  remedies
120
485
  end
121
486
 
122
- def run_tf_init(upgrade: nil, reconfigure: nil) # rubocop:disable Metrics/MethodLength
123
- 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
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
124
531
 
532
+ def run_tf_init(upgrade: nil, reconfigure: nil) # rubocop:disable Metrics/MethodLength
125
533
  phase = :init
126
534
 
127
535
  meta = {}
@@ -136,6 +544,8 @@ module MuxTf
136
544
  parser.state(:plugin_warnings, /^$/, [:plugins])
137
545
  parser.state(:backend_error, /Error:/, [:backend])
138
546
 
547
+ setup_error_handling(parser, from_states: [:plugins])
548
+
139
549
  status = tf_init(upgrade: upgrade, reconfigure: reconfigure) { |raw_line|
140
550
  stripped_line = pastel.strip(raw_line.rstrip)
141
551
 
@@ -157,7 +567,7 @@ module MuxTf
157
567
  when ""
158
568
  puts
159
569
  else
160
- p [state, stripped_line]
570
+ log_unhandled_line(state, line, reason: "unexpected line in :modules_init state")
161
571
  end
162
572
  when :modules_upgrade
163
573
  if phase != state
@@ -176,13 +586,13 @@ module MuxTf
176
586
  when ""
177
587
  puts
178
588
  else
179
- p [state, stripped_line]
589
+ log_unhandled_line(state, line, reason: "unexpected line in :modules_upgrade state")
180
590
  end
181
591
  when :backend
182
592
  if phase != state
183
593
  # first line
184
594
  phase = state
185
- log "Initializing the backend ", depth: 1, newline: false
595
+ log "Initializing the backend ", depth: 1 # , newline: false
186
596
  next
187
597
  end
188
598
  case stripped_line
@@ -193,12 +603,12 @@ module MuxTf
193
603
  when ""
194
604
  puts
195
605
  else
196
- p [state, stripped_line]
606
+ log_unhandled_line(state, line, reason: "unexpected line in :backend state")
197
607
  end
198
608
  when :backend_error
199
609
  if raw_line.match "terraform init -reconfigure"
200
610
  meta[:need_reconfigure] = true
201
- log Paint["module needs to be reconfigured", :red], depth: 2
611
+ log pastel.red("module needs to be reconfigured"), depth: 2
202
612
  end
203
613
  when :plugins
204
614
  if phase != state
@@ -235,7 +645,7 @@ module MuxTf
235
645
  when "- Checking for available provider plugins..."
236
646
  # noop
237
647
  else
238
- p [state, line]
648
+ log_unhandled_line(state, line, reason: "unexpected line in :plugins state")
239
649
  end
240
650
  when :plugin_warnings
241
651
  if phase != state
@@ -244,13 +654,13 @@ module MuxTf
244
654
  next
245
655
  end
246
656
 
247
- log Paint[line, :yellow], depth: 1
657
+ log pastel.yellow(line), depth: 1
248
658
  when :none
249
659
  next if line == ""
250
660
 
251
- p [state, line]
661
+ log_unhandled_line(state, line, reason: "unexpected line in :none state")
252
662
  else
253
- p [state, line]
663
+ log_unhandled_line(state, line, reason: "unexpected state") unless handle_error_states(meta, state, line)
254
664
  end
255
665
  end
256
666
  }
@@ -258,33 +668,75 @@ module MuxTf
258
668
  [status.status, meta]
259
669
  end
260
670
 
261
- 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
262
685
  remedies = Set.new
263
686
 
264
687
  if (info["error_count"]).positive? || (info["warning_count"]).positive?
265
- log "Encountered #{Paint[info['error_count'], :red]} Errors and #{Paint[info['warning_count'], :yellow]} Warnings!", depth: 2
266
- info["diagnostics"].each do |dinfo|
267
- color = dinfo["severity"] == "error" ? :red : :yellow
268
- log "#{Paint[dinfo['severity'].capitalize, color]}: #{dinfo['summary']}", depth: 3
269
- if dinfo["detail"]&.include?("terraform init")
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/
270
698
  remedies << :init
271
- elsif /there is no package for .+ cached in/.match?(dinfo["summary"]) # rubocop:disable Lint/DuplicateBranch
272
- remedies << :init
273
- else
274
- log dinfo["detail"], depth: 4 if dinfo["detail"]
275
- 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
276
711
 
277
- 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
278
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"
279
729
  end
280
730
  end
281
731
 
282
732
  remedies
283
733
  end
734
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
284
735
 
285
736
  private
286
737
 
287
- 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"]
288
740
  # filename: "../../../modules/pods/jane_pod/main.tf"
289
741
  # start:
290
742
  # line: 151
@@ -310,6 +762,7 @@ module MuxTf
310
762
  end
311
763
  output << "on: #{range['filename']} line#{lines.size > 1 ? 's' : ''}: #{lines_info}"
312
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
313
766
  if File.exist?(range["filename"])
314
767
  file_lines = File.read(range["filename"]).split("\n")
315
768
  extract_range = (([lines.first - context_lines,
@@ -325,12 +778,25 @@ module MuxTf
325
778
  start_col = columns.last
326
779
  end
327
780
  painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
328
- output << "#{Paint['>', color]} #{index + 1}: #{painted_line}"
781
+ output << "#{pastel.decorate('>', color)} #{index + 1}: #{painted_line}"
329
782
  else
330
783
  output << " #{index + 1}: #{line}"
331
784
  end
332
785
  end
333
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
334
800
  end
335
801
 
336
802
  output
@@ -341,7 +807,7 @@ module MuxTf
341
807
  prefix = line[0, start_col - 1]
342
808
  suffix = line[end_col..]
343
809
  middle = line[start_col - 1..end_col - 1]
344
- "#{prefix}#{Paint[middle, *paint_options]}#{suffix}"
810
+ "#{prefix}#{pastel.decorate(middle, *paint_options)}#{suffix}"
345
811
  end
346
812
  end
347
813
  end