awx 0.2.0 → 0.3.0

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.
@@ -12,13 +12,15 @@ module App
12
12
  KEY_REGIONS = 'regions'
13
13
  KEY_REGIONS_PREFERRED = 'regionsPreferred'
14
14
 
15
- YML_FILE = 'aws-reports.yml'
15
+ YML_FILE = 'reports.yml'
16
16
 
17
17
  # Parses the YML file and returns a bunch of Hashes. Throws straight-up runtime errors if something is wrong.
18
18
  # @return void
19
19
  def self.parse_metadata(regions)
20
20
 
21
- yml_file = "#{App::Opt::get_base_path}#{App::Opt::OPT_PATH_YML}/#{YML_FILE}"
21
+ yml_file = "#{App::Opt::get_base_path}#{App::Opt::OPT_PATH}/awx/#{YML_FILE}"
22
+
23
+ # Throw error if the aws-report.yml isn't found in the /opt folder.
22
24
  Blufin::Terminal::error("File not found: #{Blufin::Terminal::format_directory(yml_file)}", 'This file should be located in the /opt folder of the my (ruby-gem) source-code.', true) unless Blufin::Files::file_exists(yml_file)
23
25
 
24
26
  columns = {}
@@ -101,15 +103,17 @@ module App
101
103
  end
102
104
  end
103
105
  # Validate Exports.
104
- if !data[KEY_EXPORT].nil? && data[KEY_EXPORT].any?
105
- raise RuntimeError, "Expected #{Blufin::Terminal::format_highlight('export')} to be a Hash, instead got: #{data[KEY_EXPORT].class}" unless data[KEY_EXPORT].is_a?(Hash)
106
- raise RuntimeError, "#{resource}.#{KEY_EXPORT} \xe2\x86\x92 Missing: id" unless data[KEY_EXPORT].has_key?('id')
107
- raise RuntimeError, "#{resource}.#{KEY_EXPORT} \xe2\x86\x92 Missing: value" unless data[KEY_EXPORT].has_key?('value')
108
- raise RuntimeError, "#{resource}.#{KEY_EXPORT} \xe2\x86\x92 Missing: description" unless data[KEY_EXPORT].has_key?('description')
109
- # Make sure formatter exists.
110
- App::AWSOutputter::get_formatter(data[KEY_EXPORT]['valueFormatter']) if data[KEY_EXPORT].has_key?('valueFormatter')
111
- data[KEY_EXPORT].each { |key, val| raise RuntimeError, "Unexpected key: #{key} (#{val})" unless %w(id value valueFormatter description).include?(key) }
112
- export_map[data[KEY_EXPORT]['id']] = resource
106
+ unless data[KEY_EXPORT].nil?
107
+ raise RuntimeError, "Expected #{resource}.export to be an Array, instead got: #{data[KEY_EXPORT].class}" unless data[KEY_EXPORT].is_a?(Array)
108
+ raise RuntimeError, "Expected #{resource}.export to have at least 1 entry." unless data[KEY_EXPORT].any?
109
+ data[KEY_EXPORT].each do |export|
110
+ raise RuntimeError, "#{resource}.#{KEY_EXPORT} \xe2\x86\x92 Missing: id" unless export.has_key?('id')
111
+ raise RuntimeError, "#{resource}.#{KEY_EXPORT} \xe2\x86\x92 Missing: value" unless export.has_key?('value')
112
+ # Make sure formatter exists.
113
+ App::AWSOutputter::get_formatter(export['valueFormatter']) if export.has_key?('valueFormatter')
114
+ export.each { |key, val| raise RuntimeError, "Unexpected key: #{key} (#{val})" unless %w(id value valueFormatter description).include?(key) }
115
+ export_map[export['id']] = resource
116
+ end
113
117
  end
114
118
  end
115
119
 
@@ -131,8 +135,8 @@ module App
131
135
  response[region] = {} if response[region].nil?
132
136
  sleep(0.01) unless silent
133
137
  threads << Thread.new {
134
- cmd = "aws #{resource[App::AWSReports::KEY_CLI][App::AWSReports::KEY_COMMAND]}#{region == App::AWSReports::CONST_GLOBAL ? '' : " --region #{region}"}"
135
- puts " \x1B[38;5;70m$ \x1B[38;5;240m#{cmd}\x1B[0m" unless silent
138
+ cmd = "aws #{resource[App::AWSReports::KEY_CLI][App::AWSReports::KEY_COMMAND]}#{region == App::AWSReports::CONST_GLOBAL ? '' : " --region #{region}"} --profile #{App::AWSProfile::get_profile_name}"
139
+ App::AWSOutputter::output_cli_command(cmd) unless silent
136
140
  json = `#{cmd}`
137
141
  begin
138
142
  semaphore.synchronize do
@@ -152,7 +156,7 @@ module App
152
156
  puts unless silent
153
157
 
154
158
  # Display spinner while waiting for threads to finish.
155
- Blufin::Terminal::execute_proc("AWS - Fetching: #{Blufin::Terminal::format_highlight(resource_title, false)}", Proc.new {
159
+ Blufin::Terminal::execute_proc("AWS \xe2\x80\x94 Fetching: #{Blufin::Terminal::format_highlight(resource_title, false)}", Proc.new {
156
160
  threads.each { |thread| thread.join }
157
161
  }, verbose: !silent)
158
162
  puts unless silent
@@ -178,33 +182,50 @@ module App
178
182
  auto_fetch_resources = {}
179
183
  data.each do |resource|
180
184
  if resource[1].has_key?(App::AWSReports::KEY_EXPORT)
181
- if resource[1][App::AWSReports::KEY_EXPORT].has_key?('id')
182
- auto_fetch_resources[resource[1][App::AWSReports::KEY_EXPORT]['id']] = {
183
- :resource_title => resource[0],
184
- :resource => resource[1]
185
- }
185
+ resource[1][App::AWSReports::KEY_EXPORT].each do |export|
186
+ if export.has_key?('id')
187
+ auto_fetch_resources[export['id']] = {
188
+ :resource_title => resource[0],
189
+ :resource => resource[1]
190
+ }
191
+ end
186
192
  end
187
193
  end
188
194
  end
189
195
  auto_fetch_resources
190
196
  end
191
197
 
192
- # Takes AWS api results and translates it to an array of key/value pairs we can pass to Blufin::Terminal::prompt_select().
198
+ # Takes AWS API results and translates it to an array of key/value pairs we can pass to Blufin::Terminal::prompt_select().
193
199
  # @return Array (of Hashes)
194
- def self.parse_results_for_prompt(resource, results)
200
+ def self.parse_results_for_prompt(resource, export_id, results)
195
201
  options = []
196
202
  values = []
203
+ export = nil
204
+ resource[App::AWSReports::KEY_EXPORT].each do |exp|
205
+ if exp['id'] == export_id
206
+ export = exp
207
+ break
208
+ end
209
+ end
210
+ raise RuntimeError, "Export with ID not found: #{export_id}" if export.nil?
197
211
  results.each do |result|
198
- value = result[resource[App::AWSReports::KEY_EXPORT]['value']]
199
- value = App::AWSOutputter::get_formatter(resource[App::AWSReports::KEY_EXPORT]['valueFormatter']).call(value)[0] if resource[App::AWSReports::KEY_EXPORT].has_key?('valueFormatter')
212
+ value = result[export['value']]
213
+ value = App::AWSOutputter::get_formatter(export['valueFormatter']).call(value)[0] if export.has_key?('valueFormatter')
200
214
  values << value
201
215
  end
202
216
  results.each do |result|
203
- value = result[resource[App::AWSReports::KEY_EXPORT]['value']]
204
- value = App::AWSOutputter::get_formatter(resource[App::AWSReports::KEY_EXPORT]['valueFormatter']).call(value)[0] if resource[App::AWSReports::KEY_EXPORT].has_key?('valueFormatter')
217
+ value = result[export['value']]
218
+ value = App::AWSOutputter::get_formatter(export['valueFormatter']).call(value)[0] if export.has_key?('valueFormatter')
219
+ sort = result[export['description']] # Used for sorting.
220
+ if export.has_key?('description')
221
+ text = "#{value.rjust(values.max_by(&:length).length.to_i, ' ')} \x1B[38;5;246m\xe2\x80\x94 \x1B[38;5;240m#{sort}\x1B[0m"
222
+ else
223
+ text = value
224
+ end
205
225
  options << {
206
226
  :value => value,
207
- :text => "#{value.rjust(values.max_by(&:length).length.to_i, ' ')} \x1B[38;5;246m\xe2\x80\x94 \x1B[38;5;240m#{result[resource[App::AWSReports::KEY_EXPORT]['description']]}\x1B[0m",
227
+ :text => text,
228
+ :sort => sort,
208
229
  }
209
230
  end
210
231
  options
data/lib/awx.rb CHANGED
@@ -4,40 +4,58 @@ require 'yaml'
4
4
  require 'blufin-lib'
5
5
 
6
6
  require_relative 'version'
7
- require 'core/config_unique'
8
- require 'aws/aws_config'
7
+ require 'core/opt'
9
8
  require 'aws/aws_credentials'
9
+ require 'aws/aws_profile'
10
10
 
11
- Dir["#{File.dirname(__FILE__)}/aws/**/*.rb"].each { |file| load(file) unless file =~ /\/(config_unique|aws_config|aws_credentials)\.rb\z/ }
12
- Dir["#{File.dirname(__FILE__)}/core/**/*.rb"].each { |file| load(file) unless file =~ /\/(config_unique|aws_config|aws_credentials)\.rb\z/ }
11
+ Dir["#{File.dirname(__FILE__)}/aws/**/*.rb"].each { |file| load(file) unless file =~ /\/(aws_credentials|aws_profile)\.rb\z/ }
12
+ Dir["#{File.dirname(__FILE__)}/core/**/*.rb"].each { |file| load(file) unless file =~ /\/(opt)\.rb\z/ }
13
13
  Dir["#{File.dirname(__FILE__)}/routes/**/*.rb"].each { |file| load(file) }
14
14
 
15
15
  module App
16
16
 
17
+ GEM_NAME = 'awx'
18
+ SCHEMA_FILE = "#{App::Opt::get_base_path}#{App::Opt::OPT_PATH}/config/schema.yml"
19
+ TEMPLATE_FILE = "#{App::Opt::get_base_path}#{App::Opt::OPT_PATH}/config/template.yml"
20
+ CONFIG_FILE = '~/.awx.yml'
21
+ SECRET = 'gts8cxeCn1EkzxH3ASXwnz7nboOnf6AjnQhqdjQp8kzxH7q7Ne'
22
+
17
23
  def self.execute
18
24
 
19
25
  begin
20
26
 
21
- unless !ARGV.any? || ARGV[0] == 'setup' || ARGV[0] == 'x'
22
- App::Config.initialize
27
+ unless ARGV[0] == 'config' || ARGV[0] == 'x'
28
+ Blufin::Config::init(SCHEMA_FILE, TEMPLATE_FILE, CONFIG_FILE, GEM_NAME)
29
+ App::AWSProfile::init(Blufin::Config::get)
23
30
  end
24
31
 
25
32
  Convoy::App.create do |awx|
26
33
 
27
34
  awx.version AWX_VERSION
28
- awx.summary "\x1B[38;5;198mAWX\x1B[0m \x1B[38;5;134m\xe2\x80\x94 Amazon Web-Services X-Tender (Beta)\x1B[0m"
35
+ awx.summary <<TEMPLATE
36
+ \x1B[48;5;130m\x1B[38;5;255m AWX \x1B[0m\x1B[0m \x1B[38;5;130m\xe2\x80\x94 Amazon Web-Services X️-Tender\x1B[38;5;248m
37
+
38
+ _____ __ ______ ___
39
+ / _ \\/ \\ / \\ \\/ /
40
+ / /_\\ \\ \\/\\/ /\\ /
41
+ / | \\ / / \\
42
+ \\____|__ /\\__/\\ / /___/\\ \\
43
+ \\/ \\/ \\_/\x1B[0m
44
+ TEMPLATE
29
45
  awx.description 'An abstraction layer built around the AWS-cli (written by: Albert Rannetsperger)'
30
46
 
31
- # Checks whether a AWS 'blufin-cli' profile exists within ~/.awx ...
32
- unless App::AWSConfig::get_credentials(App::AWSConfig::AWS_PROFILE_ALBERT_CLI).nil?
33
- # cf - AWS CLOUD FORMATION
47
+ # c - AWS CLOUD FORMATION
48
+ if ARGV[0] != 'setup' && ARGV[0] != 'x' && App::AWSProfile::get_profile.has_key?('CloudFormation')
34
49
  awx.command :cloudformation, :aliases => [:c] do |awx_cloudformation|
35
50
  awx_cloudformation.summary 'Create, list and delete cloud-formation stacks'
36
51
  # c - AWS CLOUD FORMATION CREATE
37
52
  awx_cloudformation.command :create, :aliases => [:c] do |awx_cloudformation_create|
38
53
  awx_cloudformation_create.summary 'Create stack'
39
54
  awx_cloudformation_create.options do |opts|
40
- opts.opt :test, 'Run through test-template.', :short => '-t', :long => '--test', :type => :boolean
55
+ if Blufin::Files::path_exists("#{File.expand_path(App::AWSProfile::get_profile['CloudFormation']['TemplatePath'])}/test")
56
+ opts.opt :test, 'Run through test-template.', :short => '-t', :long => '--test', :type => :boolean
57
+ end
58
+ opts.opt :test, "Re-run previous with cached values (if exists) \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-R', :long => '--re-run', :type => :boolean
41
59
  end
42
60
  awx_cloudformation_create.action do |opts, args|
43
61
  AppCommand::AWSCloudFormationCreate.new(opts, args).execute
@@ -45,7 +63,7 @@ module App
45
63
  end
46
64
  # d - AWS CLOUD FORMATION DETECT-DRIFT
47
65
  awx_cloudformation.command :detect_drift, :aliases => [:d] do |awx_cloudformation_detect_drift|
48
- awx_cloudformation_detect_drift.summary 'Detect drift (for stack)'
66
+ awx_cloudformation_detect_drift.summary 'Detect drift (currently for all stacks)'
49
67
  awx_cloudformation_detect_drift.action do |opts, args|
50
68
  AppCommand::AWSCloudFormationDetectDrift.new(opts, args).execute
51
69
  end
@@ -58,52 +76,77 @@ module App
58
76
  end
59
77
  end
60
78
  awx_cloudformation.action do
61
- system("#{ConfigUnique::GEM_NAME} c -h")
79
+ system("#{App::GEM_NAME} c -h")
62
80
  end
63
81
  end
64
- # l - AWS LIST
65
- awx.command :list, :aliases => [:l] do |awx_list|
66
- awx_list.summary 'List AWS instances/resources'
67
- awx_list.options do |opts|
68
- opts.opt :json, 'Return data as JSON', :short => '-j', :long => '--json', :type => :boolean
69
- opts.opt :json_prompt, 'Return data as JSON (for Terminal::prompt)', :short => '-J', :long => '--json-prompt', :type => :boolean
70
- opts.opt :yaml, 'Return data as Yaml', :short => '-y', :long => '--yaml', :type => :boolean
71
- opts.opt :resource, "Specify a resource. For all resources type: #{Blufin::Terminal::format_command('all')}", :short => '-r', :long => '--resource', :type => :string
72
- opts.opt :verbose, 'Output the original response from AWS (with all fields).', :short => '-v', :long => '--verbose', :type => :boolean
73
- opts.opt :metadata, 'Output the YML definition file metadata.', :short => '-m', :long => '--metadata', :type => :boolean
74
- # TODO - Implement Region + Project filtering (possibly more).
75
- opts.opt :region, "Specify a region \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-R', :long => '--region', :type => :string
76
- opts.opt :environment, "Specify a environment \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-e', :long => '--environment', :type => :string
77
- opts.opt :project, "Specify a project \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-p', :long => '--project', :type => :string
78
- end
79
- awx_list.action do |opts, args|
80
- AppCommand::AWSList.new(opts, args).execute
82
+ end
83
+
84
+ # d - DYNAMO-DB
85
+ if ARGV[0] != 'setup' && ARGV[0] != 'x' && Blufin::Config::get.has_key?('DynamoDBPath')
86
+ awx.command :dynamodb, :aliases => [:db] do |db|
87
+ db.summary 'Run a local instance of DynamoDB'
88
+ db.action do |opts, args|
89
+ AppCommand::AWSDynamoDB.new(opts, args).execute
81
90
  end
82
91
  end
83
- # L - AWS LAMBDA
84
- awx.command :lambda, :aliases => [:L] do |awx_lambda|
85
- awx_lambda.summary 'Quickly invoke a Lambda function locally (without the need to deploy)'
86
- awx_lambda.options do |opts|
87
- opts.opt :headers_file, 'The path to a JSON containing HTTP Headers you want to send', :short => '-H', :long => '--headers-file', :type => :string
88
- opts.opt :payload_file, 'The path to a JSON payload you want to send', :short => '-p', :long => '--pay-load-file', :type => :string
92
+ end
93
+
94
+ # D - DEPLOY
95
+ if ARGV[0] != 'setup' && ARGV[0] != 'x' && (App::AWSProfile::get_profile.has_key?('Projects'))
96
+ awx.command :deploy, :aliases => [:D] do |deploy|
97
+ deploy.summary 'Deploy application(s) to AWS'
98
+ deploy.options do |opts|
99
+ opts.opt :skip_build, 'Skip build', :short => '-S', :long => '--skip-build', :type => :boolean
100
+ end
101
+ deploy.action do |opts, args|
102
+ AppCommand::AWSDeploy.new(opts, args).execute
89
103
  end
90
- awx_lambda.action do |opts, args|
91
- AppCommand::AWSLambda.new(opts, args).execute
104
+ end
105
+ end
106
+
107
+ # l - AWS LIST
108
+ awx.command :list, :aliases => [:l] do |awx_list|
109
+ awx_list.summary 'List AWS instances/resources'
110
+ awx_list.options do |opts|
111
+ opts.opt :json, 'Return data as JSON', :short => '-j', :long => '--json', :type => :boolean
112
+ opts.opt :json_prompt, 'Return data as JSON (for Terminal::prompt)', :short => '-J', :long => '--json-prompt', :type => :boolean
113
+ opts.opt :yaml, 'Return data as Yaml', :short => '-y', :long => '--yaml', :type => :boolean
114
+ opts.opt :yaml_colored, 'Return data as Yaml (colored)', :short => '-Y', :long => '--yaml-colored', :type => :boolean
115
+ opts.opt :resource, "Specify a resource. For all resources type: #{Blufin::Terminal::format_command('all')}", :short => '-r', :long => '--resource', :type => :string
116
+ opts.opt :verbose, 'Output the original response from AWS (with all fields).', :short => '-v', :long => '--verbose', :type => :boolean
117
+ opts.opt :metadata, 'Output the YML definition file metadata.', :short => '-m', :long => '--metadata', :type => :boolean
118
+ # TODO - Implement Region + Project filtering (possibly more).
119
+ opts.opt :region, "Specify a region \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-R', :long => '--region', :type => :string
120
+ opts.opt :environment, "Specify a environment \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-e', :long => '--environment', :type => :string
121
+ opts.opt :project, "Specify a project \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-p', :long => '--project', :type => :string
122
+ end
123
+ awx_list.action do |opts, args|
124
+ AppCommand::AWSList.new(opts, args).execute
125
+ end
126
+ end
127
+
128
+ # s - SWITCH
129
+ if ARGV[0] != 'setup' && ARGV[0] != 'x' && App::AWSProfile::get_profile_names.length > 1
130
+ # Only show if we have multiple profiles.
131
+ awx.command :switch, :aliases => [:s] do |switch|
132
+ switch.summary 'Switch Profiles'
133
+ switch.action do |opts, args|
134
+ AppCommand::AWSSwitch.new(opts, args).execute
92
135
  end
93
136
  end
94
137
  end
95
138
 
96
- # x - SETUP
97
- awx.command :setup, :aliases => [:x] do |setup|
98
- setup.summary 'Setup your configuration file'
99
- setup.action do |opts, args|
100
- AppCommand::Setup.new(opts, args).execute
139
+ # x - CONFIG
140
+ awx.command :config, :aliases => [:x] do |config|
141
+ config.summary 'Setup your configuration file'
142
+ config.action do
143
+ Blufin::Config::edit_config(App::CONFIG_FILE)
101
144
  end
102
145
  end
103
146
 
104
- # AWX (DEFAULT)
147
+ # awx - DEFAULT
105
148
  awx.action do
106
- system("#{ConfigUnique::GEM_NAME} -h")
149
+ system("#{App::GEM_NAME} -h")
107
150
  end
108
151
 
109
152
  end
@@ -111,8 +154,18 @@ module App
111
154
  rescue RuntimeError => e
112
155
 
113
156
  Blufin::Terminal::print_exception(e);
157
+
114
158
  end
115
159
 
116
160
  end
117
161
 
162
+ # Very hacky code that looks in the configuration file for a key/pair value and if exists, returns true.
163
+ # @return bool
164
+ def self.is_albert_mac
165
+ if Blufin::Config::get.has_key?('CustomOptions') && Blufin::Config::get['CustomOptions'].has_key?('Secret')
166
+ return Blufin::Config::get['CustomOptions']['Secret'] == SECRET
167
+ end
168
+ false
169
+ end
170
+
118
171
  end
data/lib/core/opt.rb CHANGED
@@ -2,7 +2,7 @@ module App
2
2
 
3
3
  class Opt
4
4
 
5
- OPT_PATH_YML = '/opt/yml'
5
+ OPT_PATH = '/opt'
6
6
 
7
7
  # Get PATH to opt files.
8
8
  # @return String
@@ -1,27 +1,34 @@
1
1
  require 'json'
2
- require 'securerandom'
3
2
  require 'digest'
4
3
 
5
4
  module AppCommand
6
5
 
7
6
  class AWSCloudFormationCreate < ::Convoy::ActionCommand::Base
8
7
 
9
- DEFAULT = 'Default'
10
- DESCRIPTION = 'Description'
11
- PARAMETERS = 'Parameters'
12
- SPECIAL_METHODS = [:before_create, :after_create, :before_teardown, :after_teardown]
13
- OPTION_TAGS = %w(stackname description environment project region resource cache_uuid)
14
- OPTION_STACK_NAME = 'StackName'
15
- OPTION_DESCRIPTION = 'Description'
16
- OPTION_PROJECT = 'Project'
17
- OPTION_ENVIRONMENT = 'Environment'
18
- OPTION_REGION = 'Region'
19
- MATCHERS = %w(CATEGORY TEMPLATE PROJECT ENVIRONMENT REGION UUID)
20
- OPTIONS = 'Options'
21
- SPECIAL = 'AWS-specific'
22
- CACHE_UUID = 'cache_uuid'
23
- TEST = 'test'
24
- SPACER = '<<--Spacer-->>'
8
+ DEFAULT = 'Default'
9
+ PARAMETERS = 'Parameters'
10
+ SPECIAL_METHODS = [:before_create, :after_create, :before_teardown, :after_teardown]
11
+ AWS_TAGS = %w(Environment Project Region Category Template)
12
+ RESERVED_WORDS = %w(stackname description environment project region timeout resource cache_uuid category template terminationprotection projectid)
13
+ OPTION_PROJECT_ID = 'ProjectId'
14
+ OPTION_DEPLOYMENT_STACK = 'DeploymentStack'
15
+ OPTION_STACK_NAME = 'StackName'
16
+ OPTION_TIMEOUT = 'Timeout'
17
+ OPTION_DESCRIPTION = 'Description'
18
+ OPTION_PROJECT = 'Project'
19
+ OPTION_ENVIRONMENT = 'Environment'
20
+ OPTION_REGION = 'Region'
21
+ OPTION_CATEGORY = 'Category'
22
+ OPTION_TEMPLATE = 'Template'
23
+ OPTION_TERM_PROTECT = 'TerminationProtection'
24
+ MATCHERS = %w(CATEGORY TEMPLATE PROJECT ENVIRONMENT REGION UUID)
25
+ OPTIONS = 'Options'
26
+ SPECIAL = 'AWS-specific'
27
+ CACHE_UUID = 'cache_uuid'
28
+ TEST = 'test'
29
+ SPACER = '<<--Spacer-->>'
30
+ CAPABILITIES = 'Capabilities'
31
+ RETURN_VALUE = 'PjNkHK33EopWxCpzOQfuku3la'
25
32
 
26
33
  def execute
27
34
 
@@ -41,11 +48,16 @@ module AppCommand
41
48
  @table_widths = {}
42
49
  @export_map = {}
43
50
  @columns = {}
44
- @options_default = {} # The options from the cloudformation.rb file (default for all).
51
+ @options_default = {}
45
52
  @cache = {}
53
+ @cache_valid = false
54
+ @projects = {}
46
55
 
56
+ @terminal_width = Blufin::Terminal::get_terminal_width
47
57
  @columns, @data, @export_map, @table_widths = App::AWSReports::parse_metadata(@regions)
48
58
 
59
+ Blufin::Projects::init(App::AWSProfile::get_profile, App::CONFIG_FILE)
60
+
49
61
  opts_validate
50
62
  opts_routing
51
63
 
@@ -64,26 +76,14 @@ module AppCommand
64
76
  terminal_required_width = 227
65
77
  Blufin::Terminal::error("Output for this command \x1B[38;5;240m(#{Blufin::Terminal::format_action(terminal_required_width)}\x1B[38;5;240m-width)\x1B[0m does not fit in Terminal \x1B[38;5;240m(#{Blufin::Terminal::format_action(terminal_width_actual)}\x1B[38;5;240m-width)\x1B[0m", 'Please make your terminal wider and try again.', true) if terminal_width_actual < terminal_required_width
66
78
 
67
- @warnings = []
68
- @auto_fetch_resources = App::AWSReports::get_auto_fetch_resources(@data)
69
-
70
- # Validate the cloudformation.rb file that sits in the root of: blufin-secrets/cloudformation.
71
- cloudformation_defaults_file = "#{App::AWSCloudFormation::get_cloudformation_path}/#{App::AWSCloudFormation::FILE_CLOUDFORMATION_DEFAULTS}"
72
- if Blufin::Files::file_exists(cloudformation_defaults_file)
73
- require cloudformation_defaults_file
74
- expected_constants = %w(STACK_NAME PROJECTS ENVIRONMENTS REGIONS)
75
- CloudFormation::Defaults::constants.each do |constant|
76
- constant = constant.to_s
77
- expected_constants.delete(constant) if expected_constants.include?(constant)
78
- @options_default[OPTION_STACK_NAME] = CloudFormation::Defaults::STACK_NAME if constant == 'STACK_NAME'
79
- @options_default[OPTION_PROJECT] = CloudFormation::Defaults::PROJECTS if constant == 'PROJECTS'
80
- @options_default[OPTION_ENVIRONMENT] = CloudFormation::Defaults::ENVIRONMENTS if constant == 'ENVIRONMENTS'
81
- @options_default[OPTION_REGION] = CloudFormation::Defaults::REGIONS if constant == 'REGIONS'
82
- end
83
- raise RuntimeError, "Missing expected constants: #{expected_constants.join(',')}" if expected_constants.any?
84
- else
85
- raise RuntimeError, "File does not exist: #{cloudformation_defaults_file}"
86
- end
79
+ @warnings = []
80
+ @auto_fetch_resources = App::AWSReports::get_auto_fetch_resources(@data)
81
+
82
+ @options_default[OPTION_ENVIRONMENT] = Blufin::Projects::get_environments
83
+ @options_default[OPTION_REGION] = App::AWSProfile::get_profile['CloudFormation']['Defaults']['Regions']
84
+ @options_default[OPTION_STACK_NAME] = App::AWSProfile::get_profile['CloudFormation']['Defaults']['StackName']
85
+ @options_default[OPTION_TIMEOUT] = App::AWSProfile::get_profile['CloudFormation']['Defaults']['Timeout']
86
+ @options_default[OPTION_PROJECT] = Blufin::Projects::get_project_names
87
87
 
88
88
  # Loop the entire blufin-secrets/cloudformation path(s) and validate all the template(s).
89
89
  Blufin::Files::get_dirs_in_dir(App::AWSCloudFormation::get_cloudformation_path).each do |path|
@@ -101,10 +101,14 @@ module AppCommand
101
101
  method_before_teardown = nil
102
102
  method_after_teardown = nil
103
103
  intro = nil
104
+ description = nil
104
105
  stack_name = nil
105
106
  projects = nil
106
107
  environments = nil
107
108
  regions = nil
109
+ timeout = nil
110
+ single_serve = false
111
+ deployment_stack = nil
108
112
  parameters = {}
109
113
  warnings_count = @warnings.length
110
114
  raise RuntimeError, "Template name must consist of [service]/[service-description] with exactly one slash, instead got: #{template_name}" if template_name.strip.split('/').length != 2
@@ -114,13 +118,13 @@ module AppCommand
114
118
  begin
115
119
  yml_data = YAML.load_file(File.expand_path(file))
116
120
  rescue Exception => e
117
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Unable to parse #{Blufin::Terminal::format_highlight('template.yml')}\x1B[38;5;240m: #{e.message}"
121
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 #{e.message.split('template.yml): ')[1].capitalize}"
118
122
  next
119
123
  end
120
124
  if yml_data.is_a?(Hash)
121
- if yml_data.has_key?(DESCRIPTION)
122
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Description must be omitted because this will be handled by the script."
123
- end
125
+ file_cloudformation = file
126
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 AWSTemplateFormatVersion: '2010-09-09' is missing." unless yml_data.has_key?('AWSTemplateFormatVersion')
127
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 This template either has no resources, invalid resources, or something else wrong with the resources." if !yml_data.has_key?('Resources') || yml_data['Resources'].nil? || !yml_data['Resources'].is_a?(Hash) || yml_data['Resources'].length == 0
124
128
  if yml_data.has_key?(PARAMETERS) && yml_data[PARAMETERS].is_a?(Hash) && yml_data[PARAMETERS].length > 0
125
129
  yml_data[PARAMETERS].each do |resource_name, data|
126
130
  # Validate keys are in specific order.
@@ -128,6 +132,7 @@ module AppCommand
128
132
  'Type' => true,
129
133
  'Description' => false,
130
134
  'Default' => false,
135
+ 'AllowedValues' => false,
131
136
  'AllowedPattern' => false,
132
137
  'MinLength' => false,
133
138
  'MinValue' => false,
@@ -136,10 +141,10 @@ module AppCommand
136
141
  'ConstraintDescription' => false,
137
142
  }
138
143
  Blufin::Validate::assert_valid_keys(expected, data.keys, "#{file} \xe2\x86\x92 #{resource_name}")
139
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template has reserved parameter: #{Blufin::Terminal::format_invalid(resource_name)}" if [OPTION_STACK_NAME.downcase].concat(OPTION_TAGS).include?(resource_name.downcase)
144
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template has reserved parameter: #{Blufin::Terminal::format_invalid(resource_name)}" if [OPTION_STACK_NAME.downcase].concat(RESERVED_WORDS).include?(resource_name.downcase)
140
145
  parameters[resource_name] = data
141
146
  if @auto_fetch_resources.has_key?(resource_name)
142
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Parameter: #{resource_name} cannot have default value: '#{data['Default']}' because it is a live look-up list (from AWS)." if data.keys.include?('Default')
147
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Parameter: #{resource_name} cannot have default value: '#{data[DEFAULT]}' because it is a live look-up list (from AWS)." if data.keys.include?(DEFAULT)
143
148
  end
144
149
  # Validate parameter type.
145
150
  valid_parameter_types = %w(String Number List<Number> CommaDelimitedList)
@@ -182,32 +187,77 @@ module AppCommand
182
187
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid parameter constraint(s): #{Blufin::Terminal::format_invalid(constraints.join(','))}. To use these, type must be Number."
183
188
  end
184
189
  end
190
+ # If Allowed Values is set, certain conditions must apply.
191
+ if data.has_key?('AllowedValues')
192
+ %w(AllowedPattern MinLength MaxLength MinValue MaxValue).each do |invalid_key|
193
+ if data.has_key?(invalid_key)
194
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 AllowedValues is set so cannot have: #{Blufin::Terminal::format_invalid(data['invalid_key'])}"
195
+ end
196
+ end
197
+ # Must be Array.
198
+ unless data['AllowedValues'].is_a?(Array)
199
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 AllowedValues must be Array, instead got: #{Blufin::Terminal::format_invalid(data['AllowedValues'].class)}"
200
+ end
201
+ end
202
+ end
203
+ end
204
+ # Validate description (if exists).
205
+ if yml_data.has_key?(OPTION_DESCRIPTION)
206
+ description = yml_data[OPTION_DESCRIPTION]
207
+ # Validate replaceable value(s) exist.
208
+ matches = description.scan(/{{[A-Za-z0-9]+}}/)
209
+ matches.each do |match|
210
+ match = match.gsub(/^{{/, '').gsub(/}}$/, '')
211
+ unless parameters.keys.include?(match)
212
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid description matcher: #{Blufin::Terminal::format_invalid(match)}"
213
+ end
214
+ end
215
+ else
216
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template is missing description."
217
+ end
218
+ # Look for (and validate) DeletionPolicy: Retain
219
+ if yml_data.has_key?('Resources') && yml_data['Resources'].is_a?(Hash)
220
+ key_has_dp = {}
221
+ yml_data['Resources'].keys { |k| key_has_dp[k] = 0 }
222
+ yml_data['Resources'].each do |key, value|
223
+ key_has_dp[key] = 1 if value.is_a?(Hash) && value.has_key?('DeletionPolicy') && value['DeletionPolicy'].downcase == 'retain'
224
+ end
225
+ key_has_dp_sum = 0
226
+ key_has_dp.each { |k, v| key_has_dp_sum += v }
227
+ if key_has_dp_sum == 0 || key_has_dp_sum == yml_data['Resources'].keys.length
228
+ # If we have at least 1 DeletionPolicy: Retain and no errors, this is a single-serve template.
229
+ single_serve = key_has_dp_sum > 0
230
+ else
231
+ # Add a warning if not all resources have the DeletionPolicy: Retain.
232
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 If DeletionPolicy: Retain is used, it must be set on all resources for template."
185
233
  end
186
234
  end
187
- file_cloudformation = file
188
235
  end
189
236
  elsif filename == 'template.rb'
190
237
  file_ruby = file
191
238
  # Load the template.rb file/class.
192
239
  require file
193
- expected_constants = %w(INTRO)
240
+ expected_constants = %w()
194
241
  Template::constants.each do |constant|
195
242
  constant = constant.to_s
196
243
  expected_constants.delete(constant) if expected_constants.include?(constant)
197
- intro = Template::INTRO if constant == 'INTRO'
198
244
  # Optional constants.
199
- stack_name = Template::STACK_NAME if constant == 'STACK_NAME'
200
- projects = Template::PROJECTS if constant == 'PROJECTS'
201
- environments = Template::ENVIRONMENTS if constant == 'ENVIRONMENTS'
202
- regions = Template::REGIONS if constant == 'REGIONS'
245
+ intro = Template::INTRO if constant == 'INTRO'
246
+ stack_name = Template::STACK_NAME if constant == 'STACK_NAME'
247
+ projects = Template::PROJECTS if constant == 'PROJECTS'
248
+ environments = Template::ENVIRONMENTS if constant == 'ENVIRONMENTS'
249
+ regions = Template::REGIONS if constant == 'REGIONS'
250
+ timeout = Template::TIMEOUT if constant == 'TIMEOUT'
251
+ deployment_stack = Template::DEPLOYMENT_STACK if constant == 'DEPLOYMENT_STACK'
203
252
  end
253
+ # Validate stack name.
204
254
  if stack_name.nil? || stack_name.strip == ''
205
255
  stack_name = @options_default[OPTION_STACK_NAME]
206
256
  else
207
257
  # Validate Stack Name (if exists).
208
- matches = stack_name.scan(/{[A-Za-z0-9]+}/)
258
+ matches = stack_name.scan(/{{[A-Za-z0-9]+}}/)
209
259
  matches.each do |match|
210
- match = match.gsub(/^{/, '').gsub(/}$/, '')
260
+ match = match.gsub(/^{{/, '').gsub(/}}$/, '')
211
261
  if match != match.upcase
212
262
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 All stack-name matchers must be uppercase, found: #{Blufin::Terminal::format_invalid(match)}"
213
263
  next
@@ -215,12 +265,16 @@ module AppCommand
215
265
  unless MATCHERS.include?(match)
216
266
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid stack-name matcher: #{Blufin::Terminal::format_invalid(match)}"
217
267
  end
218
- stack_name_stripped = stack_name.gsub(/{[A-Za-z0-9]+}/, '')
268
+ stack_name_stripped = stack_name.gsub(/{{[A-Za-z0-9]+}}/, '')
219
269
  if stack_name_stripped !~ /[a-z0-9\-]/
220
270
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Stack-name has invalid or non-matcher, uppercase characters: #{Blufin::Terminal::format_invalid(stack_name)} \x1B[38;5;240m(#{stack_name_stripped})\x1B[0m"
221
271
  end
222
272
  end
223
273
  end
274
+ # Make sure deployment stack is not a reserved word.
275
+ if deployment_stack.is_a?(String) && %w(lambda).include?(deployment_stack.downcase)
276
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 #{Blufin::Terminal::format_highlight('DEPLOYMENT_STACK')} value is a reserved word: #{Blufin::Terminal::format_invalid(deployment_stack)}"
277
+ end
224
278
  # If any constants are missing, this should catch it.
225
279
  expected_constants.each { |missing_constant| @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 The #{Blufin::Terminal::format_highlight('template.rb')}\x1B[38;5;240m file is missing a constant: #{Blufin::Terminal::format_invalid(missing_constant)}" }
226
280
  Template::methods.each do |method|
@@ -259,8 +313,9 @@ module AppCommand
259
313
  end
260
314
  end
261
315
  end
262
- intro = nil if !intro.nil? && intro.gsub("\n", '').gsub("\t", '').strip == ''
263
- stack_name = nil if !stack_name.nil? && stack_name.gsub("\n", '').gsub("\t", '').strip == ''
316
+ intro = nil if !intro.nil? && intro.gsub("\n", '').gsub("\t", '').strip == ''
317
+ stack_name = nil if !stack_name.nil? && stack_name.gsub("\n", '').gsub("\t", '').strip == ''
318
+ description = nil if !description.nil? && description.gsub("\n", '').gsub("\t", '').strip == ''
264
319
  # Unload the template.rb file/class.
265
320
  Object.send(:remove_const, :Template)
266
321
  end
@@ -274,7 +329,7 @@ module AppCommand
274
329
  :broken => 'Broken'
275
330
  }
276
331
  else
277
- @templates[category][template] = {
332
+ @templates[category][template] = {
278
333
  :name => template,
279
334
  :broken => false,
280
335
  :file_cloudformation => file_cloudformation,
@@ -285,11 +340,15 @@ module AppCommand
285
340
  :method_after_teardown => method_after_teardown,
286
341
  :parameters => parameters,
287
342
  :intro => intro,
343
+ :description => description,
288
344
  :stack_name => stack_name,
289
345
  :projects => projects,
290
346
  :environments => environments,
291
347
  :regions => regions,
348
+ :timeout => timeout,
349
+ :single_serve => single_serve
292
350
  }
351
+ @templates[category][template][:deployment_stack] = deployment_stack unless deployment_stack.nil?
293
352
  end
294
353
  end
295
354
  end
@@ -306,15 +365,50 @@ module AppCommand
306
365
 
307
366
  def opts_routing
308
367
 
368
+ used_cache = true
369
+ showing_tags = false
370
+
309
371
  # Show prompt to select template.
310
372
  category, template, @template = select_template_prompt
311
- unless @params.any?
373
+
374
+ if @params.any?
375
+ # Here we replace the UUID for the stack-name at the last-second to enable parallel, identical runs (if exists).
376
+ @params[OPTION_STACK_NAME] = replace_stack_suffix(@params[OPTION_STACK_NAME]) if @params.has_key?(OPTION_STACK_NAME)
377
+ else
378
+ used_cache = false
312
379
  if !@template[:parameters].nil? && @template[:parameters].any?
313
- @template[:parameters].each do |(param_name, param_data)|
380
+ @template[:parameters].each_with_index do |(param_name, param_data), idx|
381
+ if AWS_TAGS.include?(param_name) && !showing_tags
382
+ showing_tags = true
383
+ puts "\x1B[38;5;#{App::AWSOutputter::DIVIDER_COLOR}m--- \x1B[38;5;136m[Tags]\x1B[0m\x1B[38;5;#{App::AWSOutputter::DIVIDER_COLOR}m #{'-' * (@terminal_width - 17)}\x1B[0m"
384
+ puts
385
+ end
386
+ # Deployment ID needs to be handled here because we need to know what the project/region/environment is before we can display the options.
387
+ if param_name == OPTION_PROJECT_ID
388
+ # Figure out which deployments are eligible for this stack.
389
+ if @template.has_key?(:deployment_stack)
390
+ @projects = Blufin::Projects::get_deployments(
391
+ @params[OPTION_PROJECT],
392
+ @template[:deployment_stack],
393
+ @params[OPTION_REGION],
394
+ @params[OPTION_ENVIRONMENT]
395
+ )
396
+ if @projects.any?
397
+ project_options = []
398
+ @projects.each { |project| project_options << project[1][Blufin::Projects::DEPLOYMENT_ID] }
399
+ @params[param_name] = Blufin::Terminal::prompt_select('Select Project ID:', project_options, help: param_data[OPTION_DESCRIPTION])
400
+ puts
401
+ end
402
+ end
403
+ next
404
+ end
314
405
  @params[param_name] = get_parameter_value(param_data, param_name, category, template)
315
- puts
406
+ # Puts space unless it's a hidden parameter. Keeps spacing consistent.
407
+ puts unless [OPTION_TIMEOUT].include?(param_name)
316
408
  end
317
409
  end
410
+ @params[OPTION_CATEGORY] = category
411
+ @params[OPTION_TEMPLATE] = template
318
412
  # Cache the inputted value(s).
319
413
  cache_params = @params
320
414
  cache_params[CACHE_UUID] = get_cache_hexdigest(@template[:parameters])
@@ -323,88 +417,115 @@ module AppCommand
323
417
 
324
418
  # If this is a test-run, abandon ship here.
325
419
  if @opts[:test]
326
- puts
420
+ puts if used_cache
327
421
  puts "\x1B[38;5;196m Exiting because this is only a test-run.\x1B[0m"
328
422
  puts
423
+ puts "\x1B[38;5;246m#{@params.to_yaml}\x1B[0m"
329
424
  exit
330
425
  end
331
426
 
332
- # Clear the screen.
333
427
  system('clear')
334
428
 
335
429
  capabilities_arr = []
336
430
  capabilities_str = nil
337
431
 
338
432
  # Upload the template to S3.
339
- App::AWSCloudFormation::upload_cloudformation_template(category, template, @params[OPTION_DESCRIPTION])
433
+ s3_url = App::AWSCloudFormation::upload_cloudformation_template(category, template, @params[OPTION_DESCRIPTION])
340
434
 
341
435
  # Validates the template.
342
- s3_url = App::AWSCloudFormation::get_cloudformation_s3_bucket_url(category, template)
343
436
  validation = App::AWSCli::cloudformation_stack_validate(@params[OPTION_REGION], s3_url)
344
437
 
345
438
  # Check if validation output is JSON (and output appropriate format).
346
439
  begin
347
440
  hash = JSON.parse(validation.to_json)
348
441
  raise RuntimeError, 'Not a Hash' unless hash.is_a?(Hash)
349
- puts
350
- puts " \x1B[38;5;240mAWS says your template is: \x1B[38;5;40mVALID\x1B[0m"
351
- puts
352
-
353
- # TODO - Remove (once dev-done).
354
- # Blufin::Terminal::code_highlight(hash.to_yaml, 'yml', 4)
355
-
442
+ Blufin::Terminal::success("AWS says your template is: \x1B[38;5;40mVALID")
443
+ # Output the AWS response in YML format.
444
+ Blufin::Terminal::code_highlight(hash.to_yaml, 'yml', 4)
356
445
  # Extract required capabilities (if any).
357
- if hash.has_key?('Capabilities')
358
- capabilities_arr = hash['Capabilities']
446
+ if hash.has_key?(CAPABILITIES)
447
+ capabilities_arr = hash[CAPABILITIES]
359
448
  capabilities_str = "\"#{capabilities_arr.join('" "')}\""
360
449
  end
361
450
  rescue
362
451
  Blufin::Terminal::error("AWS says your template is: \x1B[38;5;196mINVALID", App::AWSCli::format_cli_error(validation), true)
363
452
  end
364
453
 
365
- # TODO - 2) Must output parameters to have the colon aligned.
366
- # TODO - 3) Implement Stack Termination protection (CRUCIAL!!).
367
-
368
- output = {}
369
- output['StackName'] = @params[OPTION_STACK_NAME]
370
- output['Description'] = @params[OPTION_DESCRIPTION]
371
- output['Capabilities'] = capabilities_arr.join(', ') unless capabilities_str.nil?
372
- output[SPACER] = true
454
+ output = {}
455
+ output[OPTION_STACK_NAME] = @params[OPTION_STACK_NAME]
456
+ output[OPTION_DESCRIPTION] = @params[OPTION_DESCRIPTION]
457
+ output[SPACER] = true
458
+ if @template.has_key?(:deployment_stack) && !@params[OPTION_PROJECT_ID].nil? && @params[OPTION_PROJECT_ID].length > 0
459
+ output[OPTION_DEPLOYMENT_STACK] = @template[:deployment_stack]
460
+ output[OPTION_PROJECT_ID] = @params[OPTION_PROJECT_ID]
461
+ end
462
+ output[OPTION_TIMEOUT] = @params[OPTION_TIMEOUT]
463
+ output[OPTION_TERM_PROTECT] = @params[OPTION_TERM_PROTECT] unless @template[:single_serve]
464
+ output[CAPABILITIES] = capabilities_arr.join(', ') unless capabilities_str.nil?
373
465
 
374
466
  @params.each do |key, value|
375
- next if OPTION_TAGS.include?(key.downcase)
376
- next if OPTION_STACK_NAME.downcase == key.downcase
377
- next if OPTION_DESCRIPTION.downcase == key.downcase
467
+ next if RESERVED_WORDS.include?(key.downcase)
378
468
  output[key] = value
379
469
  end
380
470
 
381
- # TODO NOW - FINISH THIS!
382
- puts output.to_yaml
383
- exit
384
-
385
471
  params_output = []
386
- params_output << "\x1B[38;5;240mStackName \xe2\x80\x94 \x1B[38;5;40m#{@params[OPTION_STACK_NAME]}"
387
- params_output << "\x1B[38;5;240mCapabilities \xe2\x80\x94 \x1B[38;5;40m#{capabilities_arr.join(', ')}" unless capabilities_str.nil?
472
+ padding = output.keys.max_by(&:length).length
473
+ spacer_passed = false
474
+ output.each do |key, value|
475
+ next if [OPTION_DEPLOYMENT_STACK, OPTION_PROJECT_ID, OPTION_TIMEOUT, OPTION_TERM_PROTECT, CAPABILITIES].include?(key)
476
+ if key == SPACER
477
+ spacer_passed = true
478
+ next
479
+ end
480
+ if spacer_passed
481
+ params_output << "\x1B[38;5;240m#{key.rjust(padding, ' ')} : \x1B[38;5;28m#{value}"
482
+ else
483
+ params_output << "\x1B[38;5;240m#{key.rjust(padding, ' ')} : \x1B[38;5;28m#{value}"
484
+ end
485
+ end
388
486
 
389
- @params.each do |key, value|
390
- next if OPTION_TAGS.include?(key.downcase)
391
- next if OPTION_STACK_NAME.downcase == key.downcase
392
- params_output << "\x1B[38;5;240m#{key} \xe2\x80\x94 \x1B[38;5;40m#{value}"
487
+ if @template.has_key?(:deployment_stack)
488
+ params_output << "\x1B[38;5;240m#{OPTION_PROJECT_ID.rjust(padding, ' ')} : \x1B[38;5;246m#{output[OPTION_PROJECT_ID]}" if output.has_key?(OPTION_PROJECT_ID)
489
+ params_output << "\x1B[38;5;240m#{OPTION_DEPLOYMENT_STACK.rjust(padding, ' ')} : \x1B[38;5;246m#{output[OPTION_DEPLOYMENT_STACK]}" if output.has_key?(OPTION_DEPLOYMENT_STACK)
393
490
  end
491
+ params_output << "\x1B[38;5;240m#{OPTION_TIMEOUT.rjust(padding, ' ')} : \x1B[38;5;246m#{output[OPTION_TIMEOUT]} Minute(s)\x1B[0m"
492
+ params_output << "\x1B[38;5;240m#{OPTION_TERM_PROTECT.rjust(padding, ' ')} : #{output[OPTION_TERM_PROTECT] ? "\x1B[38;5;246mYES\x1B[0m" : "\x1B[38;5;246mNO\x1B[0m"}" unless @template[:single_serve]
493
+ params_output << "\x1B[38;5;240m#{CAPABILITIES.rjust(padding, ' ')} : \x1B[38;5;196m#{output[CAPABILITIES]}" if output.has_key?(CAPABILITIES)
494
+
495
+ if Blufin::Terminal::prompt_yes_no("Review: #{App::AWSOutputter::render_selection(category, template)}", params_output, 'Create Stack?', false)
394
496
 
395
- if Blufin::Terminal::prompt_yes_no("Deploy: #{render_category_template(category, template)} ?", params_output, 'Create CloudFormation Stack in AWS', false)
497
+ # If no 'after' actions necessary, give option to terminate script.
498
+ term_script = false
499
+ if @template[:method_after_create].nil? && !@template[:single_serve]
500
+ term_script = Blufin::Terminal::prompt_yes?(' End script after CloudFormation starts running?')
501
+ puts
502
+ end
396
503
 
397
504
  # Call :before_create() -- if exists.
398
505
  @template[:method_before_create].call(@params) unless @template[:method_before_create].nil?
399
506
 
400
- # Create the Stack.
401
- App::AWSCli::cloudformation_stack_create(@params[OPTION_REGION], @params[OPTION_STACK_NAME], s3_url, params: assemble_params(@params), tags: assemble_tags(@params), capabilities: capabilities_arr)
507
+ # Create the Stack (if term_scrip == TRUE, script terminates inside).
508
+ App::AWSCli::cloudformation_stack_create(@params[OPTION_REGION], @params[OPTION_STACK_NAME], s3_url,
509
+ params: assemble_params(@params),
510
+ tags: assemble_tags(@params),
511
+ capabilities: capabilities_arr,
512
+ term_protect: @template[:single_serve] ? false : @params[OPTION_TERM_PROTECT],
513
+ term_script: term_script,
514
+ timeout: @params[OPTION_TIMEOUT])
402
515
 
403
- # Call :before_create() -- if exists.
516
+ # Call :after_create() -- if exists.
404
517
  @template[:method_after_create].call(@params) unless @template[:method_after_create].nil?
405
518
 
406
- # Success message.
407
- Blufin::Terminal::success('Stack creation was successful.')
519
+ # Success/Failure messages.
520
+ if @template[:single_serve]
521
+ if App::AWSCli::execute_as_proc("Deleting Stack: #{Blufin::Terminal::format_highlight(@params[OPTION_STACK_NAME])}", "cloudformation delete-stack --stack-name #{@params[OPTION_STACK_NAME]}", @params[OPTION_REGION])
522
+ Blufin::Terminal::success("#{App::AWSOutputter::render_selection(category, template)} was successfully created (and stack removed).") unless term_script
523
+ else
524
+ Blufin::Terminal::error('Something went wrong.')
525
+ end
526
+ else
527
+ Blufin::Terminal::success('Stack creation was successful.') unless term_script
528
+ end
408
529
  end
409
530
  end
410
531
 
@@ -412,39 +533,47 @@ module AppCommand
412
533
 
413
534
  # Show prompt(s) to select template.
414
535
  # @return void
415
- def select_template_prompt
536
+ def select_template_prompt(first_time = true)
416
537
  if @opts[:test]
417
538
  category = TEST
418
539
  template = TEST
419
540
  else
420
541
  # Get a list of categories which have at least 1 valid template.
421
- categories = []
422
- @templates.keys.each do |template_category|
423
- next if template_category == TEST
424
- has_valid = false
425
- @templates[template_category].each { |template| has_valid = true unless template[1][:broken] }
426
- categories << template_category if has_valid
427
- end
428
- # Bomb-out if all templates are broken. This will happen a lot during dev.
429
- Blufin::Terminal::error('No valid templates.', 'Please fix the above error(s) and try again.', true) unless categories.any?
430
- # Show a prompt identical to the 2nd one, except without a template selected.
431
- Blufin::Terminal::custom('CLOUDFORMATION', 55)
432
- # Select category prompt.
433
- category = Blufin::Terminal::prompt_select('Select Category:', categories)
434
- options = []
435
- @templates[category].each do |key, data|
436
- option = {
437
- :text => "#{data[:name]}\x1B[38;5;246m\x1B[0m",
438
- :value => data[:name]
542
+ template = nil
543
+ while template == RETURN_VALUE || template.nil? do
544
+ system('clear') if !first_time || template == RETURN_VALUE
545
+ categories = []
546
+ @templates.keys.each do |template_category|
547
+ next if template_category == TEST
548
+ has_valid = false
549
+ @templates[template_category].each { |template| has_valid = true unless template[1][:broken] }
550
+ categories << template_category if has_valid
551
+ end
552
+ # Bomb-out if all templates are broken. This will happen a lot during dev.
553
+ Blufin::Terminal::error('No valid templates.', 'Please fix the above error(s) and try again.', true) unless categories.any?
554
+ # Show a prompt identical to the 2nd one, except without a template selected.
555
+ Blufin::Terminal::custom('CLOUDFORMATION', 55, nil, nil, !(@warnings.any? && first_time))
556
+ # Select category prompt.
557
+ category = Blufin::Terminal::prompt_select('Select Category:', categories)
558
+ options = []
559
+ @templates[category].each do |key, data|
560
+ option = {
561
+ :text => "#{data[:name]}\x1B[38;5;246m\x1B[0m",
562
+ :value => data[:name]
563
+ }
564
+ option[:disabled] = data[:broken] if data[:broken]
565
+ options << option
566
+ end
567
+ options << {
568
+ :text => "\x1B[38;5;240m#{Blufin::Strings::RETURN_CHARACTER}\x1B[0m",
569
+ :value => RETURN_VALUE
439
570
  }
440
- option[:disabled] = data[:broken] if data[:broken]
441
- options << option
571
+ system('clear')
572
+ # Show a prompt identical to the 2nd one, now with category selected.
573
+ Blufin::Terminal::custom('CLOUDFORMATION', 55, "Selected template: #{App::AWSOutputter::render_selection(category)}")
574
+ # Select template prompt.
575
+ template = Blufin::Terminal::prompt_select('Select Template:', options)
442
576
  end
443
- system('clear')
444
- # Show a prompt identical to the 2nd one, now with category selected.
445
- Blufin::Terminal::custom('CLOUDFORMATION', 55, "Selected template: #{Blufin::Terminal::format_action(category)}")
446
- # Select template prompt.
447
- template = Blufin::Terminal::prompt_select('Select Template:', options)
448
577
  # If template is broken, bomb-out (in theory, a broken template should never be selectable.).
449
578
  raise RuntimeError, "Template with category: #{category} and Id: #{template} does not exist." unless @templates.has_key?(category) && @templates[category].has_key?(template)
450
579
  Blufin::Terminal::error("Template: #{Blufin::Terminal::format_highlight(@templates[category][template][:name])} is currently broken/incomplete.", 'Please fix error(s) and try again.', true) if @templates[category][template][:broken]
@@ -460,44 +589,69 @@ module AppCommand
460
589
  template_intro = @template[:intro].split("\n")
461
590
  template_intro << ''
462
591
  end
592
+ unless template_intro.any?
593
+ if @template.has_key?(:description) && !@template[:description].nil? && @template[:description].to_s.strip != ''
594
+ template_intro << @template[:description]
595
+ template_intro << ''
596
+ end
597
+ end
463
598
  params_to_fetch = []
464
599
  params_to_fetch_colored = []
465
600
  # Output the title + intro.
466
- Blufin::Terminal::custom('CLOUDFORMATION', 55, "Selected template: #{render_category_template(category, template)}")
467
- # TODO - This is where you would put more info about the template (either do later or remove this TODO).
468
- # puts "\x1B[38;5;240m Stack-name: \x1B[38;5;30m#{@template[:stack_name]}\x1B[0m"
469
- # puts
470
- template_intro.each { |line| puts "\x1B[38;5;240m #{line}\x1B[0m" }
601
+ Blufin::Terminal::custom('CLOUDFORMATION', 55, "Selected template: #{App::AWSOutputter::render_selection(category, @template[:single_serve] ? "#{template} 💥 " : template)}")
602
+ template_intro.each { |line| puts "\x1B[38;5;246m #{line}\x1B[0m" }
471
603
  # Prepare options. Merge custom with defaults.
472
604
  options_current = {}
473
605
  @options_default.each do |key, default_options|
474
606
  options_current[OPTION_PROJECT] = (@template[:projects].nil? ? default_options : @template[:projects]) if key == OPTION_PROJECT
475
607
  options_current[OPTION_ENVIRONMENT] = (@template[:environments].nil? ? default_options : @template[:environments]) if key == OPTION_ENVIRONMENT
476
608
  options_current[OPTION_REGION] = (@template[:regions].nil? ? default_options : @template[:regions]) if key == OPTION_REGION
609
+ options_current[OPTION_TIMEOUT] = (@template[:timeout].nil? ? default_options : @template[:timeout]) if key == OPTION_TIMEOUT
477
610
  end
478
611
  @options_default.keys.each do |key|
479
- next if key == OPTION_STACK_NAME
612
+ next if [OPTION_STACK_NAME, OPTION_TIMEOUT].include?(key)
480
613
  @template[:parameters][key] = {
481
- 'Type' => 'String',
482
- 'Description' => "%w(#{options_current[key].join(' ')})",
483
- OPTIONS => options_current[key],
614
+ 'Type' => 'String',
615
+ OPTION_DESCRIPTION => "[#{options_current[key].join(',')}]",
616
+ OPTIONS => options_current[key],
484
617
  }
485
618
  end
486
- @template[:parameters][OPTION_STACK_NAME] = {
487
- 'Type' => 'String',
488
- 'Description' => @template[:stack_name].nil? ? default_options : @template[:stack_name],
489
- 'MinLength' => 1
619
+ unless @template[:deployment_stack].nil?
620
+ @template[:parameters][OPTION_PROJECT_ID] = {
621
+ 'Type' => 'String',
622
+ OPTION_DESCRIPTION => 'Project ID (used for associating this Stack with an existing Project).'
623
+ }
624
+ end
625
+ @template[:parameters][OPTION_STACK_NAME] = {
626
+ 'Type' => 'String',
627
+ OPTION_DESCRIPTION => @template[:stack_name].nil? ? default_options : @template[:stack_name],
628
+ 'MinLength' => 1
629
+ }
630
+ @template[:parameters][OPTION_DESCRIPTION] = {
631
+ 'Type' => 'String',
632
+ OPTION_DESCRIPTION => @template[:description],
633
+ 'MinLength' => 1
490
634
  }
491
- @template[:parameters][OPTION_DESCRIPTION] = {
492
- 'Type' => 'String',
493
- 'Description' => 'A quick note to describe this stack (displayed in CloudFormation console).',
494
- 'MinLength' => 1
635
+ # Add default if Description exists in template.yml (must come after above line).
636
+ @template[:parameters][OPTION_DESCRIPTION][DEFAULT] = @template[:description] if @template.has_key?(:description) && !@template[:description].nil? && @template[:description].to_s.strip != ''
637
+ @template[:parameters][OPTION_TIMEOUT] = {
638
+ 'Type' => 'Number',
639
+ OPTION_DESCRIPTION => 'The amount of minutes that can pass before the stack aborts the mission.',
640
+ DEFAULT => options_current[OPTION_TIMEOUT]
495
641
  }
642
+ unless @template[:single_serve]
643
+ @template[:parameters][OPTION_TERM_PROTECT] = {
644
+ 'Type' => 'Boolean',
645
+ OPTION_DESCRIPTION => 'Should this stack be protected against accidental Termination?'
646
+ }
647
+ end
496
648
  # Get cached values (if exist and parameters haven't changed).
497
649
  # Even a one-character change in a description will invalidate the cache.
498
- cache_file = get_cache_file(category, template)
499
- @cache = {}
650
+ cache_file = get_cache_file(category, template)
651
+ @cache = {}
500
652
  if Blufin::Files::file_exists(cache_file)
653
+ @cache = nil
654
+ @cache_valid = false
501
655
  begin
502
656
  @cache = JSON.parse(`cat #{cache_file}`)
503
657
  rescue
@@ -505,9 +659,7 @@ module AppCommand
505
659
  end
506
660
  # Makes sure non of the parameter definitions changed.
507
661
  if @cache.has_key?(CACHE_UUID)
508
- @cache = {} if @cache[CACHE_UUID] != get_cache_hexdigest(@template[:parameters])
509
- else
510
- @cache = {}
662
+ @cache_valid = true if @cache[CACHE_UUID] == get_cache_hexdigest(@template[:parameters])
511
663
  end
512
664
  end
513
665
  # Output the parameters (in table).
@@ -523,7 +675,7 @@ module AppCommand
523
675
  table_data << {
524
676
  :type_internal => @auto_fetch_resources.has_key?(param_name) ? :autocomplete : ti,
525
677
  :parameter => param_name,
526
- :type => %w(string number).include?(param_data['Type'].downcase) ? param_data['Type'] : SPECIAL,
678
+ :type => %w(string number boolean).include?(param_data['Type'].downcase) ? param_data['Type'] : SPECIAL,
527
679
  :description => param_data.has_key?('Description') ? param_data['Description'] : "\xe2\x80\x94",
528
680
  :allowed_pattern => param_data.has_key?('AllowedPattern') ? param_data['AllowedPattern'] : nil,
529
681
  :min_length => param_data.has_key?('MinLength') ? param_data['MinLength'] : nil,
@@ -553,7 +705,7 @@ module AppCommand
553
705
  }
554
706
  end
555
707
  sleep(0.1)
556
- Blufin::Terminal::execute_proc("AWS - Getting parameter data: [ #{params_to_fetch_colored.join(' | ')}\x1B[38;5;208m ]", Proc.new {
708
+ Blufin::Terminal::execute_proc("AWS \xe2\x80\x94 Getting parameter data: [ #{params_to_fetch_colored.join(' | ')}\x1B[38;5;208m ]", Proc.new {
557
709
  threads.each { |thread| thread.join }
558
710
  })
559
711
  puts
@@ -564,12 +716,11 @@ module AppCommand
564
716
  puts
565
717
  choices = []
566
718
  else
567
- choices = @cache.any? ? [{value: 'Y', text: "Select this template \x1B[38;5;198m(and apply cached values)\x1B[0m"}] : []
568
- choices << {value: 'y', text: 'Select this template'}
719
+ choices = [{ value: 'y', text: 'Select this template' }]
720
+ choices << { value: 'Y', text: "Select this template \x1B[38;5;198m(and apply cached values)\x1B[0m" } if @cache_valid
569
721
  end
570
722
  # The prompt at the end of the intro.
571
- choices << {value: 'n', text: 'Select another template'}
572
- choices << {value: 'q', text: 'Quit'}
723
+ choices << { value: 'n', text: "\x1B[38;5;240m#{Blufin::Strings::RETURN_CHARACTER}\x1B[0m" }
573
724
  choice = Blufin::Terminal::prompt_select('What would you like to do?', choices)
574
725
  case choice
575
726
  when 'y'
@@ -580,10 +731,7 @@ module AppCommand
580
731
  return [category, template, @template]
581
732
  when 'n'
582
733
  system('clear')
583
- select_template_prompt
584
- when 'q'
585
- puts
586
- exit
734
+ select_template_prompt(false)
587
735
  else
588
736
  raise RuntimeError, "Unhandled choice: #{choice}"
589
737
  end
@@ -592,31 +740,60 @@ module AppCommand
592
740
  # Shows a parameter and gets input from user.
593
741
  # @return void
594
742
  def get_parameter_value(param_data, param_name, category, template)
595
- description = param_data.has_key?(DESCRIPTION) ? param_data[DESCRIPTION] : nil
743
+ description = param_data.has_key?(OPTION_DESCRIPTION) ? param_data[OPTION_DESCRIPTION] : nil
596
744
  options_text = "Select #{param_name}:"
597
- if param_name == OPTION_STACK_NAME
745
+ if [OPTION_STACK_NAME].include?(param_name)
746
+ constraints = []
747
+ constraints << "\x1B[38;5;240mMinLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinLength']}" if param_data.has_key?('MinLength')
748
+ constraints << "\x1B[38;5;240mMaxLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxLength']}" if param_data.has_key?('MaxLength')
749
+ default = @template[:stack_name]
750
+ default = default.gsub(/{{CATEGORY}}/i, category) if default =~ /{{CATEGORY}}/i
751
+ default = default.gsub(/{{TEMPLATE}}/i, template) if default =~ /{{TEMPLATE}}/i
752
+ default = default.gsub(/{{PROJECT}}/i, @params[OPTION_PROJECT]) if default =~ /{{PROJECT}}/i
753
+ default = default.gsub(/{{ENVIRONMENT}}/i, @params[OPTION_ENVIRONMENT]) if default =~ /{{ENVIRONMENT}}/i
754
+ default = default.gsub(/{{REGION}}/i, @params[OPTION_REGION]) if default =~ /{{REGION}}/i
755
+ default = default.gsub(/{{UUID}}/i, random_stack_suffix) if default =~ /{{UUID}}/i
756
+ default = default.downcase
757
+ help_text = 'Stack Name (will be displayed in CloudFormation console).'
758
+ return Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: help_text)
759
+ elsif param_name == OPTION_DESCRIPTION
598
760
  constraints = []
599
761
  constraints << "\x1B[38;5;240mMinLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinLength']}" if param_data.has_key?('MinLength')
600
- default = @template[:stack_name]
601
- default = default.gsub(/\{\{CATEGORY\}\}/i, category) if default =~ /\{\{CATEGORY\}\}/i
602
- default = default.gsub(/\{\{TEMPLATE\}\}/i, template) if default =~ /\{\{TEMPLATE\}\}/i
603
- default = default.gsub(/\{\{PROJECT\}\}/i, @params[OPTION_PROJECT]) if default =~ /\{\{PROJECT\}\}/i
604
- default = default.gsub(/\{\{ENVIRONMENT\}\}/i, @params[OPTION_ENVIRONMENT]) if default =~ /\{\{ENVIRONMENT\}\}/i
605
- default = default.gsub(/\{\{REGION\}\}/i, @params[OPTION_REGION]) if default =~ /\{\{REGION\}\}/i
606
- default = default.gsub(/\{\{UUID\}\}/i, SecureRandom.uuid.split('-')[0]) if default =~ /\{\{UUID\{\{/i
607
- default = default.downcase
608
- return Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: 'The name of the Stack (as displayed in CloudFormation console).')
762
+ constraints << "\x1B[38;5;240mMaxLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxLength']}" if param_data.has_key?('MaxLength')
763
+ default = param_data.has_key?(DEFAULT) ? param_data[DEFAULT] : nil
764
+ unless default.nil?
765
+ # Check if description has any replaceable values.
766
+ default = replace_matchers_with_params(default, @params)
767
+ end
768
+ help_text = 'Description (will be displayed in CloudFormation console).'
769
+ return Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: help_text)
770
+ elsif param_name == OPTION_TIMEOUT
771
+ return param_data[DEFAULT]
772
+ elsif param_name == OPTION_TERM_PROTECT
773
+ return Blufin::Terminal::prompt_yes?('Protect against accidental Termination?')
609
774
  elsif @auto_fetch_resources.has_key?(param_name)
775
+ # Sort alphabetically.
610
776
  options = fetch_autocomplete_options(param_name, silent: true)
777
+ options.sort_by! { |hsh| hsh[:sort] }
778
+ # If we have a cached value, make that the first in the options list.
779
+ options = move_default_option_to_top(options, param_name) if @cache.has_key?(param_name)
611
780
  return Blufin::Terminal::prompt_select(options_text, options, help: description)
612
- elsif param_data.has_key?(OPTIONS)
613
- raise RuntimeError, "Expected Array, instead got: #{param_data[OPTIONS].class}" unless param_data[OPTIONS].is_a?(Array)
614
- if param_data[OPTIONS].length == 1
615
- default_option = param_data[OPTIONS][0]
616
- puts "#{Blufin::Terminal::display_prompt_text(options_text)} \x1B[38;5;46m#{default_option}\x1B[0m"
781
+ elsif param_data.has_key?(OPTIONS) || param_data.has_key?('AllowedValues')
782
+ options = param_data[OPTIONS]
783
+ options = param_data['AllowedValues'] if param_data.has_key?('AllowedValues')
784
+ raise RuntimeError, "Expected Array, instead got: #{options.class} (#{options.inspect})" unless options.is_a?(Array)
785
+ if options.length == 1
786
+ default_option = options[0]
787
+ puts Blufin::Terminal::display_prompt_text(options_text, default_option)
617
788
  return default_option
618
789
  else
619
- return Blufin::Terminal::prompt_select(options_text, param_data[OPTIONS])
790
+ # Sort alphabetically.
791
+ options.uniq!
792
+ options.sort!
793
+ # If we have a cached value, make that the first in the options list.
794
+ options = move_default_option_to_top(options, param_name) if @cache.has_key?(param_name)
795
+ puts Blufin::Terminal::display_prompt_help(param_data[OPTION_DESCRIPTION]) if param_data.has_key?(OPTION_DESCRIPTION)
796
+ return Blufin::Terminal::prompt_select(options_text, options)
620
797
  end
621
798
  else
622
799
  constraints = []
@@ -625,19 +802,28 @@ module AppCommand
625
802
  constraints << "\x1B[38;5;240mMinValue: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinValue']}" if param_data.has_key?('MinValue')
626
803
  constraints << "\x1B[38;5;240mMaxLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxLength']}" if param_data.has_key?('MaxLength')
627
804
  constraints << "\x1B[38;5;240mMaxValue: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxValue']}" if param_data.has_key?('MaxValue')
628
- default = param_data.has_key?('Default') ? param_data['Default'] : nil
805
+ default = param_data.has_key?(DEFAULT) ? param_data[DEFAULT] : nil
806
+ if default.nil?
807
+ default = @cache[param_name] if @cache.has_key?(param_name)
808
+ else
809
+ if default =~ /{{(System):([A-Za-z0-9]+)}}/
810
+ matches = default.scan(/{{(System):([A-Za-z0-9]+)}}/)
811
+ default = replace_with_dynamic_data(default, matches) if matches.any?
812
+ end
813
+ end
629
814
  loop do
630
815
  value = Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: description)
816
+ value = value.nil? ? '' : value
631
817
  value_invalid = false
632
818
  value_invalid = true if param_data.has_key?('AllowedPattern') && value !~ /#{param_data['AllowedPattern']}/
633
819
  value_invalid = true if param_data['Type'] == 'Number' && value.to_s !~/^\d+(\.\d+)?$/
634
- value_invalid = true if param_data.has_key?('MinLength') && value.to_s.length < param_data['MinLength']
635
- value_invalid = true if param_data.has_key?('MaxLength') && value.to_s.length > param_data['MaxLength']
636
- value_invalid = true if param_data.has_key?('MinValue') && value.to_i < param_data['MinValue']
637
- value_invalid = true if param_data.has_key?('MaxValue') && value.to_i > param_data['MaxValue']
820
+ value_invalid = true if param_data.has_key?('MinLength') && value.to_s.length < param_data['MinLength'].to_i
821
+ value_invalid = true if param_data.has_key?('MaxLength') && value.to_s.length > param_data['MaxLength'].to_i
822
+ value_invalid = true if param_data.has_key?('MinValue') && value.to_i < param_data['MinValue'].to_i
823
+ value_invalid = true if param_data.has_key?('MaxValue') && value.to_i > param_data['MaxValue'].to_i
638
824
  if value_invalid
639
825
  description = nil
640
- puts "\x1B[38;5;196mValue does not meet allowed constraint(s).\x1B[0m"
826
+ puts "\x1B[38;5;196mValue does not meet allowed constraint(s): #{value.inspect}\x1B[0m"
641
827
  else
642
828
  return value
643
829
  end
@@ -646,6 +832,53 @@ module AppCommand
646
832
  puts
647
833
  end
648
834
 
835
+ # TODO - Think about possible combining both of the below methods?
836
+
837
+ # Takes something like "{{System:RandomString}}" and returns "aif8d5njk".
838
+ # @return string
839
+ def replace_with_dynamic_data(original_string, matcher_pairs)
840
+ raise RuntimeError, "Expected Array, instead got: #{matcher_pairs.class}" unless matcher_pairs.is_a?(Array)
841
+ matcher_pairs.each do |pair|
842
+ matcher = "#{pair[0]}:#{pair[1]}"
843
+ case matcher
844
+ when 'System:RandomString'
845
+ original_string = original_string.gsub('{{System:RandomString}}', Blufin::Strings::random_string)
846
+ else
847
+ raise RuntimeError, "Unrecognized matcher: #{Blufin::Terminal::format_invalid(matcher)}"
848
+ end
849
+ end
850
+ original_string
851
+ end
852
+
853
+ # Takes a string like: '{{Subdomain}}.{{Route53DomainName}}' and converts it to -> nuxt-v1.blufin.org
854
+ # @return void
855
+ def replace_matchers_with_params(default, params)
856
+ unless default.nil? || !default.is_a?(String)
857
+ matches = default.scan(/{{[A-Za-z0-9]+}}/)
858
+ matches.each do |match|
859
+ match = match.gsub(/^{{/, '').gsub(/}}$/, '')
860
+ default = default.gsub(/{{#{match}}}/, params[match]) if params.has_key?(match)
861
+ end
862
+ end
863
+ default
864
+ end
865
+
866
+ # Moves the default option to the top of the list.
867
+ # @return Array
868
+ def move_default_option_to_top(options, param_name)
869
+ options_rearranged = []
870
+ options.each do |option|
871
+ if option.is_a?(Hash)
872
+ options_rearranged.unshift(option) if option[:value] == @cache[param_name]
873
+ options_rearranged << option unless option[:value] == @cache[param_name]
874
+ else
875
+ options_rearranged.unshift(option) if option == @cache[param_name]
876
+ options_rearranged << option unless option == @cache[param_name]
877
+ end
878
+ end
879
+ options_rearranged
880
+ end
881
+
649
882
  # Goes off to AWS and gets values for autocomplete supported fields.
650
883
  # @return string
651
884
  def fetch_autocomplete_options(resource_name, silent: true)
@@ -654,7 +887,7 @@ module AppCommand
654
887
  resource_title = @auto_fetch_resources[resource_name][:resource_title]
655
888
  resource = @auto_fetch_resources[resource_name][:resource]
656
889
  results = App::AWSReports::get_aws_data(@regions, resource, resource_title, silent: silent)
657
- options = App::AWSReports::parse_results_for_prompt(resource, results)
890
+ options = App::AWSReports::parse_results_for_prompt(resource, resource_name, results)
658
891
  @auto_fetch_cache[resource_name] = options
659
892
  options
660
893
  end
@@ -664,12 +897,22 @@ module AppCommand
664
897
  def assemble_tags(params)
665
898
  output = []
666
899
  params.each do |key, value|
667
- next unless OPTION_TAGS.include?(key.downcase)
900
+ next unless AWS_TAGS.include?(key)
668
901
  output << {
669
902
  'Key' => key,
670
903
  'Value' => value
671
904
  }
672
905
  end
906
+ if @template.has_key?(:deployment_stack) && params.has_key?(OPTION_PROJECT_ID)
907
+ output << {
908
+ 'Key' => OPTION_DEPLOYMENT_STACK,
909
+ 'Value' => @template[:deployment_stack].to_s
910
+ }
911
+ output << {
912
+ 'Key' => OPTION_PROJECT_ID,
913
+ 'Value' => params[OPTION_PROJECT_ID]
914
+ }
915
+ end
673
916
  output.to_json
674
917
  end
675
918
 
@@ -678,8 +921,7 @@ module AppCommand
678
921
  def assemble_params(params)
679
922
  output = []
680
923
  params.each do |key, value|
681
- next if OPTION_STACK_NAME.downcase == key.downcase
682
- unless OPTION_TAGS.include?(key.downcase)
924
+ unless RESERVED_WORDS.include?(key.downcase)
683
925
  output << {
684
926
  'ParameterKey' => key,
685
927
  'ParameterValue' => value
@@ -711,13 +953,14 @@ module AppCommand
711
953
  # @return string
712
954
  def get_cache_hexdigest(parameter_hash)
713
955
  raise RuntimeError, "Expected Hash, instead got #{parameter_hash.class}" unless parameter_hash.is_a?(Hash)
714
- Digest::SHA2.hexdigest(parameter_hash.to_s)
715
- end
716
-
717
- # Standardized way of rendering category/template output.
718
- # @return string
719
- def render_category_template(category, template)
720
- "#{Blufin::Terminal::format_action(category)} \x1B[38;5;240m[#{Blufin::Terminal::format_highlight(template)}\x1B[38;5;240m]"
956
+ parameter_hash_standardized = {}
957
+ parameter_hash.each do |k, v|
958
+ parameter_hash_standardized[k] = v
959
+ if v.has_key?(OPTIONS)
960
+ parameter_hash_standardized[k][OPTIONS].sort!.uniq!
961
+ end
962
+ end
963
+ Digest::SHA2.hexdigest(parameter_hash_standardized.to_s)
721
964
  end
722
965
 
723
966
  # Standardized way of rendering constraints output.
@@ -727,6 +970,18 @@ module AppCommand
727
970
  constraints.any? ? " \x1B[38;5;#{constraint_bracket_color}m[ #{constraints.join("\x1B[38;5;240m | ")} \x1B[38;5;#{constraint_bracket_color}m] \x1B[38;5;214m:" : ':'
728
971
  end
729
972
 
973
+ # Generates a random stack suffix that always has the same patters: a[a-z0-9]w[a-z0-9]x
974
+ # @return string
975
+ def random_stack_suffix
976
+ "a#{Blufin::Strings::random_string}10w17#{Blufin::Strings::random_string}x"
977
+ end
978
+
979
+ # Replaces current random stack suffix with another one.
980
+ # @return string
981
+ def replace_stack_suffix(current)
982
+ current.gsub(/a[0-9a-z]{8}10w17[0-9a-z]{8}x/, random_stack_suffix)
983
+ end
984
+
730
985
  end
731
986
 
732
987
  end