mux_tf 0.2.2 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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/deps.rb +5 -0
- data/lib/mux_tf.rb +28 -25
- data/lib/mux_tf/cli/current.rb +70 -61
- 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 +4 -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
|