mux_tf 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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