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