mux_tf 0.8.4 → 0.9.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.
- checksums.yaml +4 -4
- data/exe/tf_current +1 -1
- data/exe/tf_mux +1 -1
- data/exe/tf_plan_summary +1 -1
- data/lib/deps.rb +2 -0
- data/lib/mux_tf/cli/current.rb +27 -27
- data/lib/mux_tf/cli/mux.rb +2 -2
- data/lib/mux_tf/cli/plan_summary.rb +7 -13
- data/lib/mux_tf/once_helper.rb +9 -11
- data/lib/mux_tf/plan_filename_generator.rb +12 -0
- data/lib/mux_tf/plan_formatter.rb +44 -41
- data/lib/mux_tf/plan_summary_handler.rb +116 -121
- data/lib/mux_tf/resource_tokenizer.rb +59 -55
- data/lib/mux_tf/terraform_helpers.rb +17 -15
- data/lib/mux_tf/tmux.rb +4 -6
- data/lib/mux_tf/version.rb +1 -1
- data/lib/mux_tf/version_check.rb +4 -2
- data/lib/mux_tf/yaml_cache.rb +41 -36
- data/lib/mux_tf.rb +3 -0
- data/mux_tf.gemspec +2 -1
- metadata +10 -8
@@ -6,8 +6,6 @@ module MuxTf
|
|
6
6
|
include TerraformHelpers
|
7
7
|
include PiotrbCliUtils::Util
|
8
8
|
|
9
|
-
PLAN_FILENAME = "foo.tfplan"
|
10
|
-
|
11
9
|
def self.from_file(file)
|
12
10
|
data = data_from_file(file)
|
13
11
|
new data
|
@@ -20,7 +18,7 @@ module MuxTf
|
|
20
18
|
puts "Analyzing changes ..."
|
21
19
|
result = tf_show(file, json: true)
|
22
20
|
data = result.parsed_output
|
23
|
-
File.
|
21
|
+
File.write("#{file}.json", JSON.dump(data))
|
24
22
|
data
|
25
23
|
end
|
26
24
|
end
|
@@ -29,93 +27,89 @@ module MuxTf
|
|
29
27
|
new(data)
|
30
28
|
end
|
31
29
|
|
32
|
-
def initialize(data)
|
30
|
+
def initialize(data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
33
31
|
@parts = []
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
puts "TODO: update plan_summary to support this!"
|
68
|
-
end
|
33
|
+
data["output_changes"]&.each do |output_name, v|
|
34
|
+
case v["actions"]
|
35
|
+
when ["no-op"]
|
36
|
+
# do nothing
|
37
|
+
when ["create"]
|
38
|
+
parts << {
|
39
|
+
type: "output",
|
40
|
+
action: "create",
|
41
|
+
after_unknown: v["after_unknown"],
|
42
|
+
sensitive: [v["before_sensitive"], v["after_sensitive"]],
|
43
|
+
address: output_name
|
44
|
+
}
|
45
|
+
when ["update"]
|
46
|
+
parts << {
|
47
|
+
type: "output",
|
48
|
+
action: "update",
|
49
|
+
after_unknown: v["after_unknown"],
|
50
|
+
sensitive: [v["before_sensitive"], v["after_sensitive"]],
|
51
|
+
address: output_name
|
52
|
+
}
|
53
|
+
when ["delete"]
|
54
|
+
parts << {
|
55
|
+
type: "output",
|
56
|
+
action: "delete",
|
57
|
+
after_unknown: v["after_unknown"],
|
58
|
+
sensitive: [v["before_sensitive"], v["after_sensitive"]],
|
59
|
+
address: output_name
|
60
|
+
}
|
61
|
+
else
|
62
|
+
puts "[??] #{output_name}"
|
63
|
+
puts "UNKNOWN ACTIONS: #{v['actions'].inspect}"
|
64
|
+
puts "TODO: update plan_summary to support this!"
|
69
65
|
end
|
70
66
|
end
|
71
67
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
puts "TODO: update plan_summary to support this!"
|
118
|
-
end
|
68
|
+
data["resource_changes"]&.each do |v|
|
69
|
+
next unless v["change"]
|
70
|
+
|
71
|
+
case v["change"]["actions"]
|
72
|
+
when ["no-op"]
|
73
|
+
# do nothing
|
74
|
+
when ["create"]
|
75
|
+
parts << {
|
76
|
+
type: "resource",
|
77
|
+
action: "create",
|
78
|
+
address: v["address"],
|
79
|
+
deps: find_deps(data, v["address"])
|
80
|
+
}
|
81
|
+
when ["update"]
|
82
|
+
parts << {
|
83
|
+
type: "resource",
|
84
|
+
action: "update",
|
85
|
+
address: v["address"],
|
86
|
+
deps: find_deps(data, v["address"])
|
87
|
+
}
|
88
|
+
when ["delete"]
|
89
|
+
parts << {
|
90
|
+
type: "resource",
|
91
|
+
action: "delete",
|
92
|
+
address: v["address"],
|
93
|
+
deps: find_deps(data, v["address"])
|
94
|
+
}
|
95
|
+
when %w[delete create]
|
96
|
+
parts << {
|
97
|
+
type: "resource",
|
98
|
+
action: "replace",
|
99
|
+
address: v["address"],
|
100
|
+
deps: find_deps(data, v["address"])
|
101
|
+
}
|
102
|
+
when ["read"]
|
103
|
+
parts << {
|
104
|
+
type: "resource",
|
105
|
+
action: "read",
|
106
|
+
address: v["address"],
|
107
|
+
deps: find_deps(data, v["address"])
|
108
|
+
}
|
109
|
+
else
|
110
|
+
puts "[??] #{v['address']}"
|
111
|
+
puts "UNKNOWN ACTIONS: #{v['change']['actions'].inspect}"
|
112
|
+
puts "TODO: update plan_summary to support this!"
|
119
113
|
end
|
120
114
|
end
|
121
115
|
|
@@ -130,7 +124,7 @@ module MuxTf
|
|
130
124
|
parts.select { |part| part[:type] == "output" }
|
131
125
|
end
|
132
126
|
|
133
|
-
def summary
|
127
|
+
def summary # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
134
128
|
# resources
|
135
129
|
resource_summary = {}
|
136
130
|
resource_parts.each do |part|
|
@@ -157,7 +151,7 @@ module MuxTf
|
|
157
151
|
[
|
158
152
|
"Plan Summary:",
|
159
153
|
resource_pieces.any? ? resource_pieces.join(Paint[", ", :gray]) : nil,
|
160
|
-
output_pieces.any? ? "Outputs: #{output_pieces.join(Paint[
|
154
|
+
output_pieces.any? ? "Outputs: #{output_pieces.join(Paint[', ', :gray])}" : nil
|
161
155
|
].compact.join(" ")
|
162
156
|
else
|
163
157
|
"Plan Summary: no changes"
|
@@ -172,14 +166,14 @@ module MuxTf
|
|
172
166
|
result
|
173
167
|
end
|
174
168
|
|
175
|
-
def sensitive_summary(
|
169
|
+
def sensitive_summary(before_value, after_value)
|
176
170
|
# before vs after
|
177
|
-
if
|
178
|
-
"(#{Paint[
|
179
|
-
elsif
|
180
|
-
"(#{Paint[
|
181
|
-
elsif
|
182
|
-
"(#{Paint[
|
171
|
+
if before_value && after_value
|
172
|
+
"(#{Paint['sensitive', :yellow]})"
|
173
|
+
elsif before_value
|
174
|
+
"(#{Paint['-sensitive', :red]})"
|
175
|
+
elsif after_value
|
176
|
+
"(#{Paint['+sensitive', :cyan]})"
|
183
177
|
end
|
184
178
|
end
|
185
179
|
|
@@ -197,19 +191,19 @@ module MuxTf
|
|
197
191
|
result
|
198
192
|
end
|
199
193
|
|
200
|
-
def nested_summary
|
194
|
+
def nested_summary # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
201
195
|
result = []
|
202
196
|
parts = resource_parts.deep_dup
|
203
197
|
until parts.empty?
|
204
198
|
part = parts.shift
|
205
199
|
if part[:deps] == []
|
206
200
|
indent = if part[:met_deps] && !part[:met_deps].empty?
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
201
|
+
" "
|
202
|
+
else
|
203
|
+
""
|
204
|
+
end
|
211
205
|
message = "[#{format_action(part[:action])}]#{indent} #{format_address(part[:address])}"
|
212
|
-
message += " - (needs: #{part[:met_deps].join(
|
206
|
+
message += " - (needs: #{part[:met_deps].join(', ')})" if part[:met_deps]
|
213
207
|
result << message
|
214
208
|
parts.each do |ipart|
|
215
209
|
d = ipart[:deps].delete(part[:address])
|
@@ -234,11 +228,11 @@ module MuxTf
|
|
234
228
|
end
|
235
229
|
}
|
236
230
|
|
237
|
-
if
|
231
|
+
if result.empty?
|
232
|
+
throw :abort, "nothing selected"
|
233
|
+
else
|
238
234
|
log "Re-running apply with the selected resources ..."
|
239
235
|
run_plan(targets: result)
|
240
|
-
else
|
241
|
-
throw :abort, "nothing selected"
|
242
236
|
end
|
243
237
|
end
|
244
238
|
|
@@ -263,7 +257,8 @@ module MuxTf
|
|
263
257
|
end
|
264
258
|
|
265
259
|
def run_plan(targets: [])
|
266
|
-
|
260
|
+
plan_filename = MuxTf::Cli::Current.plan_filename
|
261
|
+
plan_status, @plan_meta = create_plan(plan_filename, targets: targets)
|
267
262
|
|
268
263
|
case plan_status
|
269
264
|
when :ok
|
@@ -272,7 +267,7 @@ module MuxTf
|
|
272
267
|
log "something went wrong", depth: 1
|
273
268
|
when :changes
|
274
269
|
log "Printing Plan Summary ...", depth: 1
|
275
|
-
pretty_plan_summary(
|
270
|
+
pretty_plan_summary(plan_filename)
|
276
271
|
when :unknown
|
277
272
|
# nothing
|
278
273
|
end
|
@@ -291,7 +286,7 @@ module MuxTf
|
|
291
286
|
log plan.summary, depth: 2
|
292
287
|
end
|
293
288
|
|
294
|
-
def prune_unchanged_deps(
|
289
|
+
def prune_unchanged_deps(_parts)
|
295
290
|
valid_addresses = resource_parts.map { |part| part[:address] }
|
296
291
|
|
297
292
|
resource_parts.each do |part|
|
@@ -299,7 +294,7 @@ module MuxTf
|
|
299
294
|
end
|
300
295
|
end
|
301
296
|
|
302
|
-
def find_deps(data, address)
|
297
|
+
def find_deps(data, address) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
303
298
|
result = []
|
304
299
|
|
305
300
|
m = address.match(/\[(.+)\]$/)
|
@@ -309,8 +304,8 @@ module MuxTf
|
|
309
304
|
end
|
310
305
|
|
311
306
|
if data.dig("prior_state", "values", "root_module", "resources")
|
312
|
-
resource = data["prior_state"]["values"]["root_module"]["resources"].find { |
|
313
|
-
address ==
|
307
|
+
resource = data["prior_state"]["values"]["root_module"]["resources"].find { |inner_resource|
|
308
|
+
address == inner_resource["address"] && index == inner_resource["index"]
|
314
309
|
}
|
315
310
|
end
|
316
311
|
|
@@ -330,19 +325,19 @@ module MuxTf
|
|
330
325
|
|
331
326
|
def find_config(module_root, module_name, address, parent_address)
|
332
327
|
module_info = if parent_address.empty?
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
if m = address.match(/^module\.([^.]+)\./)
|
328
|
+
module_root[module_name]
|
329
|
+
elsif module_root && module_root[module_name]
|
330
|
+
module_root[module_name]["module"]
|
331
|
+
else
|
332
|
+
{}
|
333
|
+
end
|
334
|
+
|
335
|
+
if (m = address.match(/^module\.([^.]+)\./))
|
341
336
|
find_config(module_info["module_calls"], m[1], m.post_match, parent_address + ["module.#{m[1]}"])
|
342
337
|
else
|
343
338
|
if module_info["resources"]
|
344
|
-
resource = module_info["resources"].find { |
|
345
|
-
address ==
|
339
|
+
resource = module_info["resources"].find { |inner_resource|
|
340
|
+
address == inner_resource["address"]
|
346
341
|
}
|
347
342
|
end
|
348
343
|
[resource, parent_address]
|
@@ -357,7 +352,7 @@ module MuxTf
|
|
357
352
|
:yellow
|
358
353
|
when "delete"
|
359
354
|
:red
|
360
|
-
when "replace"
|
355
|
+
when "replace" # rubocop:disable Lint/DuplicateBranch
|
361
356
|
:red
|
362
357
|
when "read"
|
363
358
|
:cyan
|
@@ -395,7 +390,7 @@ module MuxTf
|
|
395
390
|
parts.each_with_index do |(part_type, part_value), index|
|
396
391
|
case part_type
|
397
392
|
when :rt
|
398
|
-
result << "." if index
|
393
|
+
result << "." if index.positive?
|
399
394
|
result << Paint[part_value, :cyan]
|
400
395
|
when :rn
|
401
396
|
result << "."
|
@@ -1,62 +1,66 @@
|
|
1
|
-
|
2
|
-
def self.split(resource)
|
3
|
-
tokenize(resource).map(&:last)
|
4
|
-
end
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
3
|
+
module MuxTf
|
4
|
+
class ResourceTokenizer
|
5
|
+
def self.split(resource)
|
6
|
+
tokenize(resource).map(&:last)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.tokenize(resource) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
10
|
+
result = []
|
11
|
+
n = 0
|
12
|
+
pn = 0
|
13
|
+
state = :rt
|
14
|
+
until n >= resource.length
|
15
|
+
case state
|
16
|
+
when :rt
|
17
|
+
# looking for .
|
18
|
+
if resource[n] == "."
|
19
|
+
# reached the dot ..
|
20
|
+
result << [:rt, resource[pn...n]]
|
21
|
+
pn = n + 1
|
22
|
+
state = :rn
|
23
|
+
end
|
24
|
+
when :rn
|
25
|
+
# looking for [ or .
|
26
|
+
if resource[n] == "."
|
27
|
+
# reached the dot ..
|
28
|
+
result << [:rn, resource[pn...n]]
|
29
|
+
pn = n + 1
|
30
|
+
state = :rt
|
31
|
+
end
|
32
|
+
if resource[n] == "["
|
33
|
+
# reached the open bracket
|
34
|
+
result << [:rn, resource[pn...n]]
|
35
|
+
pn = n
|
36
|
+
state = :ri
|
37
|
+
end
|
38
|
+
if n == resource.length - 1
|
39
|
+
# last character .. close the current group
|
40
|
+
# the last thing should only ever be an index or a name
|
41
|
+
result << [:rn, resource[pn..n]]
|
42
|
+
pn = n
|
43
|
+
state = :done
|
44
|
+
end
|
45
|
+
when :ri
|
46
|
+
# looking for ]
|
47
|
+
if resource[n] == "]"
|
48
|
+
# reached the close bracket
|
49
|
+
result << [:ri, resource[pn..n]]
|
50
|
+
pn = n + 1
|
51
|
+
state = :rt
|
52
|
+
if resource[n + 1] == "."
|
53
|
+
pn = n + 2
|
54
|
+
n += 1
|
55
|
+
end
|
52
56
|
end
|
57
|
+
else
|
58
|
+
warn "unhandled state: #{state.inspect}"
|
53
59
|
end
|
54
|
-
|
55
|
-
|
60
|
+
# p resource[n]
|
61
|
+
n += 1
|
56
62
|
end
|
57
|
-
|
58
|
-
n += 1
|
63
|
+
result
|
59
64
|
end
|
60
|
-
result
|
61
65
|
end
|
62
66
|
end
|
@@ -4,6 +4,8 @@ module MuxTf
|
|
4
4
|
module TerraformHelpers
|
5
5
|
include PiotrbCliUtils::ShellHelpers
|
6
6
|
|
7
|
+
ResultStruct = Struct.new("TerraformResponse", :status, :success?, :output, :parsed_output, keyword_init: true)
|
8
|
+
|
7
9
|
def tf_force_unlock(id:)
|
8
10
|
run_terraform(tf_prepare_command(["force-unlock", "-force", id], need_auth: true))
|
9
11
|
end
|
@@ -88,32 +90,32 @@ module MuxTf
|
|
88
90
|
# return_status: false, echo_command: true, quiet: false, indent: 0
|
89
91
|
def run_terraform(args, **_options)
|
90
92
|
status = run_shell(args, return_status: true, echo_command: true, quiet: false)
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
93
|
+
ResultStruct.new({
|
94
|
+
status: status,
|
95
|
+
success?: status.zero?
|
96
|
+
})
|
95
97
|
end
|
96
98
|
|
97
99
|
def stream_terraform(args, &block)
|
98
100
|
status = run_with_each_line(args, &block)
|
99
101
|
# status is a Process::Status
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
102
|
+
ResultStruct.new({
|
103
|
+
status: status.exitstatus,
|
104
|
+
success?: status.exitstatus.zero?
|
105
|
+
})
|
104
106
|
end
|
105
107
|
|
106
108
|
# error: true, echo_command: true, indent: 0, raise_on_error: false, detailed_result: false
|
107
109
|
def capture_terraform(args, json: nil)
|
108
110
|
result = capture_shell(args, error: true, echo_command: false, raise_on_error: false, detailed_result: true)
|
109
111
|
parsed_output = JSON.parse(result.output) if json
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
rescue JSON::ParserError
|
112
|
+
ResultStruct.new({
|
113
|
+
status: result.status,
|
114
|
+
success?: result.status.zero?,
|
115
|
+
output: result.output,
|
116
|
+
parsed_output: parsed_output
|
117
|
+
})
|
118
|
+
rescue JSON::ParserError
|
117
119
|
fail_with "Execution Failed! - #{result.inspect}"
|
118
120
|
end
|
119
121
|
end
|
data/lib/mux_tf/tmux.rb
CHANGED
@@ -16,7 +16,7 @@ module MuxTf
|
|
16
16
|
def find_pane(name)
|
17
17
|
panes = `tmux list-panes -F "\#{pane_id},\#{pane_title}"`.strip.split("\n").map { |row|
|
18
18
|
x = row.split(",")
|
19
|
-
|
19
|
+
{ id: x[0], name: x[1] }
|
20
20
|
}
|
21
21
|
panes.find { |pane| pane[:name] == name }
|
22
22
|
end
|
@@ -24,7 +24,7 @@ module MuxTf
|
|
24
24
|
def list_windows
|
25
25
|
`tmux list-windows -F "\#{window_id},\#{window_index},\#{window_name}"`.strip.split("\n").map do |row|
|
26
26
|
x = row.split(",")
|
27
|
-
{id: x[0], index: x[1], name: x[2]}
|
27
|
+
{ id: x[0], index: x[1], name: x[2] }
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -49,7 +49,7 @@ module MuxTf
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def attach(name, cc: false)
|
52
|
-
tmux %(#{cc &&
|
52
|
+
tmux %(#{(cc && '-CC') || ''} attach -t #{name.inspect}), raise_on_error: false
|
53
53
|
end
|
54
54
|
|
55
55
|
def kill_pane(pane_id)
|
@@ -93,9 +93,7 @@ module MuxTf
|
|
93
93
|
# puts " => tmux #{cmd}"
|
94
94
|
system("#{tmux_bin} #{cmd}")
|
95
95
|
unless $CHILD_STATUS.success?
|
96
|
-
if raise_on_error
|
97
|
-
fail_with("`tmux #{cmd}' failed with code: #{$CHILD_STATUS.exitstatus}")
|
98
|
-
end
|
96
|
+
fail_with("`tmux #{cmd}' failed with code: #{$CHILD_STATUS.exitstatus}") if raise_on_error
|
99
97
|
|
100
98
|
return false
|
101
99
|
end
|
data/lib/mux_tf/version.rb
CHANGED
data/lib/mux_tf/version_check.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MuxTf
|
2
4
|
module VersionCheck
|
3
|
-
def has_updates?
|
5
|
+
def has_updates? # rubocop:disable Naming/PredicateName
|
4
6
|
current_gem_version < latest_gem_version
|
5
7
|
end
|
6
8
|
|
@@ -23,6 +25,6 @@ module MuxTf
|
|
23
25
|
@cache ||= YamlCache.new(File.expand_path("~/.mux_tf.yaml"), default_ttl: 1.hour)
|
24
26
|
end
|
25
27
|
|
26
|
-
|
28
|
+
module_function :has_updates?, :latest_gem_version, :current_gem_version, :cache
|
27
29
|
end
|
28
30
|
end
|