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.
- 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
|