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.
- checksums.yaml +4 -4
- data/lib/aws/aws.rb +9 -0
- data/lib/aws/aws_cli.rb +36 -30
- data/lib/aws/aws_cloudformation.rb +59 -36
- data/lib/aws/aws_outputter.rb +33 -2
- data/lib/aws/aws_profile.rb +116 -0
- data/lib/aws/aws_reports.rb +47 -26
- data/lib/awx.rb +98 -45
- data/lib/core/opt.rb +1 -1
- data/lib/routes/aws_cloudformation_create.rb +447 -192
- data/lib/routes/aws_cloudformation_detect_drift.rb +33 -7
- data/lib/routes/aws_deploy.rb +486 -0
- data/lib/routes/aws_dynamo_db.rb +43 -0
- data/lib/routes/aws_list.rb +17 -11
- data/lib/routes/aws_switch.rb +76 -0
- data/lib/version.rb +1 -1
- data/opt/{yml/aws-reports.yml → awx/reports.yml} +15 -12
- data/opt/config/schema.yml +63 -0
- data/opt/config/template.yml +21 -0
- metadata +25 -23
- data/lib/aws/aws_config.rb +0 -39
- data/lib/core/config.rb +0 -127
- data/lib/core/config_unique.rb +0 -64
- data/lib/routes/aws_lambda.rb +0 -122
- data/lib/routes/setup.rb +0 -31
data/lib/aws/aws_reports.rb
CHANGED
@@ -12,13 +12,15 @@ module App
|
|
12
12
|
KEY_REGIONS = 'regions'
|
13
13
|
KEY_REGIONS_PREFERRED = 'regionsPreferred'
|
14
14
|
|
15
|
-
YML_FILE = '
|
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::
|
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
|
-
|
105
|
-
raise RuntimeError, "Expected #{
|
106
|
-
raise RuntimeError, "#{resource}
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
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
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
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[
|
199
|
-
value = App::AWSOutputter::get_formatter(
|
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[
|
204
|
-
value = App::AWSOutputter::get_formatter(
|
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 =>
|
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/
|
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 =~ /\/(
|
12
|
-
Dir["#{File.dirname(__FILE__)}/core/**/*.rb"].each { |file| load(file) unless file =~ /\/(
|
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
|
22
|
-
|
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
|
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
|
-
#
|
32
|
-
|
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
|
-
|
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
|
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("#{
|
79
|
+
system("#{App::GEM_NAME} c -h")
|
62
80
|
end
|
63
81
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
opts
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
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 -
|
97
|
-
awx.command :
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
#
|
147
|
+
# awx - DEFAULT
|
105
148
|
awx.action do
|
106
|
-
system("#{
|
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
@@ -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
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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 = {}
|
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
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
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
|
-
|
122
|
-
|
123
|
-
|
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(
|
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[
|
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(
|
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
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
263
|
-
stack_name
|
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
|
-
|
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].
|
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
|
-
|
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
|
-
|
350
|
-
|
351
|
-
|
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?(
|
358
|
-
capabilities_arr = hash[
|
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
|
-
|
366
|
-
|
367
|
-
|
368
|
-
output
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
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
|
-
|
387
|
-
|
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
|
-
@
|
390
|
-
|
391
|
-
|
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
|
-
|
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,
|
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 :
|
516
|
+
# Call :after_create() -- if exists.
|
404
517
|
@template[:method_after_create].call(@params) unless @template[:method_after_create].nil?
|
405
518
|
|
406
|
-
# Success
|
407
|
-
|
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
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
@templates
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
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
|
-
|
441
|
-
|
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: #{
|
467
|
-
|
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
|
612
|
+
next if [OPTION_STACK_NAME, OPTION_TIMEOUT].include?(key)
|
480
613
|
@template[:parameters][key] = {
|
481
|
-
'Type'
|
482
|
-
|
483
|
-
OPTIONS
|
614
|
+
'Type' => 'String',
|
615
|
+
OPTION_DESCRIPTION => "[#{options_current[key].join(',')}]",
|
616
|
+
OPTIONS => options_current[key],
|
484
617
|
}
|
485
618
|
end
|
486
|
-
@template[:
|
487
|
-
|
488
|
-
|
489
|
-
|
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
|
-
|
492
|
-
|
493
|
-
|
494
|
-
'
|
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
|
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
|
-
@
|
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
|
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 =
|
568
|
-
choices << {value: '
|
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:
|
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?(
|
743
|
+
description = param_data.has_key?(OPTION_DESCRIPTION) ? param_data[OPTION_DESCRIPTION] : nil
|
596
744
|
options_text = "Select #{param_name}:"
|
597
|
-
if param_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
|
-
|
601
|
-
default =
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
default
|
608
|
-
|
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
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
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
|
-
|
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?(
|
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)
|
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
|
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
|
-
|
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
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
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
|