mux_tf 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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