awx 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/aws/aws_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
|