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 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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nexoform
4
+ def self.version
5
+ '0.0.1'
6
+ end
7
+
8
+ def self.date
9
+ '2018-11-09'
10
+ end
11
+ end
data/lib/nexoform.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem.find_files('nexoform/**/*.rb').each do |path|
4
+ require path.gsub(/\.rb$/, '') unless path =~ /bot.*cli/
5
+ end
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: []