mux_tf 0.2.2 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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