mux_tf 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +4 -0
- data/exe/tf_current +8 -0
- data/exe/tf_mux +8 -0
- data/exe/tf_plan_summary +8 -0
- data/lib/mux_tf.rb +28 -0
- data/lib/mux_tf/cli.rb +21 -0
- data/lib/mux_tf/cli/current.rb +280 -0
- data/lib/mux_tf/cli/mux.rb +89 -0
- data/lib/mux_tf/cli/plan_summary.rb +286 -0
- data/lib/mux_tf/plan_formatter.rb +260 -0
- data/lib/mux_tf/terraform_helpers.rb +114 -0
- data/lib/mux_tf/tmux.rb +102 -0
- data/lib/mux_tf/version.rb +5 -0
- data/mux_tf.gemspec +40 -0
- metadata +178 -0
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
data/Gemfile
ADDED
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
data/exe/tf_current
ADDED
data/exe/tf_mux
ADDED
data/exe/tf_plan_summary
ADDED
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
|