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.
- checksums.yaml +4 -4
- data/lib/mux_tf.rb +21 -20
- data/lib/mux_tf/cli/current.rb +71 -62
- data/lib/mux_tf/cli/mux.rb +26 -24
- 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
|