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.
- checksums.yaml +4 -4
- data/exe/tf_current +2 -0
- data/exe/tf_mux +2 -0
- data/exe/tf_plan_summary +2 -0
- data/lib/deps.rb +10 -1
- data/lib/mux_tf/cli/current.rb +192 -76
- data/lib/mux_tf/cli/plan_summary.rb +2 -1
- data/lib/mux_tf/cli.rb +3 -3
- data/lib/mux_tf/coloring.rb +23 -0
- data/lib/mux_tf/plan_formatter.rb +503 -80
- data/lib/mux_tf/plan_summary_handler.rb +99 -94
- data/lib/mux_tf/plan_utils.rb +360 -0
- data/lib/mux_tf/terraform_helpers.rb +51 -9
- data/lib/mux_tf/version.rb +1 -1
- data/lib/mux_tf.rb +18 -10
- data/mux_tf.gemspec +3 -1
- metadata +33 -3
@@ -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
|
10
|
-
|
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(:
|
23
|
-
parser.state(:
|
366
|
+
parser.state(:none, /^----------+$/, [:refreshing])
|
367
|
+
parser.state(:none, /^$/, [:refreshing])
|
24
368
|
|
25
|
-
parser.state(:output_info, /^Changes to Outputs:$/, [:
|
26
|
-
parser.state(:
|
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:/, [:
|
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(:
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
164
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
657
|
+
log pastel.yellow(line), depth: 1
|
289
658
|
when :none
|
290
659
|
next if line == ""
|
291
660
|
|
292
|
-
|
661
|
+
log_unhandled_line(state, line, reason: "unexpected line in :none state")
|
293
662
|
else
|
294
|
-
|
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
|
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
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
317
|
-
|
318
|
-
|
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
|
-
|
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(
|
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 << "#{
|
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}#{
|
810
|
+
"#{prefix}#{pastel.decorate(middle, *paint_options)}#{suffix}"
|
388
811
|
end
|
389
812
|
end
|
390
813
|
end
|