mux_tf 0.15.0 → 0.17.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.
@@ -17,7 +17,7 @@ module MuxTf
17
17
  if File.exist?("#{file}.json") && File.mtime("#{file}.json").to_f >= File.mtime(file).to_f
18
18
  JSON.parse(File.read("#{file}.json"))
19
19
  else
20
- puts "Analyzing changes ..."
20
+ puts "Analyzing changes ... #{file}"
21
21
  result = tf_show(file, json: true)
22
22
  data = result.parsed_output
23
23
  File.write("#{file}.json", JSON.dump(data))
@@ -33,7 +33,7 @@ module MuxTf
33
33
  case action
34
34
  when "create", "add"
35
35
  :green
36
- when "update", "change"
36
+ when "update", "change", "import-update"
37
37
  :yellow
38
38
  when "delete", "remove"
39
39
  :red
@@ -43,7 +43,7 @@ module MuxTf
43
43
  :red
44
44
  when "read"
45
45
  :cyan
46
- when "import" # rubocop:disable Lint/DuplicateBranch
46
+ when "import", "forget" # rubocop:disable Lint/DuplicateBranch
47
47
  :cyan
48
48
  else
49
49
  :reset
@@ -64,6 +64,12 @@ module MuxTf
64
64
  "±"
65
65
  when "read"
66
66
  ">"
67
+ when "import"
68
+ "→"
69
+ when "import-update"
70
+ "↗︎"
71
+ when "forget"
72
+ "↺"
67
73
  else
68
74
  action
69
75
  end
@@ -94,7 +100,8 @@ module MuxTf
94
100
  end
95
101
  end
96
102
 
97
- def initialize(data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
103
+ def initialize(data)
104
+ @data = data
98
105
  @parts = []
99
106
 
100
107
  data["output_changes"]&.each do |output_name, v|
@@ -138,6 +145,14 @@ module MuxTf
138
145
  case v["change"]["actions"]
139
146
  when ["no-op"]
140
147
  # do nothing
148
+ if v["change"]["importing"]
149
+ parts << {
150
+ type: "resource",
151
+ action: "import",
152
+ address: v["address"],
153
+ deps: find_deps(data, v["address"])
154
+ }
155
+ end
141
156
  when ["create"]
142
157
  parts << {
143
158
  type: "resource",
@@ -146,9 +161,10 @@ module MuxTf
146
161
  deps: find_deps(data, v["address"])
147
162
  }
148
163
  when ["update"]
164
+ # ap [v["change"]["actions"], v["change"]["importing"]]
149
165
  parts << {
150
166
  type: "resource",
151
- action: "update",
167
+ action: v["change"]["importing"] ? "import-update" : "update",
152
168
  address: v["address"],
153
169
  deps: find_deps(data, v["address"])
154
170
  }
@@ -180,6 +196,13 @@ module MuxTf
180
196
  address: v["address"],
181
197
  deps: find_deps(data, v["address"])
182
198
  }
199
+ when ["forget"]
200
+ parts << {
201
+ type: "resource",
202
+ action: "forget",
203
+ address: v["address"],
204
+ deps: find_deps(data, v["address"])
205
+ }
183
206
  else
184
207
  puts "[??] #{v['address']}"
185
208
  puts "UNKNOWN RESOURCE ACTIONS: #{v['change']['actions'].inspect}"
@@ -198,7 +221,7 @@ module MuxTf
198
221
  parts.select { |part| part[:type] == "output" }
199
222
  end
200
223
 
201
- def summary # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
224
+ def summary
202
225
  # resources
203
226
  resource_summary = {}
204
227
  resource_parts.each do |part|
@@ -233,11 +256,9 @@ module MuxTf
233
256
  end
234
257
 
235
258
  def flat_summary
236
- result = []
237
- resource_parts.each do |part|
238
- result << "[#{self.class.format_action(part[:action])}] #{self.class.format_address(part[:address])}"
259
+ resource_parts.map do |part|
260
+ "[#{self.class.format_action(part[:action])}] #{self.class.format_address(part[:address])}"
239
261
  end
240
- result
241
262
  end
242
263
 
243
264
  def sensitive_summary(before_value, after_value)
@@ -265,7 +286,20 @@ module MuxTf
265
286
  result
266
287
  end
267
288
 
268
- def nested_summary # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
289
+ def simple_summary(&printer)
290
+ printer = method(:puts) if printer.nil?
291
+
292
+ flat_summary.each do |line|
293
+ printer.call line
294
+ end
295
+ output_summary.each do |line|
296
+ printer.call line
297
+ end
298
+ printer.call ""
299
+ printer.call summary
300
+ end
301
+
302
+ def nested_summary
269
303
  result = []
270
304
  parts = resource_parts.deep_dup
271
305
  until parts.empty?
@@ -305,31 +339,18 @@ module MuxTf
305
339
  if result.empty?
306
340
  throw :abort, "nothing selected"
307
341
  else
308
- log "Re-running apply with the selected resources ..."
309
- MuxTf::Cli::Current.run_plan(targets: result)
342
+ result
310
343
  end
311
344
  end
312
345
 
346
+ def plan_text_output
347
+ PlanUtils.text_version_of_plan_show_from_data(@data)
348
+ end
349
+
313
350
  private
314
351
 
315
352
  attr_reader :parts
316
353
 
317
- def create_plan(filename, targets: [])
318
- log "Preparing Plan ...", depth: 1
319
- exit_code, meta = PlanFormatter.pretty_plan(filename, targets: targets)
320
- case exit_code
321
- when 0
322
- [:ok, meta]
323
- when 1
324
- [:error, meta]
325
- when 2
326
- [:changes, meta]
327
- else
328
- log pastel.yellow("terraform plan exited with an unknown exit code: #{exit_code}")
329
- [:unknown, meta]
330
- end
331
- end
332
-
333
354
  def prune_unchanged_deps(_parts)
334
355
  valid_addresses = resource_parts.map { |part| part[:address] }
335
356
 
@@ -338,7 +359,7 @@ module MuxTf
338
359
  end
339
360
  end
340
361
 
341
- def find_deps(data, address) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
362
+ def find_deps(data, address)
342
363
  result = []
343
364
 
344
365
  m = address.match(/\[(.+)\]$/)
@@ -358,7 +379,7 @@ module MuxTf
358
379
  resource, parent_address = find_config(data["configuration"], "root_module", address, [])
359
380
  if resource
360
381
  deps = []
361
- resource["expressions"]&.each do |_k, v|
382
+ resource["expressions"]&.each_value do |v|
362
383
  deps << v["references"] if v.is_a?(Hash) && v["references"]
363
384
  end
364
385
  result += deps.map { |s| (parent_address + [s]).join(".") }
@@ -12,16 +12,21 @@ module MuxTf
12
12
  class << self
13
13
  def warning(message, binding_arg: binding)
14
14
  stack = binding_arg.send(:caller)
15
- stack_line = stack[0].match(/^(?<path>.+):(?<ln>\d+):in `(?<method>.+)'$/).named_captures
16
- stack_line["path"].gsub!(MuxTf::ROOT, pastel.gray("{mux_tf}"))
17
- msg = [
18
- "#{pastel.orange('WARNING')}: #{message}",
19
- "at #{pastel.cyan(stack_line['path'])}:#{pastel.white(stack_line['ln'])}:in `#{pastel.cyan(stack_line['method'])}'"
20
- ]
15
+ stack_match = stack[0].match(/^(?<path>.+):(?<ln>\d+):in [`'](?<method>.+)'$/)
16
+ msg = []
17
+ if stack_match
18
+ stack_line = stack_match.named_captures
19
+ stack_line["path"].gsub!(MuxTf::ROOT, pastel.gray("{mux_tf}"))
20
+ msg << "#{pastel.orange('WARNING')}: #{message}"
21
+ msg << "at #{pastel.cyan(stack_line['path'])}:#{pastel.white(stack_line['ln'])}:in `#{pastel.cyan(stack_line['method'])}'"
22
+ else
23
+ p [stack_match, stack[0]]
24
+ msg << "#{pastel.orange('WARNING')}: #{message}"
25
+ end
21
26
  puts msg.join(" - ")
22
27
  end
23
28
 
24
- def update_placeholders(dst, src, placeholder) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
29
+ def update_placeholders(dst, src, placeholder)
25
30
  return unless src
26
31
 
27
32
  case src
@@ -104,8 +109,11 @@ module MuxTf
104
109
  else
105
110
  false
106
111
  end
107
- rescue Psych::SyntaxError => e
108
- ap e
112
+ rescue Psych::DisallowedClass => _e
113
+ # ap e
114
+ false
115
+ rescue Psych::SyntaxError => _e # rubocop:disable Lint/DuplicateBranch
116
+ # ap e
109
117
  false
110
118
  end
111
119
 
@@ -115,6 +123,16 @@ module MuxTf
115
123
  pastel.green(symbol)
116
124
  when "~"
117
125
  pastel.yellow(symbol)
126
+ when "-"
127
+ pastel.red(symbol)
128
+ when "?"
129
+ pastel.orange(symbol)
130
+ when "±"
131
+ pastel.bright_red(symbol)
132
+ when ">"
133
+ pastel.blue(symbol)
134
+ when " "
135
+ symbol
118
136
  else
119
137
  warning "Unknown symbol: #{symbol.inspect}"
120
138
  symbol
@@ -143,7 +161,7 @@ module MuxTf
143
161
  }.join("\n")
144
162
  end
145
163
 
146
- def in_display_representation(value) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength
164
+ def in_display_representation(value)
147
165
  if valid_json?(value)
148
166
  json_body = JSON.pretty_generate(JSON.parse(value))
149
167
  wrap(json_body, prefix: "json(", suffix: ")", color: :gray)
@@ -223,40 +241,26 @@ module MuxTf
223
241
  format_value_diff(mode, value_arg)
224
242
  end
225
243
 
226
- # def format_value(value_arg, symbol)
227
- # case value_arg
228
- # when Array
229
- # mode = :both
230
- # case symbol
231
- # when "+"
232
- # mode = :right
233
- # when "~"
234
- # mode = :both
235
- # else
236
- # warning "Unknown symbol: #{symbol.inspect}"
237
- # end
238
-
239
- # format_value_diff(mode, value_arg)
240
- # when Hash
241
- # if value_arg.keys.all? { |k| k.is_a?(Integer) }
242
- # # assuming its a hash notation of array keys changes
243
- # value_arg.keys.sort.map { |k| "[#{k}] #{format_value(value_arg[k], symbol)[0]}" }
244
- # else
245
- # [value_arg.inspect]
246
- # end
247
- # else
248
- # [value_arg.inspect]
249
- # end
250
- # end
251
-
252
244
  def get_pretty_action_and_symbol(actions)
253
245
  case actions
246
+ when ["delete"]
247
+ pretty_action = "delete"
248
+ symbol = "-"
254
249
  when ["update"]
255
250
  pretty_action = "updated in-place"
256
251
  symbol = "~"
257
252
  when ["create"]
258
253
  pretty_action = "created"
259
254
  symbol = "+"
255
+ when %w[delete create]
256
+ pretty_action = "replaced (delete first)"
257
+ symbol = "±"
258
+ when ["read"]
259
+ pretty_action = "read"
260
+ symbol = ">"
261
+ when ["no-op"]
262
+ pretty_action = "no-op"
263
+ symbol = " "
260
264
  else
261
265
  warning "Unknown action: #{actions.inspect}"
262
266
  pretty_action = actions.inspect
@@ -286,7 +290,7 @@ module MuxTf
286
290
  # EOT
287
291
  # # (12 unchanged attributes hidden)
288
292
  # }
289
- def tf_show_json_resource(resource) # rubocop:disable Metrics/AbcSize
293
+ def tf_show_json_resource(resource)
290
294
  pretty_action, symbol = get_pretty_action_and_symbol(resource["change"]["actions"])
291
295
 
292
296
  output = []
@@ -295,44 +299,188 @@ module MuxTf
295
299
 
296
300
  output << ""
297
301
  output << "#{global_indent}#{pastel.bold("# #{resource['address']}")} will be #{pretty_action}"
302
+ output << "#{global_indent} >> #{action_reason_to_text(resource['action_reason'])}" if resource["action_reason"]
298
303
  output << "#{global_indent}#{colorize_symbol(symbol)} resource \"#{resource['type']}\" \"#{resource['name']}\" {"
299
304
  diff = tf_show_json_resource_diff(resource)
300
305
  max_diff_key_length = diff.map { |change| change[1].length }.max
306
+
307
+ fields_which_caused_replacement = []
308
+
309
+ if resource["change"]["replace_paths"]
310
+ if resource["change"]["replace_paths"].length != 1
311
+ warning "Multiple replace paths found for resource #{resource['address']}: #{resource['change']['replace_paths'].inspect}"
312
+ elsif resource["change"]["replace_paths"][0].length != 1
313
+ warning "Multiple fields found to be replaced for resource #{resource['address']}: #{resource['change']['replace_paths'].inspect}"
314
+ else
315
+ fields_which_caused_replacement << resource["change"]["replace_paths"][0][0]
316
+ end
317
+ end
318
+
301
319
  diff.each do |change|
302
320
  change_symbol, key, *_values = change
303
321
  prefix = format("#{global_indent} #{colorize_symbol(change_symbol)} %s = ", key.ljust(max_diff_key_length))
322
+ suffix = ""
323
+ suffix = " (forces replacement)" if fields_which_caused_replacement.include?(key)
304
324
  blank_prefix = " " * pastel.strip(prefix).length
305
325
  format_value(change).each_with_index do |line, index|
306
326
  output << if index.zero?
307
- "#{prefix}#{line}"
327
+ "#{prefix}#{line}#{suffix}"
308
328
  else
309
- "#{blank_prefix}#{line}"
329
+ "#{blank_prefix}#{line}#{suffix}"
310
330
  end
311
331
  end
312
332
  end
313
- # max_diff_key_length = diff.keys.map(&:length).max
314
- # diff.each do |key, value|
315
- # prefix = format("#{global_indent} #{colorize_symbol(symbol)} %s = ", key.ljust(max_diff_key_length))
316
- # blank_prefix = " " * pastel.strip(prefix).length
317
- # format_value(value, symbol).each_with_index do |line, index|
318
- # output << if index.zero?
319
- # "#{prefix}#{line}"
320
- # else
321
- # "#{blank_prefix}#{line}"
322
- # end
323
- # end
324
- # end
325
333
  output << "#{global_indent}}"
326
334
 
327
335
  output.join("\n")
328
336
  end
329
337
 
330
- def text_version_of_plan_show(plan_filename)
331
- result = tf_show(plan_filename, capture: true, json: true)
332
- data = result.parsed_output
338
+ def format_output_value_diff(mode, value_arg)
339
+ case mode
340
+ when :both
341
+ vleft = in_display_representation(value_arg[0])
342
+ vright = in_display_representation(value_arg[1])
343
+ if [vleft, vright].any? { |v| v.is_a?(String) && v.include?("\n") }
344
+ if pastel.strip(vright) == KNOWN_AFTER_APPLY
345
+ "#{vleft} -> #{vright}".split("\n")
346
+ else
347
+ string_diff(pastel.strip(vleft), pastel.strip(vright))
348
+ end
349
+ else
350
+ "#{vleft} -> #{vright}".split("\n")
351
+ end
352
+ when :right
353
+ vright = in_display_representation(value_arg[1])
354
+ vright.split("\n")
355
+ when :left, :first
356
+ vleft = in_display_representation(value_arg[0])
357
+ vleft.split("\n")
358
+ end
359
+ end
360
+
361
+ def get_x_type(key, change)
362
+ return :hash if change[key].is_a?(Hash) || change["#{key}_sensitive"].is_a?(Hash) || change["#{key}_unknown"].is_a?(Hash)
363
+ return :array if change[key].is_a?(Array) || change["#{key}_sensitive"].is_a?(Array) || change["#{key}_unknown"].is_a?(Array)
364
+
365
+ :scalar
366
+ end
367
+
368
+ def get_value_type(change)
369
+ case change["actions"]
370
+ when ["create"]
371
+ get_x_type("after", change)
372
+ when ["delete"]
373
+ get_x_type("before", change)
374
+ when ["update"]
375
+ bt = get_x_type("before", change)
376
+ at = get_x_type("after", change)
377
+ raise "Type mismatch before vs after: #{bt} != #{at}" if bt != at
378
+
379
+ at
380
+ when ["no-op"]
381
+ :none
382
+ else
383
+ raise "Unknown action: #{change['actions'].inspect}"
384
+ end
385
+ end
386
+
387
+ def prep_before_after(change)
388
+ before = change["before"]
389
+ after = change["after"]
390
+ before = KNOWN_AFTER_APPLY if change["before_unknown"]
391
+ before = SENSITIVE if change["before_sensitive"]
392
+ after = KNOWN_AFTER_APPLY if change["after_unknown"]
393
+ after = SENSITIVE if change["after_sensitive"]
394
+
395
+ [before, after]
396
+ end
397
+
398
+ def tf_show_json_output(output_key, output_change, max_outer_key_length)
399
+ _pretty_action, symbol = get_pretty_action_and_symbol(output_change["actions"])
400
+
401
+ output = []
402
+
403
+ global_indent = " " * 2
404
+
405
+ value_type = get_value_type(output_change)
406
+
407
+ start_prefix = format("#{global_indent}#{colorize_symbol(symbol)} %s = ", output_key.ljust(max_outer_key_length))
408
+ blank_start_prefix = " " * pastel.strip(start_prefix).length
409
+ outer_mode = :both
410
+ outer_mode = :right if ["+"].include?(symbol)
411
+ outer_mode = :left if ["-"].include?(symbol)
412
+ outer_mode = :right if [" "].include?(symbol)
413
+
414
+ case value_type
415
+ when :hash
416
+ output << "#{start_prefix}{"
417
+
418
+ fake_resource = {
419
+ "change" => output_change
420
+ }
421
+ diff = tf_show_json_resource_diff(fake_resource)
422
+ max_diff_key_length = diff.map { |change| change[1].length }.max
423
+
424
+ diff.each do |change|
425
+ change_symbol, key, *_values = change
426
+ prefix = format("#{global_indent} #{colorize_symbol(change_symbol)} %s = ", key.ljust(max_diff_key_length))
427
+ suffix = ""
428
+ blank_prefix = " " * pastel.strip(prefix).length
429
+ format_value(change).each_with_index do |line, index|
430
+ output << if index.zero?
431
+ "#{prefix}#{line}#{suffix}"
432
+ else
433
+ "#{blank_prefix}#{line}#{suffix}"
434
+ end
435
+ end
436
+ end
437
+
438
+ output << "#{global_indent} }"
439
+ when :array, :scalar, :none
440
+ before, after = prep_before_after(output_change)
441
+ format_value_diff(outer_mode, [before, after]).each_with_index do |line, index|
442
+ output << if index.zero?
443
+ "#{start_prefix}#{line}"
444
+ else
445
+ "#{blank_start_prefix}#{line}"
446
+ end
447
+ end
448
+ else
449
+ raise "Unknown value type: #{value_type.inspect}"
450
+ end
451
+
452
+ output.join("\n")
453
+ end
333
454
 
334
- # Plan: 0 to add, 1 to change, 0 to destroy.
455
+ # See https://developer.hashicorp.com/terraform/internals/json-format
456
+ def action_reason_to_text(action_reason)
457
+ case action_reason
458
+ when "replace_because_tainted"
459
+ "because the object is tainted in the prior state"
460
+ when "replace_because_cannot_update"
461
+ "because the provider does not support updating the object, or the changed attributes"
462
+ when "replace_by_request"
463
+ "because the user requested the object to be replaced"
464
+ when "delete_because_no_resource_config"
465
+ "because the object is no longer in the configuration"
466
+ when "delete_because_no_module"
467
+ "because the module this object belongs to is no longer in the configuration"
468
+ when "delete_because_wrong_repetition"
469
+ "because of the repetition mode has changed"
470
+ when "delete_because_count_index"
471
+ "because the repetition count has changed"
472
+ when "delete_because_each_key"
473
+ "because the repetition key has changed"
474
+ when "read_because_config_unknown"
475
+ "because reading this will only be possible during apply"
476
+ when "read_because_dependency_pending"
477
+ "because reading this will only be possible after a dependency is available"
478
+ else
479
+ "because #{action_reason}"
480
+ end
481
+ end
335
482
 
483
+ def text_version_of_plan_show_from_data(data)
336
484
  output = []
337
485
 
338
486
  output << "Terraform will perform the following actions:"
@@ -349,7 +497,18 @@ module MuxTf
349
497
  output << ""
350
498
  output << "Resource Changes:"
351
499
  data["resource_changes"].each do |resource|
352
- output << tf_show_json_resource(resource) if resource["change"]["actions"] != ["no-op"]
500
+ next if resource["change"]["actions"] == ["no-op"]
501
+
502
+ output << tf_show_json_resource(resource)
503
+ end
504
+ end
505
+
506
+ if data["output_changes"]
507
+ output << ""
508
+ output << "Changes to Outputs:"
509
+ max_outer_key_length = data["output_changes"].keys.map(&:length).max || 0
510
+ data["output_changes"].each do |key, output_change|
511
+ output << tf_show_json_output(key, output_change, max_outer_key_length)
353
512
  end
354
513
  end
355
514
 
@@ -6,7 +6,7 @@ module MuxTf
6
6
  tokenize(resource).map(&:last)
7
7
  end
8
8
 
9
- def self.tokenize(resource) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
9
+ def self.tokenize(resource)
10
10
  result = []
11
11
  n = 0
12
12
  pn = 0