awx 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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