mux_tf 0.2.1 → 0.3.1

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.
@@ -14,271 +14,45 @@ module MuxTf
14
14
  hierarchy: false
15
15
  }
16
16
 
17
- args = OptionParser.new do |opts|
18
- opts.on('-i') do |v|
17
+ args = OptionParser.new { |opts|
18
+ opts.on("-i") do |v|
19
19
  options[:interactive] = v
20
20
  end
21
- opts.on('-h') do |v|
21
+ opts.on("-h") do |v|
22
22
  options[:hierarchy] = v
23
23
  end
24
- end.parse!(args)
24
+ }.parse!(args)
25
25
 
26
26
  if options[:interactive]
27
- raise 'must specify plan file in interactive mode' if args[0].blank?
27
+ raise "must specify plan file in interactive mode" if args[0].blank?
28
28
  end
29
29
 
30
- data = if args[0]
31
- load_summary_from_file(args[0])
32
- else
33
- JSON.parse(STDIN.read)
34
- end
35
-
36
- parts = []
37
-
38
- data['resource_changes'].each do |v|
39
- next unless v['change']
40
-
41
- case v['change']['actions']
42
- when ['no-op']
43
- # do nothing
44
- when ['create']
45
- parts << {
46
- action: 'create',
47
- address: v['address'],
48
- deps: find_deps(data, v['address'])
49
- }
50
- when ['update']
51
- parts << {
52
- action: 'update',
53
- address: v['address'],
54
- deps: find_deps(data, v['address'])
55
- }
56
- when ['delete']
57
- parts << {
58
- action: 'delete',
59
- address: v['address'],
60
- deps: find_deps(data, v['address'])
61
- }
62
- when %w[delete create]
63
- parts << {
64
- action: 'replace',
65
- address: v['address'],
66
- deps: find_deps(data, v['address'])
67
- }
68
- when ['read']
69
- parts << {
70
- action: 'read',
71
- address: v['address'],
72
- deps: find_deps(data, v['address'])
73
- }
74
- else
75
- puts "[??] #{v['address']}"
76
- puts "UNKNOWN ACTIONS: #{v['change']['actions'].inspect}"
77
- puts 'TODO: update plan_summary to support this!'
78
- end
79
- end
80
-
81
- prune_unchanged_deps(parts)
82
-
83
- if options[:interactive]
84
- run_interactive(parts, args[0])
30
+ plan = if args[0]
31
+ PlanSummaryHandler.from_file(args[0])
85
32
  else
86
- if options[:hierarchy]
87
- print_nested(parts)
88
- else
89
- print_flat(parts)
90
- end
91
- print_summary(parts)
92
- end
93
- end
94
-
95
- def load_summary_from_file(file)
96
- if File.exist?("#{file}.json") && File.mtime("#{file}.json").to_f >= File.mtime(file).to_f
97
- JSON.parse(File.read("#{file}.json"))
98
- else
99
- puts 'Analyzing changes ...'
100
- result = tf_show(file, json: true)
101
- data = result.parsed_output
102
- File.open("#{file}.json", 'w') { |fh| fh.write(JSON.dump(data)) }
103
- data
104
- end
105
- end
106
-
107
- def print_summary(parts)
108
- summary = {}
109
- parts.each do |part|
110
- summary[part[:action]] ||= 0
111
- summary[part[:action]] += 1
112
- end
113
- pieces = summary.map do |k, v|
114
- color = color_for_action(k)
115
- "#{Paint[v, :yellow]} to #{Paint[k, color]}"
33
+ PlanSummaryHandler.from_data(JSON.parse(STDIN.read))
116
34
  end
117
35
 
118
- puts
119
- puts "Plan Summary: #{pieces.join(Paint[', ', :gray])}"
120
- end
121
-
122
- def print_flat(parts)
123
- parts.each do |part|
124
- puts "[#{format_action(part[:action])}] #{format_address(part[:address])}"
125
- end
126
- end
127
-
128
- def run_interactive(parts, _plan_name)
129
- prompt = TTY::Prompt.new
130
- result = prompt.multi_select('Update resources:', per_page: 99, echo: false) do |menu|
131
- parts.each do |part|
132
- label = "[#{format_action(part[:action])}] #{format_address(part[:address])}"
133
- menu.choice label, part[:address]
36
+ if options[:interactive]
37
+ abort_message = catch :abort do
38
+ plan.run_interactive
134
39
  end
135
- end
136
-
137
- if !result.empty?
138
- log 'Re-running apply with the selected resources ...'
139
- status = tf_apply(targets: result)
140
- unless status.success?
141
- log Paint["Failed! (#{status.status})", :red]
142
- exit status.status
40
+ if abort_message
41
+ log Paint["Aborted: #{abort_message}", :red]
143
42
  end
144
43
  else
145
- raise 'nothing selected'
146
- end
147
- end
148
-
149
- def print_nested(parts)
150
- parts = parts.deep_dup
151
- until parts.empty?
152
- part = parts.shift
153
- if part[:deps] == []
154
- indent = if part[:met_deps] && !part[:met_deps].empty?
155
- ' '
156
- else
157
- ''
158
- end
159
- message = "[#{format_action(part[:action])}]#{indent} #{format_address(part[:address])}"
160
- if part[:met_deps]
161
- message += " - (needs: #{part[:met_deps].join(', ')})"
162
- end
163
- puts message
164
- parts.each do |ipart|
165
- d = ipart[:deps].delete(part[:address])
166
- if d
167
- ipart[:met_deps] ||= []
168
- ipart[:met_deps] << d
169
- end
44
+ if options[:hierarchy]
45
+ plan.nested_summary.each do |line|
46
+ puts line
170
47
  end
171
48
  else
172
- parts.unshift part
173
- end
174
- end
175
- end
176
-
177
- def prune_unchanged_deps(parts)
178
- valid_addresses = parts.map { |part| part[:address] }
179
-
180
- parts.each do |part|
181
- part[:deps].select! { |dep| valid_addresses.include?(dep) }
182
- end
183
- end
184
-
185
- def find_config(module_root, module_name, address, parent_address)
186
- module_info = if parent_address.empty?
187
- module_root[module_name]
188
- elsif module_root && module_root[module_name]
189
- module_root[module_name]['module']
190
- else
191
- {}
192
- end
193
-
194
- if m = address.match(/^module\.([^.]+)\./)
195
- find_config(module_info['module_calls'], m[1], m.post_match, parent_address + ["module.#{m[1]}"])
196
- else
197
- if module_info['resources']
198
- resource = module_info['resources'].find do |resource|
199
- address == resource['address']
49
+ plan.flat_summary.each do |line|
50
+ puts line
200
51
  end
201
52
  end
202
- [resource, parent_address]
203
- end
204
- end
205
-
206
- def find_deps(data, address)
207
- result = []
208
-
209
- full_address = address
210
- m = address.match(/\[(.+)\]$/)
211
- if m
212
- address = m.pre_match
213
- index = m[1][0] == '"' ? m[1].gsub(/^"(.+)"$/, '\1') : m[1].to_i
214
- end
215
-
216
- if data['prior_state']['values']['root_module']['resources']
217
- resource = data['prior_state']['values']['root_module']['resources'].find do |resource|
218
- address == resource['address'] && index == resource['index']
219
- end
220
- end
221
-
222
- result += resource['depends_on'] if resource && resource['depends_on']
223
-
224
- resource, parent_address = find_config(data['configuration'], 'root_module', address, [])
225
- if resource
226
- deps = []
227
- resource['expressions'].each do |_k, v|
228
- deps << v['references'] if v.is_a?(Hash) && v['references']
229
- end
230
- result += deps.map { |s| (parent_address + [s]).join('.') }
231
- end
232
-
233
- result
234
- end
235
-
236
- def color_for_action(action)
237
- case action
238
- when 'create'
239
- :green
240
- when 'update'
241
- :yellow
242
- when 'delete'
243
- :red
244
- when 'replace'
245
- :red
246
- when 'read'
247
- :cyan
248
- else
249
- :reset
250
- end
251
- end
252
-
253
- def symbol_for_action(action)
254
- case action
255
- when 'create'
256
- '+'
257
- when 'update'
258
- '~'
259
- when 'delete'
260
- '-'
261
- when 'replace'
262
- '±'
263
- when 'read'
264
- '>'
265
- else
266
- action
267
- end
268
- end
269
-
270
- def format_action(action)
271
- color = color_for_action(action)
272
- symbol = symbol_for_action(action)
273
- Paint[symbol, color]
274
- end
275
-
276
- def format_address(address)
277
- parts = address.split('.')
278
- parts.each_with_index do |part, index|
279
- parts[index] = Paint[part, :cyan] if index.odd?
53
+ puts
54
+ puts plan.summary
280
55
  end
281
- parts.join('.')
282
56
  end
283
57
  end
284
58
  end
@@ -28,9 +28,9 @@ module MuxTf
28
28
  parser.state(:error_lock_info, /Lock Info/, [:error])
29
29
  parser.state(:error, /^$/, [:error_lock_info])
30
30
 
31
- parser.state(:plan_error, /^Error: /, [:refreshing])
31
+ parser.state(:plan_error, /^Error: /, %i[refreshing refresh_done])
32
32
 
33
- status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true) do |raw_line|
33
+ status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true) { |raw_line|
34
34
  plan_output << raw_line
35
35
  parser.parse(raw_line.rstrip) do |state, line|
36
36
  case state
@@ -42,19 +42,19 @@ module MuxTf
42
42
  end
43
43
  when :info
44
44
  if /Acquiring state lock. This may take a few moments.../.match?(line)
45
- log 'Acquiring state lock ...', depth: 2
45
+ log "Acquiring state lock ...", depth: 2
46
46
  else
47
47
  p [state, line]
48
48
  end
49
49
  when :error
50
- meta['error'] = 'lock'
50
+ meta["error"] = "lock"
51
51
  log Paint[line, :red], depth: 2
52
52
  when :plan_error
53
53
  if phase != :plan_error
54
54
  puts
55
55
  phase = :plan_error
56
56
  end
57
- meta['error'] = 'refresh'
57
+ meta["error"] = "refresh"
58
58
  log Paint[line, :red], depth: 2
59
59
  when :error_lock_info
60
60
  if line =~ /^ ([^ ]+):\s+([^ ].+)$/
@@ -64,9 +64,9 @@ module MuxTf
64
64
  when :refreshing
65
65
  if phase != :refreshing
66
66
  phase = :refreshing
67
- log 'Refreshing state ', depth: 2, newline: false
67
+ log "Refreshing state ", depth: 2, newline: false
68
68
  else
69
- print '.'
69
+ print "."
70
70
  end
71
71
  when :refresh_done
72
72
  if phase != :refresh_done
@@ -83,7 +83,7 @@ module MuxTf
83
83
  p [state, line]
84
84
  end
85
85
  end
86
- end
86
+ }
87
87
  [status.status, meta]
88
88
  end
89
89
 
@@ -104,7 +104,7 @@ module MuxTf
104
104
 
105
105
  parser.state(:plugin_warnings, /^$/, [:plugins])
106
106
 
107
- status = tf_init(upgrade: true, color: false) do |raw_line|
107
+ status = tf_init(upgrade: true, color: false) { |raw_line|
108
108
  plan_output << raw_line
109
109
  parser.parse(raw_line.rstrip) do |state, line|
110
110
  case state
@@ -112,19 +112,19 @@ module MuxTf
112
112
  if phase != state
113
113
  # first line
114
114
  phase = state
115
- log 'Upgrding modules ', depth: 1, newline: false
115
+ log "Upgrding modules ", depth: 1, newline: false
116
116
  next
117
117
  end
118
118
  case line
119
119
  when /^- (?<module>[^ ]+) in (?<path>.+)$/
120
120
  # info = $~.named_captures
121
121
  # log "- #{info["module"]}", depth: 2
122
- print '.'
122
+ print "."
123
123
  when /^Downloading (?<repo>[^ ]+) (?<version>[^ ]+) for (?<module>[^ ]+)\.\.\./
124
124
  # info = $~.named_captures
125
125
  # log "Downloading #{info["module"]} from #{info["repo"]} @ #{info["version"]}"
126
- print 'D'
127
- when ''
126
+ print "D"
127
+ when ""
128
128
  puts
129
129
  else
130
130
  p [state, line]
@@ -133,11 +133,11 @@ module MuxTf
133
133
  if phase != state
134
134
  # first line
135
135
  phase = state
136
- log 'Initializing the backend ', depth: 1, newline: false
136
+ log "Initializing the backend ", depth: 1, newline: false
137
137
  next
138
138
  end
139
139
  case line
140
- when ''
140
+ when ""
141
141
  puts
142
142
  else
143
143
  p [state, line]
@@ -146,14 +146,14 @@ module MuxTf
146
146
  if phase != state
147
147
  # first line
148
148
  phase = state
149
- log 'Initializing provider plugins ...', depth: 1
149
+ log "Initializing provider plugins ...", depth: 1
150
150
  next
151
151
  end
152
152
  case line
153
- when /^- Downloading plugin for provider "(?<provider>[^\"]+)" \((?<provider_path>[^\)]+)\) (?<version>.+)\.\.\.$/
153
+ when /^- Downloading plugin for provider "(?<provider>[^"]+)" \((?<provider_path>[^)]+)\) (?<version>.+)\.\.\.$/
154
154
  info = $LAST_MATCH_INFO.named_captures
155
- log "- #{info['provider']} #{info['version']}", depth: 2
156
- when '- Checking for available provider plugins...'
155
+ log "- #{info["provider"]} #{info["version"]}", depth: 2
156
+ when "- Checking for available provider plugins..."
157
157
  # noop
158
158
  else
159
159
  p [state, line]
@@ -170,7 +170,7 @@ module MuxTf
170
170
  p [state, line]
171
171
  end
172
172
  end
173
- end
173
+ }
174
174
 
175
175
  [status.status, meta]
176
176
  end
@@ -178,20 +178,20 @@ module MuxTf
178
178
  def process_validation(info)
179
179
  remedies = Set.new
180
180
 
181
- if info['error_count'] > 0 || info['warning_count'] > 0
182
- log "Encountered #{Paint[info['error_count'], :red]} Errors and #{Paint[info['warning_count'], :yellow]} Warnings!", depth: 2
183
- info['diagnostics'].each do |dinfo|
184
- color = dinfo['severity'] == 'error' ? :red : :yellow
185
- log "#{Paint[dinfo['severity'].capitalize, color]}: #{dinfo['summary']}", depth: 3
186
- if dinfo['detail']&.include?('terraform init')
181
+ if info["error_count"] > 0 || info["warning_count"] > 0
182
+ log "Encountered #{Paint[info["error_count"], :red]} Errors and #{Paint[info["warning_count"], :yellow]} Warnings!", depth: 2
183
+ info["diagnostics"].each do |dinfo|
184
+ color = dinfo["severity"] == "error" ? :red : :yellow
185
+ log "#{Paint[dinfo["severity"].capitalize, color]}: #{dinfo["summary"]}", depth: 3
186
+ if dinfo["detail"]&.include?("terraform init")
187
187
  remedies << :init
188
188
  else
189
- log dinfo['detail'], depth: 4 if dinfo['detail']
190
- if dinfo['range']
191
- log format_validation_range(dinfo['range'], color), depth: 4
189
+ log dinfo["detail"], depth: 4 if dinfo["detail"]
190
+ if dinfo["range"]
191
+ log format_validation_range(dinfo["range"], color), depth: 4
192
192
  end
193
193
 
194
- remedies << :unknown if dinfo['severity'] == 'error'
194
+ remedies << :unknown if dinfo["severity"] == "error"
195
195
  end
196
196
  end
197
197
  end
@@ -214,17 +214,17 @@ module MuxTf
214
214
 
215
215
  context_lines = 3
216
216
 
217
- lines = range['start']['line']..range['end']['line']
218
- columns = range['start']['column']..range['end']['column']
217
+ lines = range["start"]["line"]..range["end"]["line"]
218
+ columns = range["start"]["column"]..range["end"]["column"]
219
219
 
220
220
  # on ../../../modules/pods/jane_pod/main.tf line 151, in module "jane":
221
221
  # 151: jane_resources_preset = var.jane_resources_presetx
222
222
  output = []
223
223
  lines_info = lines.size == 1 ? "#{lines.first}:#{columns.first}" : "#{lines.first}:#{columns.first} to #{lines.last}:#{columns.last}"
224
- output << "on: #{range['filename']} line#{lines.size > 1 ? 's' : ''}: #{lines_info}"
224
+ output << "on: #{range["filename"]} line#{lines.size > 1 ? "s" : ""}: #{lines_info}"
225
225
 
226
- if File.exist?(range['filename'])
227
- file_lines = File.read(range['filename']).split("\n")
226
+ if File.exist?(range["filename"])
227
+ file_lines = File.read(range["filename"]).split("\n")
228
228
  extract_range = ([lines.first - context_lines, 0].max)..([lines.last + context_lines, file_lines.length - 1].min)
229
229
  file_lines.each_with_index do |line, index|
230
230
  if extract_range.cover?(index + 1)
@@ -237,7 +237,7 @@ module MuxTf
237
237
  start_col = columns.last
238
238
  end
239
239
  painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
240
- output << "#{Paint['>', color]} #{index + 1}: #{painted_line}"
240
+ output << "#{Paint[">", color]} #{index + 1}: #{painted_line}"
241
241
  else
242
242
  output << " #{index + 1}: #{line}"
243
243
  end