mux_tf 0.2.4 → 0.4.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 +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 +92 -65
- data/lib/mux_tf/cli/mux.rb +47 -24
- data/lib/mux_tf/cli/plan_summary.rb +20 -246
- data/lib/mux_tf/plan_formatter.rb +100 -52
- data/lib/mux_tf/plan_summary_handler.rb +258 -0
- data/lib/mux_tf/terraform_helpers.rb +30 -29
- data/lib/mux_tf/tmux.rb +13 -13
- 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
- data/mux_tf.gemspec +24 -25
- metadata +4 -7
- data/.gitignore +0 -10
- data/Gemfile +0 -8
- data/LICENSE.txt +0 -21
- data/README.md +0 -52
- data/Rakefile +0 -4
@@ -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,45 @@ 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
|
-
def tf_init(input: nil, upgrade: nil, color: true, &block)
|
29
|
+
def tf_init(input: nil, upgrade: nil, reconfigure: 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 << "-reconfigure" unless reconfigure.nil?
|
34
|
+
args << "-no-color" unless color
|
34
35
|
|
35
|
-
cmd = tf_prepare_command([
|
36
|
+
cmd = tf_prepare_command(["init", *args], need_auth: true)
|
36
37
|
stream_or_run_terraform(cmd, &block)
|
37
38
|
end
|
38
39
|
|
39
40
|
def tf_plan(out:, color: true, detailed_exitcode: nil, compact_warnings: false, input: nil, &block)
|
40
41
|
args = []
|
41
|
-
args += [
|
42
|
+
args += ["-out", out]
|
42
43
|
args << "-input=#{input.inspect}" unless input.nil?
|
43
|
-
args <<
|
44
|
-
args <<
|
45
|
-
args <<
|
44
|
+
args << "-compact-warnings" if compact_warnings
|
45
|
+
args << "-no-color" unless color
|
46
|
+
args << "-detailed-exitcode" if detailed_exitcode
|
46
47
|
|
47
|
-
cmd = tf_prepare_command([
|
48
|
+
cmd = tf_prepare_command(["plan", *args], need_auth: true)
|
48
49
|
stream_or_run_terraform(cmd, &block)
|
49
50
|
end
|
50
51
|
|
51
52
|
def tf_show(file, json: false)
|
52
53
|
if json
|
53
|
-
args = [
|
54
|
+
args = ["show", "-json", file]
|
54
55
|
cmd = tf_prepare_command(args, need_auth: true)
|
55
56
|
capture_terraform(cmd, json: true)
|
56
57
|
else
|
57
|
-
args = [
|
58
|
+
args = ["show", file]
|
58
59
|
cmd = tf_prepare_command(args, need_auth: true)
|
59
60
|
run_terraform(cmd)
|
60
61
|
end
|
@@ -63,11 +64,11 @@ module MuxTf
|
|
63
64
|
private
|
64
65
|
|
65
66
|
def tf_prepare_command(args, need_auth:)
|
66
|
-
if ENV[
|
67
|
-
words = Shellwords.shellsplit(ENV[
|
68
|
-
[*words,
|
67
|
+
if ENV["MUX_TF_AUTH_WRAPPER"] && need_auth
|
68
|
+
words = Shellwords.shellsplit(ENV["MUX_TF_AUTH_WRAPPER"])
|
69
|
+
[*words, "terraform", *args]
|
69
70
|
else
|
70
|
-
[
|
71
|
+
["terraform", *args]
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
@@ -83,18 +84,18 @@ module MuxTf
|
|
83
84
|
def run_terraform(args, **_options)
|
84
85
|
status = run_shell(args, return_status: true, echo_command: true, quiet: false)
|
85
86
|
OpenStruct.new({
|
86
|
-
|
87
|
-
|
88
|
-
|
87
|
+
status: status,
|
88
|
+
success?: status == 0
|
89
|
+
})
|
89
90
|
end
|
90
91
|
|
91
92
|
def stream_terraform(args, &block)
|
92
93
|
status = run_with_each_line(args, &block)
|
93
94
|
# status is a Process::Status
|
94
95
|
OpenStruct.new({
|
95
|
-
|
96
|
-
|
97
|
-
|
96
|
+
status: status.exitstatus,
|
97
|
+
success?: status.exitstatus == 0
|
98
|
+
})
|
98
99
|
end
|
99
100
|
|
100
101
|
# error: true, echo_command: true, indent: 0, raise_on_error: false, detailed_result: false
|
@@ -102,11 +103,11 @@ module MuxTf
|
|
102
103
|
result = capture_shell(args, error: true, echo_command: false, raise_on_error: false, detailed_result: true)
|
103
104
|
parsed_output = JSON.parse(result.output) if json
|
104
105
|
OpenStruct.new({
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
106
|
+
status: result.status,
|
107
|
+
success?: result.status == 0,
|
108
|
+
output: result.output,
|
109
|
+
parsed_output: parsed_output
|
110
|
+
})
|
110
111
|
rescue JSON::ParserError => e
|
111
112
|
fail_with "Execution Failed! - #{result.inspect}"
|
112
113
|
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,17 +14,17 @@ 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
24
|
def list_windows
|
25
25
|
`tmux list-windows -F "\#{window_id},\#{window_index},\#{window_name}"`.strip.split("\n").map do |row|
|
26
|
-
x = row.split(
|
27
|
-
{
|
26
|
+
x = row.split(",")
|
27
|
+
{id: x[0], index: x[1], name: x[2]}
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -45,11 +45,11 @@ module MuxTf
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def tile!
|
48
|
-
tmux
|
48
|
+
tmux "select-layout tiled"
|
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)
|
@@ -64,21 +64,21 @@ module MuxTf
|
|
64
64
|
def split_window(mode, target_pane, cwd: nil, cmd: nil)
|
65
65
|
case mode
|
66
66
|
when :horizontal
|
67
|
-
mode_part =
|
67
|
+
mode_part = "-h"
|
68
68
|
when :vertical
|
69
|
-
mode_part =
|
69
|
+
mode_part = "-v"
|
70
70
|
else
|
71
71
|
raise ArgumentError, "invalid mode: #{mode.inspect}"
|
72
72
|
end
|
73
73
|
|
74
74
|
parts = [
|
75
|
-
|
75
|
+
"split-window",
|
76
76
|
cwd && "-c #{cwd}",
|
77
77
|
mode_part,
|
78
78
|
"-t #{target_pane.inspect}",
|
79
79
|
cmd&.inspect
|
80
80
|
].compact
|
81
|
-
tmux parts.join(
|
81
|
+
tmux parts.join(" ")
|
82
82
|
end
|
83
83
|
|
84
84
|
private
|