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.
@@ -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(['force-unlock', '-force', id], need_auth: true))
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(['apply', *args], need_auth: true)
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(['validate', '-json'], need_auth: true)
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 << '-upgrade' unless upgrade.nil?
33
- args << '-no-color' unless color
32
+ args << "-upgrade" unless upgrade.nil?
33
+ args << "-no-color" unless color
34
34
 
35
- cmd = tf_prepare_command(['init', *args], need_auth: true)
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 += ['-out', out]
41
+ args += ["-out", out]
42
42
  args << "-input=#{input.inspect}" unless input.nil?
43
- args << '-compact-warnings' if compact_warnings
44
- args << '-no-color' unless color
45
- args << '-detailed-exitcode' if detailed_exitcode
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(['plan', *args], need_auth: true)
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 = ['show', '-json', file]
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 = ['show', file]
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['MUX_TF_AUTH_WRAPPER'] && need_auth
67
- words = Shellwords.shellsplit(ENV['MUX_TF_AUTH_WRAPPER'])
68
- [*words, 'terraform', *args]
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
- ['terraform', *args]
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
- status: status,
87
- success?: status == 0
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
- status: status.exitstatus,
96
- success?: status.exitstatus == 0
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
- status: result.status,
106
- success?: result.status == 0,
107
- output: result.output,
108
- parsed_output: parsed_output
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'shellwords'
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 do |row|
18
- x = row.split(',')
19
- return { id: x[0], name: x[1] }
20
- end
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 'select-layout tiled'
48
+ tmux "select-layout tiled"
42
49
  end
43
50
 
44
51
  def attach(name, cc: false)
45
- tmux %(#{cc && '-CC' || ''} attach -t #{name.inspect}), raise_on_error: false
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 = '-h'
67
+ mode_part = "-h"
61
68
  when :vertical
62
- mode_part = '-v'
69
+ mode_part = "-v"
63
70
  else
64
71
  raise ArgumentError, "invalid mode: #{mode.inspect}"
65
72
  end
66
73
 
67
74
  parts = [
68
- 'split-window',
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