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.
@@ -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,45 @@ 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
- 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 << '-upgrade' unless upgrade.nil?
33
- args << '-no-color' unless color
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(['init', *args], need_auth: true)
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 += ['-out', out]
42
+ args += ["-out", out]
42
43
  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
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(['plan', *args], need_auth: true)
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 = ['show', '-json', file]
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 = ['show', file]
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['MUX_TF_AUTH_WRAPPER'] && need_auth
67
- words = Shellwords.shellsplit(ENV['MUX_TF_AUTH_WRAPPER'])
68
- [*words, 'terraform', *args]
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
- ['terraform', *args]
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
- status: status,
87
- success?: status == 0
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
- status: status.exitstatus,
96
- success?: status.exitstatus == 0
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
- status: result.status,
106
- success?: result.status == 0,
107
- output: result.output,
108
- parsed_output: parsed_output
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
@@ -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,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 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
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
- { id: x[0], index: x[1], name: x[2] }
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 'select-layout tiled'
48
+ tmux "select-layout tiled"
49
49
  end
50
50
 
51
51
  def attach(name, cc: false)
52
- tmux %(#{cc && '-CC' || ''} attach -t #{name.inspect}), raise_on_error: false
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 = '-h'
67
+ mode_part = "-h"
68
68
  when :vertical
69
- mode_part = '-v'
69
+ mode_part = "-v"
70
70
  else
71
71
  raise ArgumentError, "invalid mode: #{mode.inspect}"
72
72
  end
73
73
 
74
74
  parts = [
75
- 'split-window',
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