mux_tf 0.1.2

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