nexoform 0.0.1
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 +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: []
|