awx 0.1.0 → 0.2.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_cache.rb +53 -0
- data/lib/aws/aws_cli.rb +166 -0
- data/lib/aws/aws_cloudformation.rb +68 -0
- data/lib/aws/aws_config.rb +39 -0
- data/lib/aws/aws_credentials.rb +9 -0
- data/lib/aws/aws_outputter.rb +247 -0
- data/lib/aws/aws_reports.rb +271 -0
- data/lib/aws/aws_validator.rb +30 -0
- data/lib/awx.rb +65 -71
- data/lib/core/config.rb +127 -0
- data/lib/core/config_unique.rb +64 -0
- data/lib/core/opt.rb +17 -0
- data/lib/routes/aws_cloudformation_create.rb +732 -0
- data/lib/routes/aws_cloudformation_delete.rb +37 -0
- data/lib/routes/aws_cloudformation_detect_drift.rb +44 -0
- data/lib/routes/aws_lambda.rb +122 -0
- data/lib/routes/aws_list.rb +234 -0
- data/lib/routes/setup.rb +31 -0
- data/lib/version.rb +1 -1
- data/opt/yml/aws-reports.yml +113 -0
- metadata +28 -10
data/lib/core/config.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'parseconfig'
|
3
|
+
|
4
|
+
module App
|
5
|
+
|
6
|
+
class Config
|
7
|
+
|
8
|
+
@params = {}
|
9
|
+
|
10
|
+
extend ConfigUnique
|
11
|
+
include ConfigUnique
|
12
|
+
|
13
|
+
def self.initialize
|
14
|
+
if config_file_exists?
|
15
|
+
run_load_config
|
16
|
+
else
|
17
|
+
run_first_journey
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config_file_exists?
|
22
|
+
unless File.exists? ("#{File.expand_path(CONFIG_FILE)}")
|
23
|
+
return false
|
24
|
+
end
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.run_load_config
|
29
|
+
config_params_get
|
30
|
+
config_params_validate
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.run_first_journey
|
34
|
+
first_journey_message
|
35
|
+
config_file_create
|
36
|
+
config_file_edit
|
37
|
+
config_params_get
|
38
|
+
config_params_validate(true)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.first_journey_message
|
42
|
+
puts
|
43
|
+
puts "Thank you for installing the #{Blufin::Terminal::format_highlight("#{GEM_NAME}-cli")} ruby gem."
|
44
|
+
puts "CLI stands for 'Command Line Interface'."
|
45
|
+
puts
|
46
|
+
puts "The first thing you'll need to do is setup your configuration file."
|
47
|
+
puts "The file is located at: #{Blufin::Terminal::format_directory(ConfigUnique::CONFIG_FILE)}"
|
48
|
+
puts
|
49
|
+
puts "You probably won't have this file so the program will create it for you."
|
50
|
+
puts "\n"
|
51
|
+
unless Blufin::Terminal::prompt_yes_no('Create configuration file?')
|
52
|
+
Blufin::Terminal::abort
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.config_file_edit
|
57
|
+
system("nano #{File.expand_path(CONFIG_FILE)}")
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.config_params_get
|
61
|
+
config = ParseConfig.new("#{File.expand_path(CONFIG_FILE)}")
|
62
|
+
config.get_params.each do |param|
|
63
|
+
@params[param] = config[param]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.config_params_validate(show_success_message = false)
|
68
|
+
error_text = ''
|
69
|
+
missing_keys = get_missing_config_keys
|
70
|
+
if missing_keys.any?
|
71
|
+
missing_keys.each do |key|
|
72
|
+
error_text = "#{error_text}\x1B[38;5;196mKey/value must exist and cannot be null:\x1B[0m \x1B[38;5;240m#{key}\x1B[0m\n"
|
73
|
+
end
|
74
|
+
error_text = error_text[0..-2]
|
75
|
+
end
|
76
|
+
unless error_text == ''
|
77
|
+
show_error_message(error_text)
|
78
|
+
end
|
79
|
+
if show_success_message
|
80
|
+
Blufin::Terminal::success('Configuration parameters are correct.', "You are now ready to start using this utility.\nStart by typing #{Blufin::Terminal::format_command("#{GEM_NAME} --help")} (or #{Blufin::Terminal::format_command("#{GEM_NAME} -h")}).")
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.show_error_message(error_text)
|
86
|
+
Blufin::Terminal::error('Your configuration parameters are invalid.', "#{error_text}\n\nYou can fix this by running #{Blufin::Terminal::format_command("#{GEM_NAME} setup")} (or #{Blufin::Terminal::format_command("#{GEM_NAME} x")}).")
|
87
|
+
exit
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.get_missing_config_keys
|
91
|
+
missing_keys = required_config_keys.dup
|
92
|
+
@params.each do |key, value|
|
93
|
+
unless value == ''
|
94
|
+
missing_keys.delete(key)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
missing_keys
|
98
|
+
end
|
99
|
+
|
100
|
+
# Checks if a config parameter exists.
|
101
|
+
# @return boolean
|
102
|
+
def self.param_exists(param_name)
|
103
|
+
@params.has_key?(param_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Get ALL config parameters.
|
107
|
+
# @return Hash
|
108
|
+
def self.params
|
109
|
+
@params
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get custom keys from the config file (IE: ssh_ec2=user|host|pem)
|
113
|
+
# @return String
|
114
|
+
def self.get_custom_key(prefix, config_key)
|
115
|
+
if config_key.nil?
|
116
|
+
Blufin::Terminal::error("The script requires a config parameter from your #{Blufin::Terminal::format_directory(ConfigUnique::CONFIG_FILE)} file", "The key should have a prefix of: #{Blufin::Terminal::format_highlight(prefix)}", true)
|
117
|
+
end
|
118
|
+
config_param = (config_key =~ /\A#{prefix}\S+\z/i) ? config_key : "#{prefix}#{config_key}"
|
119
|
+
unless App::Config::param_exists(config_param)
|
120
|
+
Blufin::Terminal::error('Invalid config parameter', "Cannot find #{Blufin::Terminal::format_highlight('key')} #{Blufin::Terminal::format_invalid("\"#{config_param}\"")} in: #{Blufin::Terminal::format_directory(ConfigUnique::CONFIG_FILE)}", true)
|
121
|
+
end
|
122
|
+
App::Config.param(config_param)
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ConfigUnique
|
2
|
+
|
3
|
+
@params = {}
|
4
|
+
|
5
|
+
# USED BY CONFIG CLASS
|
6
|
+
CONFIG_FILE = '~/.awxrc'
|
7
|
+
GEM_NAME = 'awx'
|
8
|
+
|
9
|
+
# STORED PARAMETERS
|
10
|
+
PATH_TO_REPO_AWX = 'path_to_repo_awx'
|
11
|
+
PATH_TO_REPO_SECRETS = 'path_to_repo_secrets'
|
12
|
+
PATH_TO_LAMBDA_EWORLD = 'path_to_lambda_eworld'
|
13
|
+
|
14
|
+
def param(param_name)
|
15
|
+
|
16
|
+
raise RuntimeError, "Expected String, instead got:#{param_name.class}" unless param_name.is_a?(String)
|
17
|
+
|
18
|
+
unless [
|
19
|
+
PATH_TO_REPO_AWX,
|
20
|
+
PATH_TO_REPO_SECRETS,
|
21
|
+
PATH_TO_LAMBDA_EWORLD
|
22
|
+
].include?(param_name)
|
23
|
+
unless param_name =~ /^ssh_[a-z_]+$/
|
24
|
+
raise RuntimeError, "#{Blufin::Terminal::format_highlight(param_name)} is not a valid config parameter"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
param_value = @params[param_name]
|
29
|
+
return nil if param_value.nil?
|
30
|
+
|
31
|
+
if [
|
32
|
+
PATH_TO_REPO_AWX,
|
33
|
+
PATH_TO_REPO_SECRETS,
|
34
|
+
PATH_TO_LAMBDA_EWORLD
|
35
|
+
].include?(param_name)
|
36
|
+
begin
|
37
|
+
return "/#{Blufin::Strings.remove_surrounding_slashes(File.expand_path(param_value))}"
|
38
|
+
rescue Exception => e
|
39
|
+
Blufin::Terminal::error("Something went wrong trying to get parameter: #{param_name}", e.message)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
param_value
|
44
|
+
end
|
45
|
+
|
46
|
+
def config_file_create
|
47
|
+
File.open("#{File.expand_path(CONFIG_FILE)}", 'w') { |file|
|
48
|
+
file.write("# CONFIGURATION FILE -- Make sure that ALL parameters are correct before saving.\n")
|
49
|
+
file.write("\n")
|
50
|
+
file.write("#{PATH_TO_REPO_AWX}=\n")
|
51
|
+
file.write("#{PATH_TO_REPO_SECRETS}=\n")
|
52
|
+
file.write("#{PATH_TO_LAMBDA_EWORLD}=\n")
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def required_config_keys
|
57
|
+
[
|
58
|
+
PATH_TO_REPO_AWX,
|
59
|
+
PATH_TO_REPO_SECRETS,
|
60
|
+
PATH_TO_LAMBDA_EWORLD
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/lib/core/opt.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module App
|
2
|
+
|
3
|
+
class Opt
|
4
|
+
|
5
|
+
OPT_PATH_YML = '/opt/yml'
|
6
|
+
|
7
|
+
# Get PATH to opt files.
|
8
|
+
# @return String
|
9
|
+
def self.get_base_path
|
10
|
+
base_path = File.dirname(File.expand_path(__FILE__))
|
11
|
+
base_path = base_path.gsub(/\/\w+\/\w+\z/i, '')
|
12
|
+
base_path
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,732 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module AppCommand
|
6
|
+
|
7
|
+
class AWSCloudFormationCreate < ::Convoy::ActionCommand::Base
|
8
|
+
|
9
|
+
DEFAULT = 'Default'
|
10
|
+
DESCRIPTION = 'Description'
|
11
|
+
PARAMETERS = 'Parameters'
|
12
|
+
SPECIAL_METHODS = [:before_create, :after_create, :before_teardown, :after_teardown]
|
13
|
+
OPTION_TAGS = %w(stackname description environment project region resource cache_uuid)
|
14
|
+
OPTION_STACK_NAME = 'StackName'
|
15
|
+
OPTION_DESCRIPTION = 'Description'
|
16
|
+
OPTION_PROJECT = 'Project'
|
17
|
+
OPTION_ENVIRONMENT = 'Environment'
|
18
|
+
OPTION_REGION = 'Region'
|
19
|
+
MATCHERS = %w(CATEGORY TEMPLATE PROJECT ENVIRONMENT REGION UUID)
|
20
|
+
OPTIONS = 'Options'
|
21
|
+
SPECIAL = 'AWS-specific'
|
22
|
+
CACHE_UUID = 'cache_uuid'
|
23
|
+
TEST = 'test'
|
24
|
+
SPACER = '<<--Spacer-->>'
|
25
|
+
|
26
|
+
def execute
|
27
|
+
|
28
|
+
begin
|
29
|
+
|
30
|
+
@opts = command_options
|
31
|
+
@args = arguments
|
32
|
+
|
33
|
+
@template = nil
|
34
|
+
@templates = {}
|
35
|
+
@params = {}
|
36
|
+
@output = {}
|
37
|
+
@regions = App::AWSCli::get_regions
|
38
|
+
@auto_fetch_resources = {}
|
39
|
+
@auto_fetch_cache = {}
|
40
|
+
@data = nil
|
41
|
+
@table_widths = {}
|
42
|
+
@export_map = {}
|
43
|
+
@columns = {}
|
44
|
+
@options_default = {} # The options from the cloudformation.rb file (default for all).
|
45
|
+
@cache = {}
|
46
|
+
|
47
|
+
@columns, @data, @export_map, @table_widths = App::AWSReports::parse_metadata(@regions)
|
48
|
+
|
49
|
+
opts_validate
|
50
|
+
opts_routing
|
51
|
+
|
52
|
+
rescue => e
|
53
|
+
|
54
|
+
Blufin::Terminal::print_exception(e)
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def opts_validate
|
61
|
+
|
62
|
+
# If Terminal window is smaller than 230, bomb-out.
|
63
|
+
terminal_width_actual = Blufin::Terminal::get_terminal_width
|
64
|
+
terminal_required_width = 227
|
65
|
+
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
|
+
|
67
|
+
@warnings = []
|
68
|
+
@auto_fetch_resources = App::AWSReports::get_auto_fetch_resources(@data)
|
69
|
+
|
70
|
+
# Validate the cloudformation.rb file that sits in the root of: blufin-secrets/cloudformation.
|
71
|
+
cloudformation_defaults_file = "#{App::AWSCloudFormation::get_cloudformation_path}/#{App::AWSCloudFormation::FILE_CLOUDFORMATION_DEFAULTS}"
|
72
|
+
if Blufin::Files::file_exists(cloudformation_defaults_file)
|
73
|
+
require cloudformation_defaults_file
|
74
|
+
expected_constants = %w(STACK_NAME PROJECTS ENVIRONMENTS REGIONS)
|
75
|
+
CloudFormation::Defaults::constants.each do |constant|
|
76
|
+
constant = constant.to_s
|
77
|
+
expected_constants.delete(constant) if expected_constants.include?(constant)
|
78
|
+
@options_default[OPTION_STACK_NAME] = CloudFormation::Defaults::STACK_NAME if constant == 'STACK_NAME'
|
79
|
+
@options_default[OPTION_PROJECT] = CloudFormation::Defaults::PROJECTS if constant == 'PROJECTS'
|
80
|
+
@options_default[OPTION_ENVIRONMENT] = CloudFormation::Defaults::ENVIRONMENTS if constant == 'ENVIRONMENTS'
|
81
|
+
@options_default[OPTION_REGION] = CloudFormation::Defaults::REGIONS if constant == 'REGIONS'
|
82
|
+
end
|
83
|
+
raise RuntimeError, "Missing expected constants: #{expected_constants.join(',')}" if expected_constants.any?
|
84
|
+
else
|
85
|
+
raise RuntimeError, "File does not exist: #{cloudformation_defaults_file}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Loop the entire blufin-secrets/cloudformation path(s) and validate all the template(s).
|
89
|
+
Blufin::Files::get_dirs_in_dir(App::AWSCloudFormation::get_cloudformation_path).each do |path|
|
90
|
+
ps = path.split('/')
|
91
|
+
category = ps[ps.length - 1]
|
92
|
+
template_folders = Blufin::Files::get_dirs_in_dir(path)
|
93
|
+
template_folders.each do |path_inner|
|
94
|
+
ps = path_inner.split('/')
|
95
|
+
template = ps[ps.length - 1]
|
96
|
+
template_name = "#{category}/#{template}"
|
97
|
+
file_cloudformation = nil
|
98
|
+
file_ruby = nil
|
99
|
+
method_before_create = nil
|
100
|
+
method_after_create = nil
|
101
|
+
method_before_teardown = nil
|
102
|
+
method_after_teardown = nil
|
103
|
+
intro = nil
|
104
|
+
stack_name = nil
|
105
|
+
projects = nil
|
106
|
+
environments = nil
|
107
|
+
regions = nil
|
108
|
+
parameters = {}
|
109
|
+
warnings_count = @warnings.length
|
110
|
+
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
|
111
|
+
Blufin::Files::get_files_in_dir(path_inner).each do |file|
|
112
|
+
filename = Blufin::Files::extract_file_name(file)
|
113
|
+
if filename == 'template.yml'
|
114
|
+
begin
|
115
|
+
yml_data = YAML.load_file(File.expand_path(file))
|
116
|
+
rescue Exception => e
|
117
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Unable to parse #{Blufin::Terminal::format_highlight('template.yml')}\x1B[38;5;240m: #{e.message}"
|
118
|
+
next
|
119
|
+
end
|
120
|
+
if yml_data.is_a?(Hash)
|
121
|
+
if yml_data.has_key?(DESCRIPTION)
|
122
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Description must be omitted because this will be handled by the script."
|
123
|
+
end
|
124
|
+
if yml_data.has_key?(PARAMETERS) && yml_data[PARAMETERS].is_a?(Hash) && yml_data[PARAMETERS].length > 0
|
125
|
+
yml_data[PARAMETERS].each do |resource_name, data|
|
126
|
+
# Validate keys are in specific order.
|
127
|
+
expected = {
|
128
|
+
'Type' => true,
|
129
|
+
'Description' => false,
|
130
|
+
'Default' => false,
|
131
|
+
'AllowedPattern' => false,
|
132
|
+
'MinLength' => false,
|
133
|
+
'MinValue' => false,
|
134
|
+
'MaxLength' => false,
|
135
|
+
'MaxValue' => false,
|
136
|
+
'ConstraintDescription' => false,
|
137
|
+
}
|
138
|
+
Blufin::Validate::assert_valid_keys(expected, data.keys, "#{file} \xe2\x86\x92 #{resource_name}")
|
139
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template has reserved parameter: #{Blufin::Terminal::format_invalid(resource_name)}" if [OPTION_STACK_NAME.downcase].concat(OPTION_TAGS).include?(resource_name.downcase)
|
140
|
+
parameters[resource_name] = data
|
141
|
+
if @auto_fetch_resources.has_key?(resource_name)
|
142
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Parameter: #{resource_name} cannot have default value: '#{data['Default']}' because it is a live look-up list (from AWS)." if data.keys.include?('Default')
|
143
|
+
end
|
144
|
+
# Validate parameter type.
|
145
|
+
valid_parameter_types = %w(String Number List<Number> CommaDelimitedList)
|
146
|
+
valid_parameter_regexes = [/^AWS::[A-Za-z0-9]+::[A-Za-z0-9]+::[A-Za-z0-9]+$/, /^List<AWS::[A-Za-z0-9]+::[A-Za-z0-9]+::[A-Za-z0-9]+>$/]
|
147
|
+
unless valid_parameter_types.include?(data['Type'])
|
148
|
+
matches_vpr = false
|
149
|
+
valid_parameter_regexes.each do |vpr|
|
150
|
+
matches_vpr = true if data['Type'] =~ vpr
|
151
|
+
end
|
152
|
+
if matches_vpr
|
153
|
+
constraints = []
|
154
|
+
constraints << 'AllowedPattern' if data.has_key?('AllowedPattern')
|
155
|
+
constraints << 'MinLength' if data.has_key?('MinLength')
|
156
|
+
constraints << 'MaxLength' if data.has_key?('MaxLength')
|
157
|
+
constraints << 'MinValue' if data.has_key?('MinValue')
|
158
|
+
constraints << 'MaxValue' if data.has_key?('MaxValue')
|
159
|
+
if constraints.any?
|
160
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid parameter constraint(s): #{Blufin::Terminal::format_invalid(constraints.join(','))}. These don't apply to AWS-Specific parameters."
|
161
|
+
end
|
162
|
+
else
|
163
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid parameter type: #{Blufin::Terminal::format_invalid(data['Type'])}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
# Can only have Max/MinLength if this is a String.
|
167
|
+
if data.has_key?('MinLength') || data.has_key?('MaxLength') || data.has_key?('AllowedPattern')
|
168
|
+
unless data['Type'] == 'String'
|
169
|
+
constraints = []
|
170
|
+
constraints << 'MinLength' if data.has_key?('MinLength')
|
171
|
+
constraints << 'MaxLength' if data.has_key?('MaxLength')
|
172
|
+
constraints << 'AllowedPattern' if data.has_key?('AllowedPattern')
|
173
|
+
@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 String."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
# Can only have Max/MinValue if this is a Number.
|
177
|
+
if data.has_key?('MinValue') || data.has_key?('MaxValue')
|
178
|
+
unless data['Type'] == 'Number'
|
179
|
+
constraints = []
|
180
|
+
constraints << 'MinValue' if data.has_key?('MinValue')
|
181
|
+
constraints << 'MaxValue' if data.has_key?('MaxValue')
|
182
|
+
@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
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
file_cloudformation = file
|
188
|
+
end
|
189
|
+
elsif filename == 'template.rb'
|
190
|
+
file_ruby = file
|
191
|
+
# Load the template.rb file/class.
|
192
|
+
require file
|
193
|
+
expected_constants = %w(INTRO)
|
194
|
+
Template::constants.each do |constant|
|
195
|
+
constant = constant.to_s
|
196
|
+
expected_constants.delete(constant) if expected_constants.include?(constant)
|
197
|
+
intro = Template::INTRO if constant == 'INTRO'
|
198
|
+
# Optional constants.
|
199
|
+
stack_name = Template::STACK_NAME if constant == 'STACK_NAME'
|
200
|
+
projects = Template::PROJECTS if constant == 'PROJECTS'
|
201
|
+
environments = Template::ENVIRONMENTS if constant == 'ENVIRONMENTS'
|
202
|
+
regions = Template::REGIONS if constant == 'REGIONS'
|
203
|
+
end
|
204
|
+
if stack_name.nil? || stack_name.strip == ''
|
205
|
+
stack_name = @options_default[OPTION_STACK_NAME]
|
206
|
+
else
|
207
|
+
# Validate Stack Name (if exists).
|
208
|
+
matches = stack_name.scan(/{[A-Za-z0-9]+}/)
|
209
|
+
matches.each do |match|
|
210
|
+
match = match.gsub(/^{/, '').gsub(/}$/, '')
|
211
|
+
if match != match.upcase
|
212
|
+
@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
|
+
next
|
214
|
+
end
|
215
|
+
unless MATCHERS.include?(match)
|
216
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid stack-name matcher: #{Blufin::Terminal::format_invalid(match)}"
|
217
|
+
end
|
218
|
+
stack_name_stripped = stack_name.gsub(/{[A-Za-z0-9]+}/, '')
|
219
|
+
if stack_name_stripped !~ /[a-z0-9\-]/
|
220
|
+
@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
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
# If any constants are missing, this should catch it.
|
225
|
+
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
|
+
Template::methods.each do |method|
|
227
|
+
if SPECIAL_METHODS.include?(method)
|
228
|
+
if method == :before_create
|
229
|
+
method_params = Template::method(:before_create).parameters
|
230
|
+
if method_params == [[:req, :params]]
|
231
|
+
method_before_create = Template::method(:before_create)
|
232
|
+
else
|
233
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid #{Blufin::Terminal::format_highlight(':before_create')}\x1B[38;5;240m parameters: #{Blufin::Terminal::format_invalid(method_params.inspect)}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
if method == :after_create
|
237
|
+
method_params = Template::method(:after_create).parameters
|
238
|
+
if method_params == [[:req, :params], [:req, :output]]
|
239
|
+
method_after_create = Template::method(:after_create)
|
240
|
+
else
|
241
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid #{Blufin::Terminal::format_highlight(':after_create')}\x1B[38;5;240m parameters: #{Blufin::Terminal::format_invalid(method_params.inspect)}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
if method == :before_teardown
|
245
|
+
method_params = Template::method(:before_teardown).parameters
|
246
|
+
if method_params == [[:req, :output]]
|
247
|
+
method_before_teardown = Template::method(:before_teardown)
|
248
|
+
else
|
249
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid #{Blufin::Terminal::format_highlight(':before_teardown')}\x1B[38;5;240m parameters: #{Blufin::Terminal::format_invalid(method_params.inspect)}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
if method == :after_teardown
|
253
|
+
method_params = Template::method(:after_teardown).parameters
|
254
|
+
if method_params == [[:req, :output]]
|
255
|
+
method_after_teardown = Template::method(:after_teardown)
|
256
|
+
else
|
257
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid #{Blufin::Terminal::format_highlight(':after_teardown')}\x1B[38;5;240m parameters: #{Blufin::Terminal::format_invalid(method_params.inspect)}"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
intro = nil if !intro.nil? && intro.gsub("\n", '').gsub("\t", '').strip == ''
|
263
|
+
stack_name = nil if !stack_name.nil? && stack_name.gsub("\n", '').gsub("\t", '').strip == ''
|
264
|
+
# Unload the template.rb file/class.
|
265
|
+
Object.send(:remove_const, :Template)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
@warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 The #{Blufin::Terminal::format_highlight('template.yml')}\x1B[38;5;240m is missing, empty or invalid." if file_cloudformation.nil?
|
269
|
+
@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 is missing, empty or invalid." if file_ruby.nil?
|
270
|
+
@templates[category] = {} unless @templates.has_key?(category)
|
271
|
+
if @warnings.length > warnings_count
|
272
|
+
@templates[category][template] = {
|
273
|
+
:name => template,
|
274
|
+
:broken => 'Broken'
|
275
|
+
}
|
276
|
+
else
|
277
|
+
@templates[category][template] = {
|
278
|
+
:name => template,
|
279
|
+
:broken => false,
|
280
|
+
:file_cloudformation => file_cloudformation,
|
281
|
+
:file_ruby => file_ruby,
|
282
|
+
:method_before_create => method_before_create,
|
283
|
+
:method_after_create => method_after_create,
|
284
|
+
:method_before_teardown => method_before_teardown,
|
285
|
+
:method_after_teardown => method_after_teardown,
|
286
|
+
:parameters => parameters,
|
287
|
+
:intro => intro,
|
288
|
+
:stack_name => stack_name,
|
289
|
+
:projects => projects,
|
290
|
+
:environments => environments,
|
291
|
+
:regions => regions,
|
292
|
+
}
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
system('clear')
|
298
|
+
|
299
|
+
if @warnings.any?
|
300
|
+
Blufin::Terminal::warning('Some template(s) had issues:', @warnings)
|
301
|
+
# During test runs, bomb-out if there are warnings (otherwise continue).
|
302
|
+
exit if @opts[:test]
|
303
|
+
end
|
304
|
+
|
305
|
+
end
|
306
|
+
|
307
|
+
def opts_routing
|
308
|
+
|
309
|
+
# Show prompt to select template.
|
310
|
+
category, template, @template = select_template_prompt
|
311
|
+
unless @params.any?
|
312
|
+
if !@template[:parameters].nil? && @template[:parameters].any?
|
313
|
+
@template[:parameters].each do |(param_name, param_data)|
|
314
|
+
@params[param_name] = get_parameter_value(param_data, param_name, category, template)
|
315
|
+
puts
|
316
|
+
end
|
317
|
+
end
|
318
|
+
# Cache the inputted value(s).
|
319
|
+
cache_params = @params
|
320
|
+
cache_params[CACHE_UUID] = get_cache_hexdigest(@template[:parameters])
|
321
|
+
Blufin::Files::write_file(get_cache_file(category, template), [cache_params.to_json])
|
322
|
+
end
|
323
|
+
|
324
|
+
# If this is a test-run, abandon ship here.
|
325
|
+
if @opts[:test]
|
326
|
+
puts
|
327
|
+
puts "\x1B[38;5;196m Exiting because this is only a test-run.\x1B[0m"
|
328
|
+
puts
|
329
|
+
exit
|
330
|
+
end
|
331
|
+
|
332
|
+
# Clear the screen.
|
333
|
+
system('clear')
|
334
|
+
|
335
|
+
capabilities_arr = []
|
336
|
+
capabilities_str = nil
|
337
|
+
|
338
|
+
# Upload the template to S3.
|
339
|
+
App::AWSCloudFormation::upload_cloudformation_template(category, template, @params[OPTION_DESCRIPTION])
|
340
|
+
|
341
|
+
# Validates the template.
|
342
|
+
s3_url = App::AWSCloudFormation::get_cloudformation_s3_bucket_url(category, template)
|
343
|
+
validation = App::AWSCli::cloudformation_stack_validate(@params[OPTION_REGION], s3_url)
|
344
|
+
|
345
|
+
# Check if validation output is JSON (and output appropriate format).
|
346
|
+
begin
|
347
|
+
hash = JSON.parse(validation.to_json)
|
348
|
+
raise RuntimeError, 'Not a Hash' unless hash.is_a?(Hash)
|
349
|
+
puts
|
350
|
+
puts " \x1B[38;5;240mAWS says your template is: \x1B[38;5;40mVALID\x1B[0m"
|
351
|
+
puts
|
352
|
+
|
353
|
+
# TODO - Remove (once dev-done).
|
354
|
+
# Blufin::Terminal::code_highlight(hash.to_yaml, 'yml', 4)
|
355
|
+
|
356
|
+
# Extract required capabilities (if any).
|
357
|
+
if hash.has_key?('Capabilities')
|
358
|
+
capabilities_arr = hash['Capabilities']
|
359
|
+
capabilities_str = "\"#{capabilities_arr.join('" "')}\""
|
360
|
+
end
|
361
|
+
rescue
|
362
|
+
Blufin::Terminal::error("AWS says your template is: \x1B[38;5;196mINVALID", App::AWSCli::format_cli_error(validation), true)
|
363
|
+
end
|
364
|
+
|
365
|
+
# TODO - 2) Must output parameters to have the colon aligned.
|
366
|
+
# TODO - 3) Implement Stack Termination protection (CRUCIAL!!).
|
367
|
+
|
368
|
+
output = {}
|
369
|
+
output['StackName'] = @params[OPTION_STACK_NAME]
|
370
|
+
output['Description'] = @params[OPTION_DESCRIPTION]
|
371
|
+
output['Capabilities'] = capabilities_arr.join(', ') unless capabilities_str.nil?
|
372
|
+
output[SPACER] = true
|
373
|
+
|
374
|
+
@params.each do |key, value|
|
375
|
+
next if OPTION_TAGS.include?(key.downcase)
|
376
|
+
next if OPTION_STACK_NAME.downcase == key.downcase
|
377
|
+
next if OPTION_DESCRIPTION.downcase == key.downcase
|
378
|
+
output[key] = value
|
379
|
+
end
|
380
|
+
|
381
|
+
# TODO NOW - FINISH THIS!
|
382
|
+
puts output.to_yaml
|
383
|
+
exit
|
384
|
+
|
385
|
+
params_output = []
|
386
|
+
params_output << "\x1B[38;5;240mStackName \xe2\x80\x94 \x1B[38;5;40m#{@params[OPTION_STACK_NAME]}"
|
387
|
+
params_output << "\x1B[38;5;240mCapabilities \xe2\x80\x94 \x1B[38;5;40m#{capabilities_arr.join(', ')}" unless capabilities_str.nil?
|
388
|
+
|
389
|
+
@params.each do |key, value|
|
390
|
+
next if OPTION_TAGS.include?(key.downcase)
|
391
|
+
next if OPTION_STACK_NAME.downcase == key.downcase
|
392
|
+
params_output << "\x1B[38;5;240m#{key} \xe2\x80\x94 \x1B[38;5;40m#{value}"
|
393
|
+
end
|
394
|
+
|
395
|
+
if Blufin::Terminal::prompt_yes_no("Deploy: #{render_category_template(category, template)} ?", params_output, 'Create CloudFormation Stack in AWS', false)
|
396
|
+
|
397
|
+
# Call :before_create() -- if exists.
|
398
|
+
@template[:method_before_create].call(@params) unless @template[:method_before_create].nil?
|
399
|
+
|
400
|
+
# Create the Stack.
|
401
|
+
App::AWSCli::cloudformation_stack_create(@params[OPTION_REGION], @params[OPTION_STACK_NAME], s3_url, params: assemble_params(@params), tags: assemble_tags(@params), capabilities: capabilities_arr)
|
402
|
+
|
403
|
+
# Call :before_create() -- if exists.
|
404
|
+
@template[:method_after_create].call(@params) unless @template[:method_after_create].nil?
|
405
|
+
|
406
|
+
# Success message.
|
407
|
+
Blufin::Terminal::success('Stack creation was successful.')
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
private
|
412
|
+
|
413
|
+
# Show prompt(s) to select template.
|
414
|
+
# @return void
|
415
|
+
def select_template_prompt
|
416
|
+
if @opts[:test]
|
417
|
+
category = TEST
|
418
|
+
template = TEST
|
419
|
+
else
|
420
|
+
# Get a list of categories which have at least 1 valid template.
|
421
|
+
categories = []
|
422
|
+
@templates.keys.each do |template_category|
|
423
|
+
next if template_category == TEST
|
424
|
+
has_valid = false
|
425
|
+
@templates[template_category].each { |template| has_valid = true unless template[1][:broken] }
|
426
|
+
categories << template_category if has_valid
|
427
|
+
end
|
428
|
+
# Bomb-out if all templates are broken. This will happen a lot during dev.
|
429
|
+
Blufin::Terminal::error('No valid templates.', 'Please fix the above error(s) and try again.', true) unless categories.any?
|
430
|
+
# Show a prompt identical to the 2nd one, except without a template selected.
|
431
|
+
Blufin::Terminal::custom('CLOUDFORMATION', 55)
|
432
|
+
# Select category prompt.
|
433
|
+
category = Blufin::Terminal::prompt_select('Select Category:', categories)
|
434
|
+
options = []
|
435
|
+
@templates[category].each do |key, data|
|
436
|
+
option = {
|
437
|
+
:text => "#{data[:name]}\x1B[38;5;246m\x1B[0m",
|
438
|
+
:value => data[:name]
|
439
|
+
}
|
440
|
+
option[:disabled] = data[:broken] if data[:broken]
|
441
|
+
options << option
|
442
|
+
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
|
+
# If template is broken, bomb-out (in theory, a broken template should never be selectable.).
|
449
|
+
raise RuntimeError, "Template with category: #{category} and Id: #{template} does not exist." unless @templates.has_key?(category) && @templates[category].has_key?(template)
|
450
|
+
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]
|
451
|
+
end
|
452
|
+
@params = {}
|
453
|
+
@template = @templates[category][template].dup
|
454
|
+
system('clear')
|
455
|
+
@template[:parameters] = {} if !@template.has_key?(:parameters) || !@template[:parameters].any?
|
456
|
+
# Show summary (with intro if exists).
|
457
|
+
if @template[:intro].nil?
|
458
|
+
template_intro = []
|
459
|
+
else
|
460
|
+
template_intro = @template[:intro].split("\n")
|
461
|
+
template_intro << ''
|
462
|
+
end
|
463
|
+
params_to_fetch = []
|
464
|
+
params_to_fetch_colored = []
|
465
|
+
# Output the title + intro.
|
466
|
+
Blufin::Terminal::custom('CLOUDFORMATION', 55, "Selected template: #{render_category_template(category, template)}")
|
467
|
+
# TODO - This is where you would put more info about the template (either do later or remove this TODO).
|
468
|
+
# puts "\x1B[38;5;240m Stack-name: \x1B[38;5;30m#{@template[:stack_name]}\x1B[0m"
|
469
|
+
# puts
|
470
|
+
template_intro.each { |line| puts "\x1B[38;5;240m #{line}\x1B[0m" }
|
471
|
+
# Prepare options. Merge custom with defaults.
|
472
|
+
options_current = {}
|
473
|
+
@options_default.each do |key, default_options|
|
474
|
+
options_current[OPTION_PROJECT] = (@template[:projects].nil? ? default_options : @template[:projects]) if key == OPTION_PROJECT
|
475
|
+
options_current[OPTION_ENVIRONMENT] = (@template[:environments].nil? ? default_options : @template[:environments]) if key == OPTION_ENVIRONMENT
|
476
|
+
options_current[OPTION_REGION] = (@template[:regions].nil? ? default_options : @template[:regions]) if key == OPTION_REGION
|
477
|
+
end
|
478
|
+
@options_default.keys.each do |key|
|
479
|
+
next if key == OPTION_STACK_NAME
|
480
|
+
@template[:parameters][key] = {
|
481
|
+
'Type' => 'String',
|
482
|
+
'Description' => "%w(#{options_current[key].join(' ')})",
|
483
|
+
OPTIONS => options_current[key],
|
484
|
+
}
|
485
|
+
end
|
486
|
+
@template[:parameters][OPTION_STACK_NAME] = {
|
487
|
+
'Type' => 'String',
|
488
|
+
'Description' => @template[:stack_name].nil? ? default_options : @template[:stack_name],
|
489
|
+
'MinLength' => 1
|
490
|
+
}
|
491
|
+
@template[:parameters][OPTION_DESCRIPTION] = {
|
492
|
+
'Type' => 'String',
|
493
|
+
'Description' => 'A quick note to describe this stack (displayed in CloudFormation console).',
|
494
|
+
'MinLength' => 1
|
495
|
+
}
|
496
|
+
# Get cached values (if exist and parameters haven't changed).
|
497
|
+
# Even a one-character change in a description will invalidate the cache.
|
498
|
+
cache_file = get_cache_file(category, template)
|
499
|
+
@cache = {}
|
500
|
+
if Blufin::Files::file_exists(cache_file)
|
501
|
+
begin
|
502
|
+
@cache = JSON.parse(`cat #{cache_file}`)
|
503
|
+
rescue
|
504
|
+
@cache = {}
|
505
|
+
end
|
506
|
+
# Makes sure non of the parameter definitions changed.
|
507
|
+
if @cache.has_key?(CACHE_UUID)
|
508
|
+
@cache = {} if @cache[CACHE_UUID] != get_cache_hexdigest(@template[:parameters])
|
509
|
+
else
|
510
|
+
@cache = {}
|
511
|
+
end
|
512
|
+
end
|
513
|
+
# Output the parameters (in table).
|
514
|
+
if @template[:parameters].any?
|
515
|
+
table_data = []
|
516
|
+
# Loop again to build output.
|
517
|
+
@template[:parameters].each do |param_name, param_data|
|
518
|
+
if @auto_fetch_resources.has_key?(param_name)
|
519
|
+
params_to_fetch << param_name
|
520
|
+
params_to_fetch_colored << "\x1B[38;5;61m#{param_name}\x1B[38;5;208m"
|
521
|
+
end
|
522
|
+
ti = (param_data.has_key?(OPTIONS) ? :system : :normal)
|
523
|
+
table_data << {
|
524
|
+
:type_internal => @auto_fetch_resources.has_key?(param_name) ? :autocomplete : ti,
|
525
|
+
:parameter => param_name,
|
526
|
+
:type => %w(string number).include?(param_data['Type'].downcase) ? param_data['Type'] : SPECIAL,
|
527
|
+
:description => param_data.has_key?('Description') ? param_data['Description'] : "\xe2\x80\x94",
|
528
|
+
:allowed_pattern => param_data.has_key?('AllowedPattern') ? param_data['AllowedPattern'] : nil,
|
529
|
+
:min_length => param_data.has_key?('MinLength') ? param_data['MinLength'] : nil,
|
530
|
+
:min_value => param_data.has_key?('MinValue') ? param_data['MinValue'] : nil,
|
531
|
+
:max_length => param_data.has_key?('MaxLength') ? param_data['MaxLength'] : nil,
|
532
|
+
:max_value => param_data.has_key?('MaxValue') ? param_data['MaxValue'] : nil,
|
533
|
+
:cached_value => @cache.has_key?(param_name) ? @cache[param_name] : "\xe2\x80\x94"
|
534
|
+
}
|
535
|
+
end
|
536
|
+
# This outputs the actual table with all the parameters.
|
537
|
+
App::AWSOutputter::display_parameters(table_data)
|
538
|
+
puts
|
539
|
+
else
|
540
|
+
puts
|
541
|
+
end
|
542
|
+
# If this template has auto-complete params, this shows a spinner to fetch them (whilst you're reading the intro).
|
543
|
+
empty_options = []
|
544
|
+
if params_to_fetch.any?
|
545
|
+
threads = []
|
546
|
+
params_to_fetch.each do |ptf|
|
547
|
+
sleep(0.01)
|
548
|
+
threads << Thread.new {
|
549
|
+
options = fetch_autocomplete_options(ptf, silent: true)
|
550
|
+
if options.nil? || !options.any?
|
551
|
+
empty_options << "\x1B[38;5;196m#{ptf}\x1B[0m \xe2\x80\x94 \x1B[38;5;240mFound 0 result(s)."
|
552
|
+
end
|
553
|
+
}
|
554
|
+
end
|
555
|
+
sleep(0.1)
|
556
|
+
Blufin::Terminal::execute_proc("AWS - Getting parameter data: [ #{params_to_fetch_colored.join(' | ')}\x1B[38;5;208m ]", Proc.new {
|
557
|
+
threads.each { |thread| thread.join }
|
558
|
+
})
|
559
|
+
puts
|
560
|
+
end
|
561
|
+
# If we have any auto-complete options that come back empty, we show an error and disable certain options.
|
562
|
+
if empty_options.any?
|
563
|
+
Blufin::Terminal::error("Cannot currently use this template because some #{Blufin::Terminal::format_highlight('required resources')} don't exist in \x1B[38;5;231m\x1B[48;5;17m AWS \x1B[0m :", empty_options, false)
|
564
|
+
puts
|
565
|
+
choices = []
|
566
|
+
else
|
567
|
+
choices = @cache.any? ? [{value: 'Y', text: "Select this template \x1B[38;5;198m(and apply cached values)\x1B[0m"}] : []
|
568
|
+
choices << {value: 'y', text: 'Select this template'}
|
569
|
+
end
|
570
|
+
# The prompt at the end of the intro.
|
571
|
+
choices << {value: 'n', text: 'Select another template'}
|
572
|
+
choices << {value: 'q', text: 'Quit'}
|
573
|
+
choice = Blufin::Terminal::prompt_select('What would you like to do?', choices)
|
574
|
+
case choice
|
575
|
+
when 'y'
|
576
|
+
puts
|
577
|
+
return [category, template, @template]
|
578
|
+
when 'Y'
|
579
|
+
@params = @cache
|
580
|
+
return [category, template, @template]
|
581
|
+
when 'n'
|
582
|
+
system('clear')
|
583
|
+
select_template_prompt
|
584
|
+
when 'q'
|
585
|
+
puts
|
586
|
+
exit
|
587
|
+
else
|
588
|
+
raise RuntimeError, "Unhandled choice: #{choice}"
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
# Shows a parameter and gets input from user.
|
593
|
+
# @return void
|
594
|
+
def get_parameter_value(param_data, param_name, category, template)
|
595
|
+
description = param_data.has_key?(DESCRIPTION) ? param_data[DESCRIPTION] : nil
|
596
|
+
options_text = "Select #{param_name}:"
|
597
|
+
if param_name == OPTION_STACK_NAME
|
598
|
+
constraints = []
|
599
|
+
constraints << "\x1B[38;5;240mMinLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinLength']}" if param_data.has_key?('MinLength')
|
600
|
+
default = @template[:stack_name]
|
601
|
+
default = default.gsub(/\{\{CATEGORY\}\}/i, category) if default =~ /\{\{CATEGORY\}\}/i
|
602
|
+
default = default.gsub(/\{\{TEMPLATE\}\}/i, template) if default =~ /\{\{TEMPLATE\}\}/i
|
603
|
+
default = default.gsub(/\{\{PROJECT\}\}/i, @params[OPTION_PROJECT]) if default =~ /\{\{PROJECT\}\}/i
|
604
|
+
default = default.gsub(/\{\{ENVIRONMENT\}\}/i, @params[OPTION_ENVIRONMENT]) if default =~ /\{\{ENVIRONMENT\}\}/i
|
605
|
+
default = default.gsub(/\{\{REGION\}\}/i, @params[OPTION_REGION]) if default =~ /\{\{REGION\}\}/i
|
606
|
+
default = default.gsub(/\{\{UUID\}\}/i, SecureRandom.uuid.split('-')[0]) if default =~ /\{\{UUID\{\{/i
|
607
|
+
default = default.downcase
|
608
|
+
return Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: 'The name of the Stack (as displayed in CloudFormation console).')
|
609
|
+
elsif @auto_fetch_resources.has_key?(param_name)
|
610
|
+
options = fetch_autocomplete_options(param_name, silent: true)
|
611
|
+
return Blufin::Terminal::prompt_select(options_text, options, help: description)
|
612
|
+
elsif param_data.has_key?(OPTIONS)
|
613
|
+
raise RuntimeError, "Expected Array, instead got: #{param_data[OPTIONS].class}" unless param_data[OPTIONS].is_a?(Array)
|
614
|
+
if param_data[OPTIONS].length == 1
|
615
|
+
default_option = param_data[OPTIONS][0]
|
616
|
+
puts "#{Blufin::Terminal::display_prompt_text(options_text)} \x1B[38;5;46m#{default_option}\x1B[0m"
|
617
|
+
return default_option
|
618
|
+
else
|
619
|
+
return Blufin::Terminal::prompt_select(options_text, param_data[OPTIONS])
|
620
|
+
end
|
621
|
+
else
|
622
|
+
constraints = []
|
623
|
+
constraints << "\x1B[38;5;240mRegex: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['AllowedPattern']}" if param_data.has_key?('AllowedPattern')
|
624
|
+
constraints << "\x1B[38;5;240mMinLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinLength']}" if param_data.has_key?('MinLength')
|
625
|
+
constraints << "\x1B[38;5;240mMinValue: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinValue']}" if param_data.has_key?('MinValue')
|
626
|
+
constraints << "\x1B[38;5;240mMaxLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxLength']}" if param_data.has_key?('MaxLength')
|
627
|
+
constraints << "\x1B[38;5;240mMaxValue: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxValue']}" if param_data.has_key?('MaxValue')
|
628
|
+
default = param_data.has_key?('Default') ? param_data['Default'] : nil
|
629
|
+
loop do
|
630
|
+
value = Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: description)
|
631
|
+
value_invalid = false
|
632
|
+
value_invalid = true if param_data.has_key?('AllowedPattern') && value !~ /#{param_data['AllowedPattern']}/
|
633
|
+
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']
|
638
|
+
if value_invalid
|
639
|
+
description = nil
|
640
|
+
puts "\x1B[38;5;196mValue does not meet allowed constraint(s).\x1B[0m"
|
641
|
+
else
|
642
|
+
return value
|
643
|
+
end
|
644
|
+
end
|
645
|
+
end
|
646
|
+
puts
|
647
|
+
end
|
648
|
+
|
649
|
+
# Goes off to AWS and gets values for autocomplete supported fields.
|
650
|
+
# @return string
|
651
|
+
def fetch_autocomplete_options(resource_name, silent: true)
|
652
|
+
raise RuntimeError, "Key not found in @auto_fetch_resources: #{resource_name}" unless @auto_fetch_resources.has_key?(resource_name)
|
653
|
+
return @auto_fetch_cache[resource_name] if @auto_fetch_cache.has_key?(resource_name)
|
654
|
+
resource_title = @auto_fetch_resources[resource_name][:resource_title]
|
655
|
+
resource = @auto_fetch_resources[resource_name][:resource]
|
656
|
+
results = App::AWSReports::get_aws_data(@regions, resource, resource_title, silent: silent)
|
657
|
+
options = App::AWSReports::parse_results_for_prompt(resource, results)
|
658
|
+
@auto_fetch_cache[resource_name] = options
|
659
|
+
options
|
660
|
+
end
|
661
|
+
|
662
|
+
# Returns short-hand syntax for tags.
|
663
|
+
# @return string
|
664
|
+
def assemble_tags(params)
|
665
|
+
output = []
|
666
|
+
params.each do |key, value|
|
667
|
+
next unless OPTION_TAGS.include?(key.downcase)
|
668
|
+
output << {
|
669
|
+
'Key' => key,
|
670
|
+
'Value' => value
|
671
|
+
}
|
672
|
+
end
|
673
|
+
output.to_json
|
674
|
+
end
|
675
|
+
|
676
|
+
# Returns JSON syntax for params.
|
677
|
+
# @return string
|
678
|
+
def assemble_params(params)
|
679
|
+
output = []
|
680
|
+
params.each do |key, value|
|
681
|
+
next if OPTION_STACK_NAME.downcase == key.downcase
|
682
|
+
unless OPTION_TAGS.include?(key.downcase)
|
683
|
+
output << {
|
684
|
+
'ParameterKey' => key,
|
685
|
+
'ParameterValue' => value
|
686
|
+
}
|
687
|
+
end
|
688
|
+
end
|
689
|
+
output.to_json
|
690
|
+
end
|
691
|
+
|
692
|
+
# Outputs anything returned from the .before() .after() .teardown() methods in templates.
|
693
|
+
# @return void
|
694
|
+
def output_res(res)
|
695
|
+
if res.is_a?(String) && res.length > 0
|
696
|
+
puts "\n\n"
|
697
|
+
puts res
|
698
|
+
elsif res.is_a?(Array) || res.is_a?(Hash)
|
699
|
+
puts "\n\n"
|
700
|
+
Blufin::Terminal::code_highlight(res.to_yaml, 'yml', 4)
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
# Standardized way of creating cache filename.
|
705
|
+
# @return string
|
706
|
+
def get_cache_file(category, template)
|
707
|
+
"/tmp/aws-cf-cache-#{category}-#{template}.txt"
|
708
|
+
end
|
709
|
+
|
710
|
+
# Takes the parameter keys and hashes them. Helps detect when to invalidate the cache.
|
711
|
+
# @return string
|
712
|
+
def get_cache_hexdigest(parameter_hash)
|
713
|
+
raise RuntimeError, "Expected Hash, instead got #{parameter_hash.class}" unless parameter_hash.is_a?(Hash)
|
714
|
+
Digest::SHA2.hexdigest(parameter_hash.to_s)
|
715
|
+
end
|
716
|
+
|
717
|
+
# Standardized way of rendering category/template output.
|
718
|
+
# @return string
|
719
|
+
def render_category_template(category, template)
|
720
|
+
"#{Blufin::Terminal::format_action(category)} \x1B[38;5;240m[#{Blufin::Terminal::format_highlight(template)}\x1B[38;5;240m]"
|
721
|
+
end
|
722
|
+
|
723
|
+
# Standardized way of rendering constraints output.
|
724
|
+
# @return string
|
725
|
+
def render_constraints(constraints)
|
726
|
+
constraint_bracket_color = 94
|
727
|
+
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
|
+
end
|
729
|
+
|
730
|
+
end
|
731
|
+
|
732
|
+
end
|