nexoform 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/nexoform +473 -0
- data/lib/nexoform/config.rb +166 -0
- data/lib/nexoform/shell.rb +42 -0
- data/lib/nexoform/version.rb +11 -0
- data/lib/nexoform.rb +5 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: be5988ba5a83aaf424f2d9f8b0164f738d87daca
|
4
|
+
data.tar.gz: ea0eb52b1ee24f85b886f66dd5d9c2fe073ee7e8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f51b9ec53e05106619663d190710fd741ee43f6fabf8daa45d45ab5c294c88b71231ce3d89bb00d3e8787c9e58b501520c83461e93098bf75b1bbca5fbbfdb7b
|
7
|
+
data.tar.gz: b4b7623970dd7dfc6db14e3f3dc2bad2b5808fbaba6bfc65f00a9dfb1a65eb996a1797b5ff3b9a81697181b9d285d746ddee876a9d05add42eb3019a1a4f07ed
|
data/bin/nexoform
ADDED
@@ -0,0 +1,473 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rainbow/refinement'
|
5
|
+
using Rainbow
|
6
|
+
|
7
|
+
require 'thor'
|
8
|
+
require 'yaml'
|
9
|
+
require 'json'
|
10
|
+
require 'shellwords'
|
11
|
+
|
12
|
+
require 'nexoform'
|
13
|
+
|
14
|
+
# For some reason this method definition can't be inside the NexoformBin class
|
15
|
+
# because Thor won't find it when generating description documention
|
16
|
+
def default_plan_filename
|
17
|
+
'nexoform.tfplan'
|
18
|
+
end
|
19
|
+
|
20
|
+
class NexoformBin < Thor
|
21
|
+
class_option :environment, type: :string, aliases: 'e', required: false
|
22
|
+
class_option :assume_yes, type: :boolean, aliases: 'y', default: false
|
23
|
+
|
24
|
+
desc 'apply', 'Apply changes (Runs a terraform apply)'
|
25
|
+
long_desc <<-LONGDESC
|
26
|
+
Applies any applicable changes. Under the hood, this
|
27
|
+
command runs a terraform apply.
|
28
|
+
|
29
|
+
If you pass --plan, the specified file will be used for the plan
|
30
|
+
If you pass --noplan, no plan file will be used
|
31
|
+
If you don't pass either, no plan file will be used unless the default
|
32
|
+
is present. If it is, you'll be prompted about using it
|
33
|
+
|
34
|
+
> $ nexoform apply --environment 'dev' [--plan=#{default_plan_filename}]
|
35
|
+
LONGDESC
|
36
|
+
option :plan, type: :string, aliases: 'p', required: false
|
37
|
+
option :noplan, type: :boolean, aliases: 'n', default: false
|
38
|
+
option :overwrite, type: :boolean, required: false
|
39
|
+
def apply
|
40
|
+
exec_apply(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'plan', 'Print out changes that will be made on next apply (runs a terraform plan)'
|
44
|
+
long_desc <<-LONGDESC
|
45
|
+
Prints out any changes that will be made the next time
|
46
|
+
a nexoform apply is run. Under the hood, this command
|
47
|
+
runs a terraform plan.
|
48
|
+
|
49
|
+
If you pass an arg to 'out' the plan will be saved to that filename.
|
50
|
+
If you pass '--save' or '-s' the plan will be saved to '#{default_plan_filename}'
|
51
|
+
If you pass '--nosave' or '-n' the plan will not be saved
|
52
|
+
If you pass none of those, you'll be prompted about saving the plan
|
53
|
+
|
54
|
+
> $ nexoform plan --environment 'dev' [--save] [--out='#{default_plan_filename}']
|
55
|
+
LONGDESC
|
56
|
+
option :out, type: :string, aliases: 'o', required: false
|
57
|
+
option :save, type: :boolean, aliases: 's', required: false
|
58
|
+
option :nosave, type: :boolean, aliases: 'n', required: false
|
59
|
+
option :overwrite, type: :boolean, required: false
|
60
|
+
def plan
|
61
|
+
exec_plan(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'destroy', 'Destroy all provisioned resources (runs a terraform destroy)'
|
65
|
+
long_desc <<-LONGDESC
|
66
|
+
Destroys any resources that have been provisioned
|
67
|
+
|
68
|
+
> $ nexoform destroy --environment 'dev'
|
69
|
+
LONGDESC
|
70
|
+
def destroy
|
71
|
+
exec_destroy(options)
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'output', 'Print any output from terraform'
|
75
|
+
long_desc <<-LONGDESC
|
76
|
+
Prints any output from last terraform state. Runs a 'terraform output'
|
77
|
+
|
78
|
+
> $ nexoform output
|
79
|
+
LONGDESC
|
80
|
+
def output
|
81
|
+
execute('terraform output')
|
82
|
+
end
|
83
|
+
|
84
|
+
desc 'version', 'Check current installed version of nexoform'
|
85
|
+
def version
|
86
|
+
puts "Nexoform - Version: #{Nexoform.version}"
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'config-file', 'Write a default config file'
|
90
|
+
long_desc <<-LONGDESC
|
91
|
+
Writes a nexoform config file to #{Nexoform::Config.filename}
|
92
|
+
containing the default settings. This can then be configured
|
93
|
+
as preferred.
|
94
|
+
|
95
|
+
> $ nexoform config-file [--upgrade] [--force] [--project-name 'simplenexus']
|
96
|
+
LONGDESC
|
97
|
+
option :force, type: :boolean, aliases: 'f', default: false
|
98
|
+
option :upgrade, type: :boolean, aliases: 'u', default: false
|
99
|
+
option :'project-name', type: :string, aliases: 'c', required: false
|
100
|
+
def config_file
|
101
|
+
exec_config_file(options)
|
102
|
+
end
|
103
|
+
|
104
|
+
desc 'list-envs', 'List the environments'
|
105
|
+
long_desc <<-LONGDESC
|
106
|
+
Lists the available environments and the current default (if applicable).
|
107
|
+
These are defined in the config file at #{Nexoform::Config.filename}
|
108
|
+
|
109
|
+
> $ nexoform list-envs
|
110
|
+
LONGDESC
|
111
|
+
def list_envs
|
112
|
+
exec_list_envs(options)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def print_next_color(str)
|
118
|
+
print Rainbow(str).color(next_color)
|
119
|
+
end
|
120
|
+
|
121
|
+
def puts_next_color(str)
|
122
|
+
puts Rainbow(str).color(next_color)
|
123
|
+
end
|
124
|
+
|
125
|
+
def config_file_present?
|
126
|
+
unless File.exist?(Nexoform::Config.filename)
|
127
|
+
print 'Could not find nexoform config file. Please create one at '.red
|
128
|
+
puts Nexoform::Config.filename.yellow
|
129
|
+
print_next_color 'You can generate a starter config file with: '
|
130
|
+
puts_next_color '`nexoform config-file`'
|
131
|
+
exit 2
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def terraform_installed?
|
136
|
+
unless Nexoform::Bash.run_command('which terraform').success?
|
137
|
+
puts 'Terraform does not look to be installed, or is not in PATH.'.red
|
138
|
+
print 'To get started, check out: '.yellow
|
139
|
+
puts_next_color 'https://www.terraform.io/intro/getting-started/install.html'
|
140
|
+
exit 3
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def terraform_files_present?
|
145
|
+
if Dir.glob('*.tf').empty?
|
146
|
+
puts "There aren't any terraform files here!".red
|
147
|
+
print 'To get started, check out: '.yellow
|
148
|
+
puts_next_color 'https://www.terraform.io/intro/getting-started/build.html'
|
149
|
+
exit 4
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def plan_file_recent?(options)
|
154
|
+
# TODO: - check to see that the plan file, if it exists, was modified within the last 12 hours
|
155
|
+
# if it was not, warn/exit or something else more appropriate
|
156
|
+
end
|
157
|
+
|
158
|
+
def sanity_check(options)
|
159
|
+
config_file_present?
|
160
|
+
terraform_installed?
|
161
|
+
terraform_files_present?
|
162
|
+
plan_file_recent?(options)
|
163
|
+
end
|
164
|
+
|
165
|
+
def escape(str)
|
166
|
+
Shellwords.escape(str)
|
167
|
+
end
|
168
|
+
|
169
|
+
def env(options)
|
170
|
+
if options['environment']
|
171
|
+
options['environment']
|
172
|
+
elsif Nexoform::Config.default_env
|
173
|
+
Nexoform::Config.default_env
|
174
|
+
else
|
175
|
+
puts 'No environment was specified, and there is no default set.'.red
|
176
|
+
puts 'Please try again with a --environment specified'.yellow
|
177
|
+
exit 1
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def execute(command)
|
182
|
+
puts
|
183
|
+
puts '-----------------------------------------------'.green
|
184
|
+
puts '| Executing command: '.green
|
185
|
+
puts '|'.green
|
186
|
+
puts '| '.green + command.blue
|
187
|
+
puts '-----------------------------------------------'.green
|
188
|
+
puts
|
189
|
+
retval = Nexoform::Bash.run_command_loud(command)
|
190
|
+
unless retval.success?
|
191
|
+
puts '* The previous command exited with failure. Please check the output and try again:'.red
|
192
|
+
print ' Command: '.red
|
193
|
+
puts command.yellow
|
194
|
+
exit retval.exitstatus.to_i
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def terraform_init(options)
|
199
|
+
args = %W[
|
200
|
+
-backend=true
|
201
|
+
-force-copy
|
202
|
+
-get=true
|
203
|
+
-get-plugins=true
|
204
|
+
-reconfigure
|
205
|
+
-upgrade=true
|
206
|
+
-backend-config="bucket=#{escape(Nexoform::Config.bucket(env(options)))}"
|
207
|
+
-backend-config="key=#{escape(Nexoform::Config.key(env(options)))}"
|
208
|
+
-backend-config="region=#{escape(Nexoform::Config.region(env(options)))}"
|
209
|
+
]
|
210
|
+
"terraform init #{args.join(' ')}"
|
211
|
+
end
|
212
|
+
|
213
|
+
def run_init(options)
|
214
|
+
print_next_color '* Initializing terraform for environment: '
|
215
|
+
execute(terraform_init(options))
|
216
|
+
end
|
217
|
+
|
218
|
+
def var_file_arg(options)
|
219
|
+
"-var-file #{escape(Nexoform::Config.var_file(env(options)))}"
|
220
|
+
end
|
221
|
+
|
222
|
+
def terraform_refresh(options)
|
223
|
+
"terraform refresh #{var_file_arg(options)}"
|
224
|
+
end
|
225
|
+
|
226
|
+
def run_refresh(options)
|
227
|
+
print_next_color 'Refreshing terraform state against actual state of ' \
|
228
|
+
'resources for environment: '
|
229
|
+
print_next_color env(options)
|
230
|
+
execute(terraform_refresh(options))
|
231
|
+
end
|
232
|
+
|
233
|
+
def needs_prompt_for_plan_file?(options)
|
234
|
+
(options[:plan].nil? || options[:plan].empty?) && !options[:noplan]
|
235
|
+
end
|
236
|
+
|
237
|
+
def plan_file_in_dir?
|
238
|
+
!Dir.glob('*.tfplan').empty?
|
239
|
+
end
|
240
|
+
|
241
|
+
def prompt_for_plan_file(options)
|
242
|
+
return options if options[:noplan]
|
243
|
+
|
244
|
+
if Nexoform::Config.plan_disabled?(env(options))
|
245
|
+
puts_next_color '* Plan file is disabled in config file. Not using a plan file'
|
246
|
+
return options.merge(noplan: true)
|
247
|
+
end
|
248
|
+
|
249
|
+
if needs_prompt_for_plan_file?(options) && plan_file_in_dir?
|
250
|
+
# use config file if it has the info
|
251
|
+
if Nexoform::Config.has_plan_file?(env(options))
|
252
|
+
plan_file = Nexoform::Config.plan_file(env(options))
|
253
|
+
puts_next_color "* Using #{plan_file} for plan file as specified in " \
|
254
|
+
"config file #{Nexoform::Config.filename} for env #{env(options)}."
|
255
|
+
return options.merge(plan: plan_file)
|
256
|
+
else
|
257
|
+
plan_name = Dir.glob('*.tfplan').first
|
258
|
+
print "You didn't specify a --plan <filename>, but there is a plan " \
|
259
|
+
"file present. Would you like to use the plan '#{plan_name}'? " \
|
260
|
+
"(pass --plan or --noplan if you don't want to see this prompt) (Y/N)?: ".yellow
|
261
|
+
resp = STDIN.gets.chomp
|
262
|
+
if resp =~ /y/i
|
263
|
+
puts_next_color "* Using plan file: #{plan_name}"
|
264
|
+
return options.merge(plan: plan_name)
|
265
|
+
else
|
266
|
+
puts '* Not using a plan file'.yellow
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
options
|
271
|
+
end
|
272
|
+
|
273
|
+
def exit_if_plan_file_should_but_does_not_exist(options)
|
274
|
+
if options[:plan] && !File.exist?(options[:plan])
|
275
|
+
puts "* Was told to use plan file '#{options[:plan]}' but it does not exist!".red
|
276
|
+
puts '* Please run `nexoform plan` and try again, or Re-run with --noplan'.red
|
277
|
+
exit 6
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def apply_with_plan_file?(options)
|
282
|
+
return false if options[:noplan]
|
283
|
+
|
284
|
+
options[:plan]
|
285
|
+
end
|
286
|
+
|
287
|
+
def terraform_apply(options)
|
288
|
+
varfile = apply_with_plan_file?(options) ? '' : var_file_arg(options)
|
289
|
+
planfile = apply_with_plan_file?(options) ? options[:plan] : ''
|
290
|
+
"terraform apply #{varfile} #{planfile}"
|
291
|
+
end
|
292
|
+
|
293
|
+
def exec_apply(options)
|
294
|
+
sanity_check(options)
|
295
|
+
options = prompt_for_plan_file(options)
|
296
|
+
exit_if_plan_file_should_but_does_not_exist(options)
|
297
|
+
unless options[:plan]
|
298
|
+
run_init(options)
|
299
|
+
run_refresh(options)
|
300
|
+
end
|
301
|
+
print_next_color 'Applying any infrastructure changes ' \
|
302
|
+
'for environment: '
|
303
|
+
puts_next_color env(options)
|
304
|
+
execute(terraform_apply(options))
|
305
|
+
end
|
306
|
+
|
307
|
+
def prompt_for_save_file(options)
|
308
|
+
return options if options[:nosave]
|
309
|
+
|
310
|
+
if Nexoform::Config.has_plan_file?(env(options))
|
311
|
+
plan_file = Nexoform::Config.plan_file(env(options))
|
312
|
+
puts_next_color "* Found plan file in #{Nexoform::Config.filename} for env #{env(options)}. Using #{plan_file} for plan file"
|
313
|
+
opts = options.merge(out: plan_file)
|
314
|
+
|
315
|
+
if Nexoform::Config.has_plan_file_overwrite?(env(options))
|
316
|
+
pf_overwrite = Nexoform::Config.plan_file_overwrite(env(options))
|
317
|
+
puts_next_color "* Found overwrite setting for plan file '#{plan_file}' in config file '#{Nexoform::Config.filename}'. Setting to '#{pf_overwrite}'"
|
318
|
+
opts = opts.merge(overwrite: pf_overwrite)
|
319
|
+
else
|
320
|
+
puts "* Didn't see an overwrite setting for plan file '#{plan_file}' in the config file '#{Nexoform::Config.filename}' for env '#{env(options)}'. Skipping".yellow
|
321
|
+
end
|
322
|
+
|
323
|
+
return opts
|
324
|
+
elsif Nexoform::Config.plan_disabled?(env(options))
|
325
|
+
puts_next_color '* Plan file is disabled in config file. Not using a plan file'
|
326
|
+
return options.merge(nosave: true)
|
327
|
+
else
|
328
|
+
if !options[:out] && options[:save]
|
329
|
+
return options.merge(out: default_plan_filename)
|
330
|
+
elsif !options[:out]
|
331
|
+
puts "You didn't specify a --out <filename>. Would you like to save the plan? (pass --nosave if you don't want to see this prompt)\n".yellow
|
332
|
+
puts "1. 'Y' for default filename (#{default_plan_filename}), ".cyan
|
333
|
+
puts "2. 'N' for no saving, ".red
|
334
|
+
puts '3. Type in a filename'.blue
|
335
|
+
print "\nYour choice: ".yellow
|
336
|
+
resp = STDIN.gets.chomp
|
337
|
+
if resp =~ /^y$/ || resp =~ /^1$/
|
338
|
+
puts_next_color "* Using default filename: #{default_plan_filename}"
|
339
|
+
return options.merge(out: default_plan_filename)
|
340
|
+
elsif resp =~ /^n$/i || resp =~ /^2$/i || resp.empty?
|
341
|
+
puts '* Not saving plan'.yellow
|
342
|
+
return options
|
343
|
+
else
|
344
|
+
puts_next_color "* Using filename: #{resp}"
|
345
|
+
return options.merge(out: resp)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
options
|
351
|
+
end
|
352
|
+
|
353
|
+
def prompt_if_plan_file_exists(options)
|
354
|
+
if options[:out] && File.exist?(options[:out])
|
355
|
+
if options[:overwrite].nil? || options[:overwrite] == 'ask'
|
356
|
+
print "The specified plan file '#{options[:out]}' already exists. Over write? (Y/N): ".yellow
|
357
|
+
resp = STDIN.gets.chomp
|
358
|
+
if resp =~ /n/i
|
359
|
+
puts '* User declined writing over plan file. Exiting.'.red
|
360
|
+
exit 5
|
361
|
+
end
|
362
|
+
elsif options[:overwrite] == false
|
363
|
+
puts "* Overwrite is set to false but the plan file '#{options[:out]}' exists. " \
|
364
|
+
'Exitting. Please change the setting or delete/move the plan file before ' \
|
365
|
+
'trying again.'.red
|
366
|
+
exit 5
|
367
|
+
elsif options[:overwrite].is_a?(String) && options[:overwrite] != 'ask'
|
368
|
+
puts "* Plan File Overwrite for env '#{env(options)}' is set to '#{options[:overwrite]}' which is not a valid value. Must be either 'yes', 'no', or 'ask'".red
|
369
|
+
exit 5
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def plan_with_plan_file?(options)
|
375
|
+
return false if options[:noplan]
|
376
|
+
|
377
|
+
options[:plan]
|
378
|
+
end
|
379
|
+
|
380
|
+
def terraform_plan(options)
|
381
|
+
out = options[:out] ? "-out=#{options[:out]}" : ''
|
382
|
+
"terraform plan #{var_file_arg(options)} #{out}"
|
383
|
+
end
|
384
|
+
|
385
|
+
def exec_plan(options)
|
386
|
+
sanity_check(options)
|
387
|
+
options = prompt_for_save_file(options)
|
388
|
+
prompt_if_plan_file_exists(options)
|
389
|
+
run_init(options)
|
390
|
+
run_refresh(options)
|
391
|
+
print_next_color '* Running terraform plan for environment: '
|
392
|
+
puts_next_color env(options)
|
393
|
+
execute(terraform_plan(options))
|
394
|
+
end
|
395
|
+
|
396
|
+
def terraform_destroy(options)
|
397
|
+
"terraform destroy #{var_file_arg(options)}"
|
398
|
+
end
|
399
|
+
|
400
|
+
def exec_destroy(options)
|
401
|
+
sanity_check(options)
|
402
|
+
run_init(options)
|
403
|
+
run_refresh(options)
|
404
|
+
print_next_color '* Destroying any infrastructure resources ' \
|
405
|
+
'for environment: '
|
406
|
+
puts_next_color env(options)
|
407
|
+
execute(terraform_destroy(options))
|
408
|
+
end
|
409
|
+
|
410
|
+
def config_file_action(options)
|
411
|
+
if options[:upgrade]
|
412
|
+
'u'
|
413
|
+
elsif options[:force]
|
414
|
+
'o'
|
415
|
+
else
|
416
|
+
# Upgrade isn't implemented yet. Uncomment when it is
|
417
|
+
# print "A config file already exists at #{Nexoform::Config.filename}. [U]pgrade, [O]verwrite with default settings, or do [N]othing? (U/O/N): ".yellow
|
418
|
+
print "A config file already exists at #{Nexoform::Config.filename}. [O]verwrite with default settings, or do [N]othing? (O/N): ".yellow
|
419
|
+
STDIN.gets.chomp
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def take_action(action, project_name)
|
424
|
+
if action =~ /u/i
|
425
|
+
puts "* Upgrading config file at #{Nexoform::Config.filename}".green
|
426
|
+
Nexoform::Config.upgrade_settings_file
|
427
|
+
elsif action =~ /o/i
|
428
|
+
puts "* Overwriting config file with new version at #{Nexoform::Config.filename}".yellow
|
429
|
+
Nexoform::Config.write_default_settings_file(project_name)
|
430
|
+
else
|
431
|
+
puts '* User declined. Not writing config file'.red
|
432
|
+
nil
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def exec_config_file(options)
|
437
|
+
sanity_check(options)
|
438
|
+
if File.exist?(Nexoform::Config.filename)
|
439
|
+
take_action(config_file_action(options), options[:'project-name'])
|
440
|
+
else
|
441
|
+
puts "* Writing new config file to #{Nexoform::Config.filename}".green
|
442
|
+
Nexoform::Config.write_default_settings_file(options[:'project-name'])
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def next_color
|
447
|
+
# colors = %i[cyan indianred magenta aqua]
|
448
|
+
colors = %i[cyan indianred magenta]
|
449
|
+
@last_num ||= -1
|
450
|
+
@last_num = (@last_num + 1) % colors.length
|
451
|
+
colors[@last_num]
|
452
|
+
end
|
453
|
+
|
454
|
+
def exec_list_envs(options)
|
455
|
+
sanity_check(options)
|
456
|
+
puts_next_color "\nValid environments (defined in your config file): \n"
|
457
|
+
Nexoform::Config.envs.each do |env|
|
458
|
+
puts Rainbow(" - #{env}").color(next_color)
|
459
|
+
end
|
460
|
+
puts
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
if !ARGV.empty? && %w[-v --version].include?(ARGV.first)
|
465
|
+
puts "Nexoform - Version: #{Nexoform.version}"
|
466
|
+
else
|
467
|
+
begin
|
468
|
+
NexoformBin.start(ARGV)
|
469
|
+
rescue StandardError => e
|
470
|
+
puts "* Encountered an error. Make sure your config-file isn't messed up".red
|
471
|
+
puts "\n* Exception message: #{e.message}".yellow
|
472
|
+
end
|
473
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
|
6
|
+
module Nexoform
|
7
|
+
class Config
|
8
|
+
def self.has_config_file?(dir)
|
9
|
+
File.exist?("#{dir}/nexoform.yml")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.find_config_file(starting_dir)
|
13
|
+
if has_config_file?(starting_dir)
|
14
|
+
"#{starting_dir}/nexoform.yml"
|
15
|
+
elsif starting_dir == '/'
|
16
|
+
default_filename
|
17
|
+
else
|
18
|
+
find_config_file(File.dirname(starting_dir)) # recurse up to /
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.default_filename
|
23
|
+
'./nexoform.yml'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.filename
|
27
|
+
find_config_file(Dir.pwd)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.proj_name(project_name)
|
31
|
+
project_name && !project_name.empty? ? project_name : '<companyname>'
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.default_yaml(project_name)
|
35
|
+
%(---
|
36
|
+
nexoform:
|
37
|
+
environments:
|
38
|
+
default: dev # optional default env so you don't have to specify
|
39
|
+
dev: # name of environment
|
40
|
+
varFile: dev.tfvars # terraform var-file to use
|
41
|
+
plan: # optional block. Avoids getting prompted
|
42
|
+
enabled: yes # yes | no. If no, a plan file is not used
|
43
|
+
file: dev.tfplan # file the plan is saved to automatically
|
44
|
+
overwrite: yes # overwrite existing file. could be: yes | no | ask
|
45
|
+
state: # configuration for state management s3 backend
|
46
|
+
bucket: #{project_name}-terraform-state
|
47
|
+
key: dev.tfstate
|
48
|
+
region: us-east-1
|
49
|
+
staging: # name of environment
|
50
|
+
varFile: staging.tfvars # terraform var-file to use
|
51
|
+
plan: # optional block. Avoids getting prompted
|
52
|
+
enabled: yes # yes | no. If no, a plan file is not used
|
53
|
+
file: staging.tfplan # file the plan is saved to automatically
|
54
|
+
overwrite: yes # overwrite existing file. could be: yes | no | ask
|
55
|
+
state: # configuration for state management s3 backend
|
56
|
+
bucket: #{project_name}-terraform-state
|
57
|
+
key: staging.tfstate
|
58
|
+
region: us-east-1
|
59
|
+
prod: # name of environment
|
60
|
+
varFile: prod.tfvars # terraform var-file to use
|
61
|
+
plan: # optional block. Avoids getting prompted
|
62
|
+
enabled: yes # yes | no. If no, a plan file is not used
|
63
|
+
file: prod.tfplan # file the plan is saved to automatically
|
64
|
+
overwrite: yes # overwrite existing file. could be: yes | no | ask
|
65
|
+
state: # configuration for state management s3 backend
|
66
|
+
bucket: #{project_name}-terraform-state
|
67
|
+
key: prod.tfstate
|
68
|
+
region: us-east-1
|
69
|
+
).split("\n").map { |s| s.sub(' ' * 8, '') }.join("\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.default_settings(project_name = nil)
|
73
|
+
YAML.safe_load(default_yaml(proj_name(project_name))).with_indifferent_access
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.settings(filename = self.filename)
|
77
|
+
YAML.load_file(filename).with_indifferent_access if File.exist?(filename)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.write_settings(settings, filename = self.filename, is_yaml: false)
|
81
|
+
settings = settings.to_yaml unless is_yaml
|
82
|
+
settings.gsub!(/\s*!ruby\/hash:ActiveSupport::HashWithIndifferentAccess/, '')
|
83
|
+
File.write(filename, settings)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.write_default_settings_file(project_name = nil)
|
87
|
+
write_settings(default_yaml(proj_name(project_name)), filename, is_yaml: true)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.debug?
|
91
|
+
settings[:nexoform][:debug]
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.envs
|
95
|
+
settings[:nexoform][:environments].keys.reject { |k| k == 'default' }
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.var_file(environment)
|
99
|
+
find_value(%I[nexoform environments #{environment} varFile])
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.default_env
|
103
|
+
settings[:nexoform][:environments][:default]
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.bucket(environment)
|
107
|
+
find_value(%I[nexoform environments #{environment} state bucket])
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.key(environment)
|
111
|
+
find_value(%I[nexoform environments #{environment} state key])
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.region(environment)
|
115
|
+
find_value(%I[nexoform environments #{environment} state region])
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.plan_enabled(environment)
|
119
|
+
find_value(%I[nexoform environments #{environment} plan enabled])
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.plan_disabled?(environment)
|
123
|
+
!plan_enabled(environment)
|
124
|
+
rescue ConfigError => e
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.plan_file(environment)
|
129
|
+
find_value(%I[nexoform environments #{environment} plan file])
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.has_plan_file?(environment)
|
133
|
+
return false if plan_disabled?(environment)
|
134
|
+
|
135
|
+
plan_file(environment)
|
136
|
+
rescue ConfigError => e
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.plan_file_overwrite(environment)
|
141
|
+
find_value(%I[nexoform environments #{environment} plan overwrite])
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.has_plan_file_overwrite?(environment)
|
145
|
+
plan_file_overwrite(environment)
|
146
|
+
true
|
147
|
+
rescue ConfigError => e
|
148
|
+
false
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
class ConfigError < StandardError
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.find_value(keys)
|
157
|
+
keys.reduce(settings) do |last_val, key|
|
158
|
+
if last_val[key].nil?
|
159
|
+
raise ConfigError, "Key '#{key}' in chain '#{keys.join(' -> ')}' produced a " \
|
160
|
+
'nil value. The expected key is missing in your config file.'
|
161
|
+
end
|
162
|
+
last_val[key]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Nexoform
|
6
|
+
module Sh
|
7
|
+
def self.run_command(command, print_stdout = false)
|
8
|
+
stdout = `#{command}`
|
9
|
+
puts stdout if print_stdout
|
10
|
+
OpenStruct.new(
|
11
|
+
success?: $?.exitstatus.zero?,
|
12
|
+
exitstatus: $?.exitstatus,
|
13
|
+
stdout: stdout
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Bash
|
19
|
+
def self.escape_double_quotes(str)
|
20
|
+
str.gsub('"', '\\"')
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.run_command(command, print_stdout = false)
|
24
|
+
stdout = `bash -c "#{escape_double_quotes(command)}"`
|
25
|
+
puts stdout if print_stdout
|
26
|
+
OpenStruct.new(
|
27
|
+
success?: $?.exitstatus.zero?,
|
28
|
+
exitstatus: $?.exitstatus,
|
29
|
+
stdout: stdout
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.run_command_loud(command)
|
34
|
+
exitstatus = system("bash -c \"#{escape_double_quotes(command)}\"")
|
35
|
+
OpenStruct.new(
|
36
|
+
success?: exitstatus,
|
37
|
+
exitstatus: exitstatus ? '0' : '1', # we lose the true exit code
|
38
|
+
stdout: '' # we lose stdout too
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/nexoform.rb
ADDED
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nexoform
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Porter
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rainbow
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: thor
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.20'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.20'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '12.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '12.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.8'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.8'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.59'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.59'
|
111
|
+
description: Nexoform wraps Terraform to provide awareness for multiple environments.
|
112
|
+
Nexoform also provides a more guided experience for using remote backends to track
|
113
|
+
your state. Without nexoform, there are several ways to accidentally lose or corrupt
|
114
|
+
your current state. Nexoform puts up guard rails to prevent accidents, and puts
|
115
|
+
you on firm ground with terraform.
|
116
|
+
email: bporter@simplenexus.com
|
117
|
+
executables:
|
118
|
+
- nexoform
|
119
|
+
extensions: []
|
120
|
+
extra_rdoc_files: []
|
121
|
+
files:
|
122
|
+
- bin/nexoform
|
123
|
+
- lib/nexoform.rb
|
124
|
+
- lib/nexoform/config.rb
|
125
|
+
- lib/nexoform/shell.rb
|
126
|
+
- lib/nexoform/version.rb
|
127
|
+
homepage: https://github.com/SimpleNexus/nexoform
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata: {}
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: 2.3.0
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
requirements: []
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 2.5.2.3
|
148
|
+
signing_key:
|
149
|
+
specification_version: 4
|
150
|
+
summary: Environment aware wrapping for Terraform
|
151
|
+
test_files: []
|