awx 0.1.0 → 0.2.0

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