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
@@ -0,0 +1,258 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MuxTf
|
4
|
+
class PlanSummaryHandler
|
5
|
+
extend TerraformHelpers
|
6
|
+
|
7
|
+
def self.from_file(file)
|
8
|
+
data = data_from_file(file)
|
9
|
+
new data
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.data_from_file(file)
|
13
|
+
if File.exist?("#{file}.json") && File.mtime("#{file}.json").to_f >= File.mtime(file).to_f
|
14
|
+
JSON.parse(File.read("#{file}.json"))
|
15
|
+
else
|
16
|
+
puts "Analyzing changes ..."
|
17
|
+
result = tf_show(file, json: true)
|
18
|
+
data = result.parsed_output
|
19
|
+
File.open("#{file}.json", "w") { |fh| fh.write(JSON.dump(data)) }
|
20
|
+
data
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_data(data)
|
25
|
+
new(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(data)
|
29
|
+
@parts = []
|
30
|
+
|
31
|
+
data["resource_changes"].each do |v|
|
32
|
+
next unless v["change"]
|
33
|
+
|
34
|
+
case v["change"]["actions"]
|
35
|
+
when ["no-op"]
|
36
|
+
# do nothing
|
37
|
+
when ["create"]
|
38
|
+
parts << {
|
39
|
+
action: "create",
|
40
|
+
address: v["address"],
|
41
|
+
deps: find_deps(data, v["address"])
|
42
|
+
}
|
43
|
+
when ["update"]
|
44
|
+
parts << {
|
45
|
+
action: "update",
|
46
|
+
address: v["address"],
|
47
|
+
deps: find_deps(data, v["address"])
|
48
|
+
}
|
49
|
+
when ["delete"]
|
50
|
+
parts << {
|
51
|
+
action: "delete",
|
52
|
+
address: v["address"],
|
53
|
+
deps: find_deps(data, v["address"])
|
54
|
+
}
|
55
|
+
when %w[delete create]
|
56
|
+
parts << {
|
57
|
+
action: "replace",
|
58
|
+
address: v["address"],
|
59
|
+
deps: find_deps(data, v["address"])
|
60
|
+
}
|
61
|
+
when ["read"]
|
62
|
+
parts << {
|
63
|
+
action: "read",
|
64
|
+
address: v["address"],
|
65
|
+
deps: find_deps(data, v["address"])
|
66
|
+
}
|
67
|
+
else
|
68
|
+
puts "[??] #{v["address"]}"
|
69
|
+
puts "UNKNOWN ACTIONS: #{v["change"]["actions"].inspect}"
|
70
|
+
puts "TODO: update plan_summary to support this!"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
prune_unchanged_deps(parts)
|
75
|
+
end
|
76
|
+
|
77
|
+
def summary
|
78
|
+
summary = {}
|
79
|
+
parts.each do |part|
|
80
|
+
summary[part[:action]] ||= 0
|
81
|
+
summary[part[:action]] += 1
|
82
|
+
end
|
83
|
+
pieces = summary.map { |k, v|
|
84
|
+
color = color_for_action(k)
|
85
|
+
"#{Paint[v, :yellow]} to #{Paint[k, color]}"
|
86
|
+
}
|
87
|
+
|
88
|
+
"Plan Summary: #{pieces.join(Paint[", ", :gray])}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def flat_summary
|
92
|
+
result = []
|
93
|
+
parts.each do |part|
|
94
|
+
result << "[#{format_action(part[:action])}] #{format_address(part[:address])}"
|
95
|
+
end
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
def nested_summary
|
100
|
+
result = []
|
101
|
+
parts = parts.deep_dup
|
102
|
+
until parts.empty?
|
103
|
+
part = parts.shift
|
104
|
+
if part[:deps] == []
|
105
|
+
indent = if part[:met_deps] && !part[:met_deps].empty?
|
106
|
+
" "
|
107
|
+
else
|
108
|
+
""
|
109
|
+
end
|
110
|
+
message = "[#{format_action(part[:action])}]#{indent} #{format_address(part[:address])}"
|
111
|
+
message += " - (needs: #{part[:met_deps].join(", ")})" if part[:met_deps]
|
112
|
+
result << message
|
113
|
+
parts.each do |ipart|
|
114
|
+
d = ipart[:deps].delete(part[:address])
|
115
|
+
if d
|
116
|
+
ipart[:met_deps] ||= []
|
117
|
+
ipart[:met_deps] << d
|
118
|
+
end
|
119
|
+
end
|
120
|
+
else
|
121
|
+
parts.unshift part
|
122
|
+
end
|
123
|
+
end
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
def run_interactive
|
128
|
+
prompt = TTY::Prompt.new
|
129
|
+
result = prompt.multi_select("Update resources:", per_page: 99, echo: false) { |menu|
|
130
|
+
parts.each do |part|
|
131
|
+
label = "[#{format_action(part[:action])}] #{format_address(part[:address])}"
|
132
|
+
menu.choice label, part[:address]
|
133
|
+
end
|
134
|
+
}
|
135
|
+
|
136
|
+
if !result.empty?
|
137
|
+
log "Re-running apply with the selected resources ..."
|
138
|
+
status = tf_apply(targets: result)
|
139
|
+
unless status.success?
|
140
|
+
log Paint["Failed! (#{status.status})", :red]
|
141
|
+
throw :abort, "Apply Failed! #{status.status}"
|
142
|
+
end
|
143
|
+
else
|
144
|
+
throw :abort, "nothing selected"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
attr_reader :parts
|
151
|
+
|
152
|
+
def prune_unchanged_deps(parts)
|
153
|
+
valid_addresses = parts.map { |part| part[:address] }
|
154
|
+
|
155
|
+
parts.each do |part|
|
156
|
+
part[:deps].select! { |dep| valid_addresses.include?(dep) }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def find_deps(data, address)
|
161
|
+
result = []
|
162
|
+
|
163
|
+
m = address.match(/\[(.+)\]$/)
|
164
|
+
if m
|
165
|
+
address = m.pre_match
|
166
|
+
index = m[1][0] == '"' ? m[1].gsub(/^"(.+)"$/, '\1') : m[1].to_i
|
167
|
+
end
|
168
|
+
|
169
|
+
if data.dig("prior_state", "values", "root_module", "resources")
|
170
|
+
resource = data["prior_state"]["values"]["root_module"]["resources"].find { |resource|
|
171
|
+
address == resource["address"] && index == resource["index"]
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
result += resource["depends_on"] if resource && resource["depends_on"]
|
176
|
+
|
177
|
+
resource, parent_address = find_config(data["configuration"], "root_module", address, [])
|
178
|
+
if resource
|
179
|
+
deps = []
|
180
|
+
resource["expressions"]&.each do |_k, v|
|
181
|
+
deps << v["references"] if v.is_a?(Hash) && v["references"]
|
182
|
+
end
|
183
|
+
result += deps.map { |s| (parent_address + [s]).join(".") }
|
184
|
+
end
|
185
|
+
|
186
|
+
result
|
187
|
+
end
|
188
|
+
|
189
|
+
def find_config(module_root, module_name, address, parent_address)
|
190
|
+
module_info = if parent_address.empty?
|
191
|
+
module_root[module_name]
|
192
|
+
elsif module_root && module_root[module_name]
|
193
|
+
module_root[module_name]["module"]
|
194
|
+
else
|
195
|
+
{}
|
196
|
+
end
|
197
|
+
|
198
|
+
if m = address.match(/^module\.([^.]+)\./)
|
199
|
+
find_config(module_info["module_calls"], m[1], m.post_match, parent_address + ["module.#{m[1]}"])
|
200
|
+
else
|
201
|
+
if module_info["resources"]
|
202
|
+
resource = module_info["resources"].find { |resource|
|
203
|
+
address == resource["address"]
|
204
|
+
}
|
205
|
+
end
|
206
|
+
[resource, parent_address]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def color_for_action(action)
|
211
|
+
case action
|
212
|
+
when "create"
|
213
|
+
:green
|
214
|
+
when "update"
|
215
|
+
:yellow
|
216
|
+
when "delete"
|
217
|
+
:red
|
218
|
+
when "replace"
|
219
|
+
:red
|
220
|
+
when "read"
|
221
|
+
:cyan
|
222
|
+
else
|
223
|
+
:reset
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def symbol_for_action(action)
|
228
|
+
case action
|
229
|
+
when "create"
|
230
|
+
"+"
|
231
|
+
when "update"
|
232
|
+
"~"
|
233
|
+
when "delete"
|
234
|
+
"-"
|
235
|
+
when "replace"
|
236
|
+
"±"
|
237
|
+
when "read"
|
238
|
+
">"
|
239
|
+
else
|
240
|
+
action
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def format_action(action)
|
245
|
+
color = color_for_action(action)
|
246
|
+
symbol = symbol_for_action(action)
|
247
|
+
Paint[symbol, color]
|
248
|
+
end
|
249
|
+
|
250
|
+
def format_address(address)
|
251
|
+
parts = address.split(".")
|
252
|
+
parts.each_with_index do |part, index|
|
253
|
+
parts[index] = Paint[part, :cyan] if index.odd?
|
254
|
+
end
|
255
|
+
parts.join(".")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -5,7 +5,7 @@ module MuxTf
|
|
5
5
|
include PiotrbCliUtils::ShellHelpers
|
6
6
|
|
7
7
|
def tf_force_unlock(id:)
|
8
|
-
run_terraform(tf_prepare_command([
|
8
|
+
run_terraform(tf_prepare_command(["force-unlock", "-force", id], need_auth: true))
|
9
9
|
end
|
10
10
|
|
11
11
|
def tf_apply(filename: nil, targets: [])
|
@@ -17,44 +17,44 @@ module MuxTf
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
cmd = tf_prepare_command([
|
20
|
+
cmd = tf_prepare_command(["apply", *args], need_auth: true)
|
21
21
|
run_terraform(cmd)
|
22
22
|
end
|
23
23
|
|
24
24
|
def tf_validate
|
25
|
-
cmd = tf_prepare_command([
|
25
|
+
cmd = tf_prepare_command(["validate", "-json"], need_auth: true)
|
26
26
|
capture_terraform(cmd, json: true)
|
27
27
|
end
|
28
28
|
|
29
29
|
def tf_init(input: nil, upgrade: nil, color: true, &block)
|
30
30
|
args = []
|
31
31
|
args << "-input=#{input.inspect}" unless input.nil?
|
32
|
-
args <<
|
33
|
-
args <<
|
32
|
+
args << "-upgrade" unless upgrade.nil?
|
33
|
+
args << "-no-color" unless color
|
34
34
|
|
35
|
-
cmd = tf_prepare_command([
|
35
|
+
cmd = tf_prepare_command(["init", *args], need_auth: true)
|
36
36
|
stream_or_run_terraform(cmd, &block)
|
37
37
|
end
|
38
38
|
|
39
39
|
def tf_plan(out:, color: true, detailed_exitcode: nil, compact_warnings: false, input: nil, &block)
|
40
40
|
args = []
|
41
|
-
args += [
|
41
|
+
args += ["-out", out]
|
42
42
|
args << "-input=#{input.inspect}" unless input.nil?
|
43
|
-
args <<
|
44
|
-
args <<
|
45
|
-
args <<
|
43
|
+
args << "-compact-warnings" if compact_warnings
|
44
|
+
args << "-no-color" unless color
|
45
|
+
args << "-detailed-exitcode" if detailed_exitcode
|
46
46
|
|
47
|
-
cmd = tf_prepare_command([
|
47
|
+
cmd = tf_prepare_command(["plan", *args], need_auth: true)
|
48
48
|
stream_or_run_terraform(cmd, &block)
|
49
49
|
end
|
50
50
|
|
51
51
|
def tf_show(file, json: false)
|
52
52
|
if json
|
53
|
-
args = [
|
53
|
+
args = ["show", "-json", file]
|
54
54
|
cmd = tf_prepare_command(args, need_auth: true)
|
55
55
|
capture_terraform(cmd, json: true)
|
56
56
|
else
|
57
|
-
args = [
|
57
|
+
args = ["show", file]
|
58
58
|
cmd = tf_prepare_command(args, need_auth: true)
|
59
59
|
run_terraform(cmd)
|
60
60
|
end
|
@@ -63,11 +63,11 @@ module MuxTf
|
|
63
63
|
private
|
64
64
|
|
65
65
|
def tf_prepare_command(args, need_auth:)
|
66
|
-
if ENV[
|
67
|
-
words = Shellwords.shellsplit(ENV[
|
68
|
-
[*words,
|
66
|
+
if ENV["MUX_TF_AUTH_WRAPPER"] && need_auth
|
67
|
+
words = Shellwords.shellsplit(ENV["MUX_TF_AUTH_WRAPPER"])
|
68
|
+
[*words, "terraform", *args]
|
69
69
|
else
|
70
|
-
[
|
70
|
+
["terraform", *args]
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
@@ -83,18 +83,18 @@ module MuxTf
|
|
83
83
|
def run_terraform(args, **_options)
|
84
84
|
status = run_shell(args, return_status: true, echo_command: true, quiet: false)
|
85
85
|
OpenStruct.new({
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
status: status,
|
87
|
+
success?: status == 0
|
88
|
+
})
|
89
89
|
end
|
90
90
|
|
91
91
|
def stream_terraform(args, &block)
|
92
92
|
status = run_with_each_line(args, &block)
|
93
93
|
# status is a Process::Status
|
94
94
|
OpenStruct.new({
|
95
|
-
|
96
|
-
|
97
|
-
|
95
|
+
status: status.exitstatus,
|
96
|
+
success?: status.exitstatus == 0
|
97
|
+
})
|
98
98
|
end
|
99
99
|
|
100
100
|
# error: true, echo_command: true, indent: 0, raise_on_error: false, detailed_result: false
|
@@ -102,11 +102,11 @@ module MuxTf
|
|
102
102
|
result = capture_shell(args, error: true, echo_command: false, raise_on_error: false, detailed_result: true)
|
103
103
|
parsed_output = JSON.parse(result.output) if json
|
104
104
|
OpenStruct.new({
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
105
|
+
status: result.status,
|
106
|
+
success?: result.status == 0,
|
107
|
+
output: result.output,
|
108
|
+
parsed_output: parsed_output
|
109
|
+
})
|
110
110
|
rescue JSON::ParserError => e
|
111
111
|
fail_with "Execution Failed! - #{result.inspect}"
|
112
112
|
end
|
data/lib/mux_tf/tmux.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "shellwords"
|
4
4
|
|
5
5
|
module MuxTf
|
6
6
|
module Tmux
|
@@ -14,13 +14,20 @@ module MuxTf
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def find_pane(name)
|
17
|
-
panes = `tmux list-panes -F "\#{pane_id},\#{pane_title}"`.strip.split("\n").map
|
18
|
-
x = row.split(
|
19
|
-
return {
|
20
|
-
|
17
|
+
panes = `tmux list-panes -F "\#{pane_id},\#{pane_title}"`.strip.split("\n").map { |row|
|
18
|
+
x = row.split(",")
|
19
|
+
return {id: x[0], name: x[1]}
|
20
|
+
}
|
21
21
|
panes.find { |pane| pane[:name] == name }
|
22
22
|
end
|
23
23
|
|
24
|
+
def list_windows
|
25
|
+
`tmux list-windows -F "\#{window_id},\#{window_index},\#{window_name}"`.strip.split("\n").map do |row|
|
26
|
+
x = row.split(",")
|
27
|
+
{id: x[0], index: x[1], name: x[2]}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
24
31
|
def new_session(name)
|
25
32
|
tmux %(new-session -s #{name.inspect} -d)
|
26
33
|
end
|
@@ -38,11 +45,11 @@ module MuxTf
|
|
38
45
|
end
|
39
46
|
|
40
47
|
def tile!
|
41
|
-
tmux
|
48
|
+
tmux "select-layout tiled"
|
42
49
|
end
|
43
50
|
|
44
51
|
def attach(name, cc: false)
|
45
|
-
tmux %(#{cc &&
|
52
|
+
tmux %(#{cc && "-CC" || ""} attach -t #{name.inspect}), raise_on_error: false
|
46
53
|
end
|
47
54
|
|
48
55
|
def kill_pane(pane_id)
|
@@ -57,21 +64,21 @@ module MuxTf
|
|
57
64
|
def split_window(mode, target_pane, cwd: nil, cmd: nil)
|
58
65
|
case mode
|
59
66
|
when :horizontal
|
60
|
-
mode_part =
|
67
|
+
mode_part = "-h"
|
61
68
|
when :vertical
|
62
|
-
mode_part =
|
69
|
+
mode_part = "-v"
|
63
70
|
else
|
64
71
|
raise ArgumentError, "invalid mode: #{mode.inspect}"
|
65
72
|
end
|
66
73
|
|
67
74
|
parts = [
|
68
|
-
|
75
|
+
"split-window",
|
69
76
|
cwd && "-c #{cwd}",
|
70
77
|
mode_part,
|
71
78
|
"-t #{target_pane.inspect}",
|
72
79
|
cmd&.inspect
|
73
80
|
].compact
|
74
|
-
tmux parts.join(
|
81
|
+
tmux parts.join(" ")
|
75
82
|
end
|
76
83
|
|
77
84
|
private
|