mux_tf 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 72d1bbf43fcbb38f0d1a8c4bba72321f582a4ff10ce75a49c87cef89e21647ac
4
+ data.tar.gz: 2be9f70c1ce5a58df894866e02cd8efcfa04aa500ac394218368ef2bb88ee6fc
5
+ SHA512:
6
+ metadata.gz: e64d359f26aafe4d2fd88f00d6294132ee1ed917a5bd02eda45500d55700327e7c70fad0a053b2007db2e350c822509288e1900ad30601571bce097795d70ae4
7
+ data.tar.gz: 1c3b1ac826bace9b8191ce9ae55b392956ddb24e3d89d4aef86080799f408fd545d9efe4254e309a48515a9896e9f757ac20dc7938f42ee12d9f09a3a1a4cc8e
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in mux_tf.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 12.0'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Piotr Banasik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # MuxTf
2
+
3
+ Terraform Module Multiplexer
4
+
5
+ ## Installation
6
+
7
+ ```shell
8
+ gem install mux_tf
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ At the root folder of your terraform modules eg:
14
+
15
+ ```text
16
+ ROOT/
17
+ production/ << == HERE
18
+ group1/
19
+ cluster1/
20
+ main.tf
21
+ cluster2/
22
+ main.tf
23
+ group2/
24
+ cluster3/
25
+ main.tf
26
+ cluster4/
27
+ main.tf
28
+ sandbox/ << == OR HERE
29
+ {SIMILLAR STRUCTURE}
30
+ ```
31
+
32
+ ## Development
33
+
34
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
35
+
36
+ ## Contributing
37
+
38
+ Bug reports and pull requests are welcome on GitHub at https://github.com/piotrb/mux_tf.
39
+
40
+ ## License
41
+
42
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ # task :default => :spec
data/exe/tf_current ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path(File.join(__dir__, '..', 'lib'))
5
+
6
+ require 'mux_tf'
7
+
8
+ MuxTf::Cli.run(:current, ARGV)
data/exe/tf_mux ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path(File.join(__dir__, '..', 'lib'))
5
+
6
+ require 'mux_tf'
7
+
8
+ MuxTf::Cli.run(:mux, ARGV)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path(File.join(__dir__, '..', 'lib'))
5
+
6
+ require 'mux_tf'
7
+
8
+ MuxTf::Cli.run(:plan_summary, ARGV)
data/lib/mux_tf.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ require 'shellwords'
6
+ require 'optparse'
7
+ require 'json'
8
+ require 'open3'
9
+
10
+ require 'piotrb_cli_utils'
11
+ require 'stateful_parser'
12
+
13
+ require 'active_support/core_ext'
14
+
15
+ require 'paint'
16
+ require 'pastel'
17
+ require 'tty-prompt'
18
+ require 'tty-table'
19
+ require 'dotenv'
20
+
21
+ require_relative './mux_tf/version'
22
+ require_relative './mux_tf/cli'
23
+ require_relative './mux_tf/tmux'
24
+ require_relative './mux_tf/terraform_helpers'
25
+ require_relative './mux_tf/plan_formatter'
26
+
27
+ module MuxTf
28
+ end
data/lib/mux_tf/cli.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MuxTf
4
+ module Cli
5
+ def self.run(mode, args)
6
+ case mode
7
+ when :mux
8
+ require_relative "./cli/mux"
9
+ MuxTf::Cli::Mux.run(args)
10
+ when :current
11
+ require_relative "./cli/current"
12
+ MuxTf::Cli::Current.run(args)
13
+ when :plan_summary
14
+ require_relative "./cli/plan_summary"
15
+ MuxTf::Cli::PlanSummary.run(args)
16
+ else
17
+ fail_with "unhandled mode: #{mode.inspect}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MuxTf
4
+ module Cli
5
+ module Current
6
+ extend TerraformHelpers
7
+ extend PiotrbCliUtils::Util
8
+ extend PiotrbCliUtils::CriCommandSupport
9
+ extend PiotrbCliUtils::CmdLoop
10
+
11
+ PLAN_FILENAME = 'foo.tfplan'
12
+
13
+ class << self
14
+ def run(args)
15
+ if args[0] == 'cli'
16
+ cmd_loop
17
+ return
18
+ end
19
+
20
+ folder_name = File.basename(Dir.getwd)
21
+ log "Processing #{Paint[folder_name, :cyan]} ..."
22
+
23
+ ENV['TF_IN_AUTOMATION'] = '1'
24
+ ENV['TF_INPUT'] = '0'
25
+
26
+ return launch_cmd_loop(:error) unless run_validate
27
+
28
+ if ENV['TF_UPGRADE']
29
+ upgrade_status, upgrade_meta = run_upgrade
30
+ return launch_cmd_loop(:error) unless upgrade_status == :ok
31
+ end
32
+
33
+ plan_status, @plan_meta = create_plan(PLAN_FILENAME)
34
+
35
+ case plan_status
36
+ when :ok
37
+ log 'no changes, exiting', depth: 1
38
+ when :error
39
+ log 'something went wrong', depth: 1
40
+ launch_cmd_loop(plan_status)
41
+ when :changes
42
+ log 'Printing Plan Summary ...', depth: 1
43
+ pretty_plan_summary(PLAN_FILENAME)
44
+ launch_cmd_loop(plan_status)
45
+ when :unknown
46
+ launch_cmd_loop(plan_status)
47
+ end
48
+ rescue Exception => e # rubocop:disable Lint/RescueException
49
+ puts Paint['Unhandled Exception!', :red]
50
+ puts '=' * 20
51
+ puts e.full_message
52
+ puts
53
+ puts '< press enter to continue >'
54
+ gets
55
+ exit 1
56
+ end
57
+
58
+ private
59
+
60
+ def run_validate
61
+ remedies = PlanFormatter.process_validation(validate)
62
+ process_remedies(remedies)
63
+ end
64
+
65
+ def process_remedies(remedies)
66
+ if remedies.delete? :init
67
+ log 'Running terraform init ...', depth: 2
68
+ tf_init
69
+ remedies = PlanFormatter.process_validation(validate)
70
+ process_remedies(remedies)
71
+ end
72
+ unless remedies.empty?
73
+ log "unprocessed remedies: #{remedies.to_a}", depth: 1
74
+ return false
75
+ end
76
+ true
77
+ end
78
+
79
+ def validate
80
+ log 'Validating module ...', depth: 1
81
+ tf_validate.parsed_output
82
+ end
83
+
84
+ def create_plan(filename)
85
+ log 'Preparing Plan ...', depth: 1
86
+ exit_code, meta = PlanFormatter.pretty_plan(filename)
87
+ case exit_code
88
+ when 0
89
+ [:ok, meta]
90
+ when 1
91
+ [:error, meta]
92
+ when 2
93
+ [:changes, meta]
94
+ else
95
+ log Paint["terraform plan exited with an unknown exit code: #{exit_code}", :yellow]
96
+ [:unknown, meta]
97
+ end
98
+ end
99
+
100
+ def launch_cmd_loop(status)
101
+ return if ENV['NO_CMD']
102
+
103
+ case status
104
+ when :error, :unknown
105
+ log Paint['Dropping to command line so you can fix the issue!', :red]
106
+ when :changes
107
+ log Paint['Dropping to command line so you can review the changes.', :yellow]
108
+ end
109
+ cmd_loop(status)
110
+ end
111
+
112
+ def cmd_loop(status = nil)
113
+ root_cmd = build_root_cmd
114
+
115
+ folder_name = File.basename(Dir.getwd)
116
+
117
+ puts root_cmd.help
118
+
119
+ prompt = "#{folder_name} => "
120
+ case status
121
+ when :error, :unknown
122
+ prompt = "[#{Paint[status.to_s, :red]}] #{prompt}"
123
+ when :changes
124
+ prompt = "[#{Paint[status.to_s, :yellow]}] #{prompt}"
125
+ end
126
+
127
+ run_cmd_loop(prompt) do |cmd|
128
+ throw(:stop, :no_input) if cmd == ''
129
+ args = Shellwords.split(cmd)
130
+ root_cmd.run(args, {}, hard_exit: false)
131
+ end
132
+ end
133
+
134
+ def build_root_cmd
135
+ root_cmd = define_cmd(nil)
136
+
137
+ root_cmd.add_command(plan_cmd)
138
+ root_cmd.add_command(apply_cmd)
139
+ root_cmd.add_command(shell_cmd)
140
+ root_cmd.add_command(force_unlock_cmd)
141
+ root_cmd.add_command(upgrade_cmd)
142
+ root_cmd.add_command(interactive_cmd)
143
+
144
+ root_cmd.add_command(exit_cmd)
145
+ root_cmd
146
+ end
147
+
148
+ def plan_cmd
149
+ define_cmd('plan', summary: 'Re-run plan') do |_opts, _args, _cmd|
150
+ run_validate && run_plan
151
+ end
152
+ end
153
+
154
+ def apply_cmd
155
+ define_cmd('apply', summary: 'Apply the current plan') do |_opts, _args, _cmd|
156
+ status = tf_apply(filename: PLAN_FILENAME)
157
+ if status.success?
158
+ throw :stop, :done
159
+ else
160
+ log 'Apply Failed!'
161
+ end
162
+ end
163
+ end
164
+
165
+ def shell_cmd
166
+ define_cmd('shell', summary: 'Open your default terminal in the current folder') do |_opts, _args, _cmd|
167
+ log Paint['Launching shell ...', :yellow]
168
+ log Paint['When it exits you will be back at this prompt.', :yellow]
169
+ system ENV['SHELL']
170
+ end
171
+ end
172
+
173
+ def force_unlock_cmd
174
+ define_cmd('force-unlock', summary: 'Force unlock state after encountering a lock error!') do
175
+ prompt = TTY::Prompt.new(interrupt: :noop)
176
+
177
+ table = TTY::Table.new(header: %w[Field Value])
178
+ table << ['Lock ID', @plan_meta['ID']]
179
+ table << ['Operation', @plan_meta['Operation']]
180
+ table << ['Who', @plan_meta['Who']]
181
+ table << ['Created', @plan_meta['Created']]
182
+
183
+ puts table.render(:unicode, padding: [0, 1])
184
+
185
+ if @plan_meta && @plan_meta['error'] == 'lock'
186
+ done = catch(:abort) do
187
+ if @plan_meta['Operation'] != 'OperationTypePlan'
188
+ throw :abort unless prompt.yes?(
189
+ "Are you sure you want to force unlock a lock for operation: #{@plan_meta['Operation']}",
190
+ default: false
191
+ )
192
+ end
193
+
194
+ throw :abort unless prompt.yes?(
195
+ 'Are you sure you want to force unlock this lock?',
196
+ default: false
197
+ )
198
+
199
+ status = tf_force_unlock(id: @plan_meta['ID'])
200
+ if status.success?
201
+ log 'Done!'
202
+ else
203
+ log Paint["Failed with status: #{status}", :red]
204
+ end
205
+
206
+ true
207
+ end
208
+
209
+ log Paint['Aborted', :yellow] unless done
210
+ else
211
+ log Paint['No lock error or no plan ran!', :red]
212
+ end
213
+ end
214
+ end
215
+
216
+ def upgrade_cmd
217
+ define_cmd('upgrade', summary: 'Upgrade modules/plguins') do |_opts, _args, _cmd|
218
+ status, meta = run_upgrade
219
+ if status != :ok
220
+ log meta.inspect unless meta.empty?
221
+ log 'Upgrade Failed!'
222
+ end
223
+ end
224
+ end
225
+
226
+ def interactive_cmd
227
+ define_cmd('interactive', summary: 'Apply interactively') do |_opts, _args, _cmd|
228
+ status = run_shell(['tf-plan-summary', PLAN_FILENAME, '-i'], return_status: true)
229
+ if status != 0
230
+ log 'Interactive Apply Failed!'
231
+ else
232
+ run_plan
233
+ end
234
+ end
235
+ end
236
+
237
+ def run_plan
238
+ plan_status, @plan_meta = create_plan(PLAN_FILENAME)
239
+
240
+ case plan_status
241
+ when :ok
242
+ log 'no changes', depth: 1
243
+ when :error
244
+ log 'something went wrong', depth: 1
245
+ when :changes
246
+ log 'Printing Plan Summary ...', depth: 1
247
+ pretty_plan_summary(PLAN_FILENAME)
248
+ when :unknown
249
+ # nothing
250
+ end
251
+ end
252
+
253
+ def run_upgrade
254
+ exit_code, meta = PlanFormatter.process_upgrade
255
+ case exit_code
256
+ when 0
257
+ [:ok, meta]
258
+ when 1
259
+ [:error, meta]
260
+ # when 2
261
+ # [:changes, meta]
262
+ else
263
+ log Paint["terraform init upgrade exited with an unknown exit code: #{exit_code}", :yellow]
264
+ [:unknown, meta]
265
+ end
266
+ end
267
+
268
+ def tf_plan_summrary_cmd
269
+ @tf_plan_summrary_cmd ||= File.expand_path(File.join(__dir__, '..', '..', '..', 'exe', 'tf_plan_summary'))
270
+ end
271
+
272
+ def pretty_plan_summary(filename)
273
+ run_with_each_line([tf_plan_summrary_cmd, filename]) do |raw_line|
274
+ log raw_line.rstrip, depth: 2
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end