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.
- checksums.yaml +4 -4
- data/exe/tf_current +10 -3
- data/exe/tf_mux +10 -3
- data/exe/tf_plan_summary +10 -3
- data/lib/mux_tf.rb +21 -20
- data/lib/mux_tf/cli/current.rb +71 -62
- data/lib/mux_tf/cli/mux.rb +50 -25
- data/lib/mux_tf/cli/plan_summary.rb +20 -246
- data/lib/mux_tf/plan_formatter.rb +36 -36
- data/lib/mux_tf/plan_summary_handler.rb +258 -0
- data/lib/mux_tf/terraform_helpers.rb +28 -28
- data/lib/mux_tf/tmux.rb +18 -11
- data/lib/mux_tf/version.rb +1 -1
- data/lib/mux_tf/version_check.rb +6 -6
- data/lib/mux_tf/yaml_cache.rb +1 -1
- metadata +3 -2
@@ -14,271 +14,45 @@ module MuxTf
|
|
14
14
|
hierarchy: false
|
15
15
|
}
|
16
16
|
|
17
|
-
args = OptionParser.new
|
18
|
-
opts.on(
|
17
|
+
args = OptionParser.new { |opts|
|
18
|
+
opts.on("-i") do |v|
|
19
19
|
options[:interactive] = v
|
20
20
|
end
|
21
|
-
opts.on(
|
21
|
+
opts.on("-h") do |v|
|
22
22
|
options[:hierarchy] = v
|
23
23
|
end
|
24
|
-
|
24
|
+
}.parse!(args)
|
25
25
|
|
26
26
|
if options[:interactive]
|
27
|
-
raise
|
27
|
+
raise "must specify plan file in interactive mode" if args[0].blank?
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
173
|
-
|
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
|
-
|
203
|
-
|
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: /, [
|
31
|
+
parser.state(:plan_error, /^Error: /, %i[refreshing refresh_done])
|
32
32
|
|
33
|
-
status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true)
|
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
|
45
|
+
log "Acquiring state lock ...", depth: 2
|
46
46
|
else
|
47
47
|
p [state, line]
|
48
48
|
end
|
49
49
|
when :error
|
50
|
-
meta[
|
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[
|
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
|
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
|
-
|
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)
|
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
|
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
|
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
|
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
|
149
|
+
log "Initializing provider plugins ...", depth: 1
|
150
150
|
next
|
151
151
|
end
|
152
152
|
case line
|
153
|
-
when /^- Downloading plugin for provider "(?<provider>[
|
153
|
+
when /^- Downloading plugin for provider "(?<provider>[^"]+)" \((?<provider_path>[^)]+)\) (?<version>.+)\.\.\.$/
|
154
154
|
info = $LAST_MATCH_INFO.named_captures
|
155
|
-
log "- #{info[
|
156
|
-
when
|
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
|
-
|
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[
|
182
|
-
log "Encountered #{Paint[info[
|
183
|
-
info[
|
184
|
-
color = dinfo[
|
185
|
-
log "#{Paint[dinfo[
|
186
|
-
if dinfo[
|
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[
|
190
|
-
if dinfo[
|
191
|
-
log format_validation_range(dinfo[
|
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[
|
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[
|
218
|
-
columns = range[
|
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[
|
224
|
+
output << "on: #{range["filename"]} line#{lines.size > 1 ? "s" : ""}: #{lines_info}"
|
225
225
|
|
226
|
-
if File.exist?(range[
|
227
|
-
file_lines = File.read(range[
|
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[
|
240
|
+
output << "#{Paint[">", color]} #{index + 1}: #{painted_line}"
|
241
241
|
else
|
242
242
|
output << " #{index + 1}: #{line}"
|
243
243
|
end
|