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.
- 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 +239 -63
- data/lib/mux_tf/cli/plan_summary.rb +2 -1
- data/lib/mux_tf/cli.rb +5 -3
- data/lib/mux_tf/coloring.rb +23 -0
- data/lib/mux_tf/plan_formatter.rb +520 -54
- data/lib/mux_tf/plan_summary_handler.rb +100 -125
- data/lib/mux_tf/plan_utils.rb +360 -0
- data/lib/mux_tf/terraform_helpers.rb +52 -9
- data/lib/mux_tf/tmux.rb +2 -0
- 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,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
|
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
|
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, /(
|
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(:
|
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(:
|
35
|
-
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])
|
36
380
|
|
37
|
-
parser.state(:plan_error,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
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
|
123
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
657
|
+
log pastel.yellow(line), depth: 1
|
248
658
|
when :none
|
249
659
|
next if line == ""
|
250
660
|
|
251
|
-
|
661
|
+
log_unhandled_line(state, line, reason: "unexpected line in :none state")
|
252
662
|
else
|
253
|
-
|
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
|
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
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
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
|
-
|
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(
|
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 << "#{
|
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}#{
|
810
|
+
"#{prefix}#{pastel.decorate(middle, *paint_options)}#{suffix}"
|
345
811
|
end
|
346
812
|
end
|
347
813
|
end
|