stack_master 2.3.0 → 2.14.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +22 -0
- data/README.md +100 -23
- data/lib/stack_master/aws_driver/cloud_formation.rb +5 -2
- data/lib/stack_master/aws_driver/s3.rb +5 -5
- data/lib/stack_master/change_set.rb +5 -5
- data/lib/stack_master/cli.rb +49 -15
- data/lib/stack_master/cloudformation_interpolating_eruby.rb +57 -0
- data/lib/stack_master/cloudformation_template_eruby.rb +32 -0
- data/lib/stack_master/commands/apply.rb +2 -14
- data/lib/stack_master/commands/compile.rb +1 -1
- data/lib/stack_master/commands/drift.rb +118 -0
- data/lib/stack_master/commands/init.rb +1 -1
- data/lib/stack_master/commands/nag.rb +30 -0
- data/lib/stack_master/commands/resources.rb +1 -1
- data/lib/stack_master/commands/status.rb +1 -1
- data/lib/stack_master/commands/tidy.rb +1 -1
- data/lib/stack_master/commands/validate.rb +1 -1
- data/lib/stack_master/config.rb +9 -1
- data/lib/stack_master/diff.rb +45 -0
- data/lib/stack_master/identity.rb +37 -5
- data/lib/stack_master/parameter_loader.rb +4 -5
- data/lib/stack_master/parameter_resolvers/acm_certificate.rb +2 -2
- data/lib/stack_master/parameter_resolvers/ami_finder.rb +3 -3
- data/lib/stack_master/parameter_resolvers/latest_container.rb +1 -1
- data/lib/stack_master/parameter_resolvers/parameter_store.rb +3 -3
- data/lib/stack_master/parameter_resolvers/stack_output.rb +1 -1
- data/lib/stack_master/parameter_validator.rb +53 -0
- data/lib/stack_master/role_assumer.rb +3 -2
- data/lib/stack_master/security_group_finder.rb +1 -1
- data/lib/stack_master/sns_topic_finder.rb +1 -1
- data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +1 -1
- data/lib/stack_master/sparkle_formation/template_file.rb +2 -50
- data/lib/stack_master/stack.rb +23 -10
- data/lib/stack_master/stack_definition.rb +27 -10
- data/lib/stack_master/stack_differ.rb +15 -39
- data/lib/stack_master/stack_events/presenter.rb +1 -1
- data/lib/stack_master/template_compilers/cfndsl.rb +3 -2
- data/lib/stack_master/template_compilers/sparkle_formation.rb +1 -1
- data/lib/stack_master/template_compilers/yaml_erb.rb +19 -0
- data/lib/stack_master/template_utils.rb +9 -3
- data/lib/stack_master/validator.rb +25 -8
- data/lib/stack_master/version.rb +1 -1
- data/lib/stack_master.rb +23 -2
- metadata +73 -17
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'pathname'
|
2
|
-
|
3
1
|
module StackMaster
|
4
2
|
module Commands
|
5
3
|
class Apply
|
@@ -205,18 +203,8 @@ module StackMaster
|
|
205
203
|
end
|
206
204
|
|
207
205
|
def ensure_valid_parameters!
|
208
|
-
|
209
|
-
|
210
|
-
Empty/blank parameters detected, ensure values exist for those parameters.
|
211
|
-
Parameters will be read from the following locations:
|
212
|
-
MESSAGE
|
213
|
-
base_dir = Pathname.new(@stack_definition.base_dir)
|
214
|
-
@stack_definition.parameter_file_globs.each do |glob|
|
215
|
-
parameter_file = Pathname.new(glob).relative_path_from(base_dir)
|
216
|
-
message << " - #{parameter_file}\n"
|
217
|
-
end
|
218
|
-
failed!(message)
|
219
|
-
end
|
206
|
+
pv = ParameterValidator.new(stack: @proposed_stack, stack_definition: @stack_definition)
|
207
|
+
failed!(pv.error_message) if pv.missing_parameters?
|
220
208
|
end
|
221
209
|
|
222
210
|
def ensure_valid_template_body_size!
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'diffy'
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
module Commands
|
5
|
+
class Drift
|
6
|
+
include Command
|
7
|
+
include Commander::UI
|
8
|
+
|
9
|
+
DETECTION_COMPLETE_STATES = [
|
10
|
+
'DETECTION_COMPLETE',
|
11
|
+
'DETECTION_FAILED'
|
12
|
+
]
|
13
|
+
|
14
|
+
def perform
|
15
|
+
detect_stack_drift_result = cf.detect_stack_drift(stack_name: stack_name)
|
16
|
+
drift_results = wait_for_drift_results(detect_stack_drift_result.stack_drift_detection_id)
|
17
|
+
|
18
|
+
puts colorize("Drift Status: #{drift_results.stack_drift_status}", stack_drift_status_color(drift_results.stack_drift_status))
|
19
|
+
return if drift_results.stack_drift_status == 'IN_SYNC'
|
20
|
+
|
21
|
+
failed
|
22
|
+
|
23
|
+
resp = cf.describe_stack_resource_drifts(stack_name: stack_name)
|
24
|
+
resp.stack_resource_drifts.each do |drift|
|
25
|
+
display_drift(drift)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def cf
|
32
|
+
@cf ||= StackMaster.cloud_formation_driver
|
33
|
+
end
|
34
|
+
|
35
|
+
def display_drift(drift)
|
36
|
+
color = drift_color(drift)
|
37
|
+
puts colorize([drift.stack_resource_drift_status,
|
38
|
+
drift.resource_type,
|
39
|
+
drift.logical_resource_id,
|
40
|
+
drift.physical_resource_id].join(' '), color)
|
41
|
+
return unless drift.stack_resource_drift_status == 'MODIFIED'
|
42
|
+
|
43
|
+
unless drift.property_differences.empty?
|
44
|
+
puts colorize(' Property differences:', color)
|
45
|
+
end
|
46
|
+
drift.property_differences.each do |property_difference|
|
47
|
+
puts colorize(" - #{property_difference.difference_type} #{property_difference.property_path}", color)
|
48
|
+
end
|
49
|
+
puts colorize(' Resource diff:', color)
|
50
|
+
display_resource_drift(drift)
|
51
|
+
end
|
52
|
+
|
53
|
+
def display_resource_drift(drift)
|
54
|
+
diff = ::StackMaster::Diff.new(before: prettify_json(drift.expected_properties),
|
55
|
+
after: prettify_json(drift.actual_properties))
|
56
|
+
diff.display_colorized_diff
|
57
|
+
end
|
58
|
+
|
59
|
+
def prettify_json(string)
|
60
|
+
JSON.pretty_generate(JSON.parse(string)) + "\n"
|
61
|
+
rescue StandardError => e
|
62
|
+
puts "Failed to prettify drifted resource: #{e.message}"
|
63
|
+
string
|
64
|
+
end
|
65
|
+
|
66
|
+
def stack_drift_status_color(stack_drift_status)
|
67
|
+
case stack_drift_status
|
68
|
+
when 'IN_SYNC'
|
69
|
+
:green
|
70
|
+
when 'DRIFTED'
|
71
|
+
:yellow
|
72
|
+
else
|
73
|
+
:blue
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def drift_color(drift)
|
78
|
+
case drift.stack_resource_drift_status
|
79
|
+
when 'IN_SYNC'
|
80
|
+
:green
|
81
|
+
when 'MODIFIED'
|
82
|
+
:yellow
|
83
|
+
when 'DELETED'
|
84
|
+
:red
|
85
|
+
else
|
86
|
+
:blue
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def wait_for_drift_results(detection_id)
|
91
|
+
resp = nil
|
92
|
+
start_time = Time.now
|
93
|
+
loop do
|
94
|
+
resp = cf.describe_stack_drift_detection_status(stack_drift_detection_id: detection_id)
|
95
|
+
break if DETECTION_COMPLETE_STATES.include?(resp.detection_status)
|
96
|
+
|
97
|
+
elapsed_time = Time.now - start_time
|
98
|
+
if elapsed_time > @options.timeout
|
99
|
+
raise "Timeout waiting for stack drift detection"
|
100
|
+
end
|
101
|
+
|
102
|
+
sleep SLEEP_SECONDS
|
103
|
+
end
|
104
|
+
resp
|
105
|
+
end
|
106
|
+
|
107
|
+
def puts(string)
|
108
|
+
StackMaster.stdout.puts(string)
|
109
|
+
end
|
110
|
+
|
111
|
+
extend Forwardable
|
112
|
+
def_delegators :@stack_definition, :stack_name, :region
|
113
|
+
def_delegators :StackMaster, :colorize
|
114
|
+
|
115
|
+
SLEEP_SECONDS = 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -29,7 +29,7 @@ module StackMaster
|
|
29
29
|
|
30
30
|
if !@options.overwrite
|
31
31
|
[@stack_master_filename, @stack_json_filename, @parameters_filename, @region_parameters_filename].each do |filename|
|
32
|
-
if File.
|
32
|
+
if File.exist?(filename)
|
33
33
|
StackMaster.stderr.puts("Aborting: #{filename} already exists. Use --overwrite to force overwriting file.")
|
34
34
|
return false
|
35
35
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module Commands
|
3
|
+
class Nag
|
4
|
+
include Command
|
5
|
+
include Commander::UI
|
6
|
+
|
7
|
+
def perform
|
8
|
+
rv = Tempfile.open(['stack', "___#{stack_definition.stack_name}.#{proposed_stack.template_format}"]) do |f|
|
9
|
+
f.write(proposed_stack.template_body)
|
10
|
+
f.flush
|
11
|
+
system('cfn_nag', f.path)
|
12
|
+
$?.exitstatus
|
13
|
+
end
|
14
|
+
|
15
|
+
failed!("cfn_nag check failed with exit status #{rv}") if rv > 0
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def stack_definition
|
21
|
+
@stack_definition ||= @config.find_stack(@region, @stack_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def proposed_stack
|
25
|
+
@proposed_stack ||= Stack.generate(stack_definition, @config)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -17,7 +17,7 @@ module StackMaster
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def stack_resources
|
20
|
-
@stack_resources ||= cf.describe_stack_resources(stack_name: @stack_definition.stack_name).stack_resources
|
20
|
+
@stack_resources ||= cf.describe_stack_resources({ stack_name: @stack_definition.stack_name }).stack_resources
|
21
21
|
rescue Aws::CloudFormation::Errors::ValidationError
|
22
22
|
nil
|
23
23
|
end
|
@@ -44,7 +44,7 @@ module StackMaster
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def running_in_allowed_account?(allowed_accounts)
|
47
|
-
StackMaster.skip_account_check? || identity.
|
47
|
+
StackMaster.skip_account_check? || identity.running_in_account?(allowed_accounts)
|
48
48
|
end
|
49
49
|
|
50
50
|
def identity
|
@@ -12,7 +12,7 @@ module StackMaster
|
|
12
12
|
parameter_files = Set.new(find_parameter_files())
|
13
13
|
|
14
14
|
status = @config.stacks.each do |stack_definition|
|
15
|
-
parameter_files.subtract(stack_definition.
|
15
|
+
parameter_files.subtract(stack_definition.parameter_files_from_globs)
|
16
16
|
template = File.absolute_path(stack_definition.template_file_path)
|
17
17
|
|
18
18
|
if template
|
data/lib/stack_master/config.rb
CHANGED
@@ -17,6 +17,7 @@ module StackMaster
|
|
17
17
|
attr_accessor :stacks,
|
18
18
|
:base_dir,
|
19
19
|
:template_dir,
|
20
|
+
:parameters_dir,
|
20
21
|
:stack_defaults,
|
21
22
|
:region_defaults,
|
22
23
|
:region_aliases,
|
@@ -27,7 +28,7 @@ module StackMaster
|
|
27
28
|
|
28
29
|
dir = Dir.pwd
|
29
30
|
parent_dir = File.expand_path("..", Dir.pwd)
|
30
|
-
while parent_dir != dir && !File.
|
31
|
+
while parent_dir != dir && !File.exist?(File.join(dir, config_file))
|
31
32
|
dir = parent_dir
|
32
33
|
parent_dir = File.expand_path("..", dir)
|
33
34
|
end
|
@@ -39,6 +40,7 @@ module StackMaster
|
|
39
40
|
@config = config
|
40
41
|
@base_dir = base_dir
|
41
42
|
@template_dir = config.fetch('template_dir', nil)
|
43
|
+
@parameters_dir = config.fetch('parameters_dir', nil)
|
42
44
|
@stack_defaults = config.fetch('stack_defaults', {})
|
43
45
|
@region_aliases = Utils.underscore_keys_to_hyphen(config.fetch('region_aliases', {}))
|
44
46
|
@region_to_aliases = @region_aliases.inject({}) do |hash, (key, value)|
|
@@ -48,6 +50,8 @@ module StackMaster
|
|
48
50
|
end
|
49
51
|
@region_defaults = normalise_region_defaults(config.fetch('region_defaults', {}))
|
50
52
|
@stacks = []
|
53
|
+
|
54
|
+
raise ConfigParseError.new("Stack defaults can't be undefined") if @stack_defaults.nil?
|
51
55
|
load_template_compilers(config)
|
52
56
|
load_config
|
53
57
|
end
|
@@ -90,6 +94,7 @@ module StackMaster
|
|
90
94
|
json: :json,
|
91
95
|
yml: :yaml,
|
92
96
|
yaml: :yaml,
|
97
|
+
erb: :yaml_erb,
|
93
98
|
}
|
94
99
|
end
|
95
100
|
|
@@ -109,12 +114,15 @@ module StackMaster
|
|
109
114
|
stacks.each do |region, stacks_for_region|
|
110
115
|
region = Utils.underscore_to_hyphen(region)
|
111
116
|
stacks_for_region.each do |stack_name, attributes|
|
117
|
+
raise ConfigParseError.new("Entry for stack #{stack_name} has no attributes") if attributes.nil?
|
118
|
+
|
112
119
|
stack_name = Utils.underscore_to_hyphen(stack_name)
|
113
120
|
stack_attributes = build_stack_defaults(region).deeper_merge!(attributes).merge(
|
114
121
|
'region' => region,
|
115
122
|
'stack_name' => stack_name,
|
116
123
|
'base_dir' => @base_dir,
|
117
124
|
'template_dir' => @template_dir,
|
125
|
+
'parameters_dir' => @parameters_dir,
|
118
126
|
'additional_parameter_lookup_dirs' => @region_to_aliases[region])
|
119
127
|
stack_attributes['allowed_accounts'] = attributes['allowed_accounts'] if attributes['allowed_accounts']
|
120
128
|
@stacks << StackDefinition.new(stack_attributes)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module StackMaster
|
2
|
+
class Diff
|
3
|
+
def initialize(name: nil, before:, after:, context: 10_000)
|
4
|
+
@name = name
|
5
|
+
@before = before
|
6
|
+
@after = after
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
def display
|
11
|
+
stdout.print "#{@name} diff: "
|
12
|
+
if diff == ''
|
13
|
+
stdout.puts "No changes"
|
14
|
+
else
|
15
|
+
stdout.puts
|
16
|
+
display_colorized_diff
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def display_colorized_diff
|
21
|
+
diff.each_line do |line|
|
22
|
+
if line.start_with?('+')
|
23
|
+
stdout.print colorize(line, :green)
|
24
|
+
elsif line.start_with?('-')
|
25
|
+
stdout.print colorize(line, :red)
|
26
|
+
else
|
27
|
+
stdout.print line
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def different?
|
33
|
+
diff != ''
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def diff
|
39
|
+
@diff ||= Diffy::Diff.new(@before, @after, context: @context).to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
extend Forwardable
|
43
|
+
def_delegators :StackMaster, :colorize, :stdout
|
44
|
+
end
|
45
|
+
end
|
@@ -1,23 +1,55 @@
|
|
1
1
|
module StackMaster
|
2
2
|
class Identity
|
3
|
-
|
4
|
-
|
3
|
+
AllowedAccountAliasesError = Class.new(StandardError)
|
4
|
+
MissingIamPermissionsError = Class.new(StandardError)
|
5
|
+
|
6
|
+
def running_in_account?(accounts)
|
7
|
+
return true if accounts.nil? || accounts.empty? || contains_account_id?(accounts)
|
8
|
+
|
9
|
+
# skip alias check (which makes an API call) if all values are account IDs
|
10
|
+
return false if accounts.all? { |account| account_id?(account) }
|
11
|
+
|
12
|
+
contains_account_alias?(accounts)
|
13
|
+
rescue MissingIamPermissionsError
|
14
|
+
raise AllowedAccountAliasesError, 'Failed to validate whether the current AWS account is allowed'
|
5
15
|
end
|
6
16
|
|
7
17
|
def account
|
8
18
|
@account ||= sts.get_caller_identity.account
|
9
19
|
end
|
10
20
|
|
11
|
-
|
21
|
+
def account_aliases
|
22
|
+
@aliases ||= iam.list_account_aliases.account_aliases
|
23
|
+
rescue Aws::IAM::Errors::AccessDenied
|
24
|
+
raise MissingIamPermissionsError, 'Failed to retrieve account aliases. Missing required IAM permission: iam:ListAccountAliases'
|
25
|
+
end
|
12
26
|
|
13
|
-
|
27
|
+
private
|
14
28
|
|
15
29
|
def region
|
16
30
|
@region ||= ENV['AWS_REGION'] || Aws.config[:region] || Aws.shared_config.region || 'us-east-1'
|
17
31
|
end
|
18
32
|
|
19
33
|
def sts
|
20
|
-
@sts ||= Aws::STS::Client.new(region: region)
|
34
|
+
@sts ||= Aws::STS::Client.new({ region: region })
|
35
|
+
end
|
36
|
+
|
37
|
+
def iam
|
38
|
+
@iam ||= Aws::IAM::Client.new({ region: region })
|
39
|
+
end
|
40
|
+
|
41
|
+
def contains_account_id?(ids)
|
42
|
+
ids.include?(account)
|
43
|
+
end
|
44
|
+
|
45
|
+
def contains_account_alias?(aliases)
|
46
|
+
account_aliases.any? { |account_alias| aliases.include?(account_alias) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def account_id?(id_or_alias)
|
50
|
+
# While it's not explicitly documented as prohibited, it cannot (currently) be possible to set an account alias of
|
51
|
+
# 12 digits, as that could cause one console sign-in URL to resolve to two separate accounts.
|
52
|
+
/^[0-9]{12}$/.match?(id_or_alias)
|
21
53
|
end
|
22
54
|
end
|
23
55
|
end
|
@@ -5,10 +5,10 @@ module StackMaster
|
|
5
5
|
|
6
6
|
COMPILE_TIME_PARAMETERS_KEY = 'compile_time_parameters'
|
7
7
|
|
8
|
-
def self.load(parameter_files)
|
8
|
+
def self.load(parameter_files: [], parameters: {})
|
9
9
|
StackMaster.debug 'Searching for parameter files...'
|
10
|
-
parameter_files.
|
11
|
-
|
10
|
+
all_parameters = parameter_files.map { |file_name| load_parameters(file_name) } + [parameters]
|
11
|
+
all_parameters.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, parameters|
|
12
12
|
template_parameters = create_template_parameters(parameters)
|
13
13
|
compile_time_parameters = create_compile_time_parameters(parameters)
|
14
14
|
|
@@ -16,13 +16,12 @@ module StackMaster
|
|
16
16
|
merge_and_camelize(hash[:compile_time_parameters], compile_time_parameters)
|
17
17
|
hash
|
18
18
|
end
|
19
|
-
|
20
19
|
end
|
21
20
|
|
22
21
|
private
|
23
22
|
|
24
23
|
def self.load_parameters(file_name)
|
25
|
-
file_exists = File.
|
24
|
+
file_exists = File.exist?(file_name)
|
26
25
|
StackMaster.debug file_exists ? " #{file_name} found" : " #{file_name} not found"
|
27
26
|
file_exists ? load_file(file_name) : {}
|
28
27
|
end
|
@@ -19,9 +19,9 @@ module StackMaster
|
|
19
19
|
def all_certs
|
20
20
|
certs = []
|
21
21
|
next_token = nil
|
22
|
-
client = Aws::ACM::Client.new(region: @stack_definition.region)
|
22
|
+
client = Aws::ACM::Client.new({ region: @stack_definition.region })
|
23
23
|
loop do
|
24
|
-
resp = client.list_certificates(certificate_statuses: ['ISSUED'], next_token: next_token)
|
24
|
+
resp = client.list_certificates({ certificate_statuses: ['ISSUED'], next_token: next_token })
|
25
25
|
certs << resp.certificate_summary_list
|
26
26
|
next_token = resp.next_token
|
27
27
|
break if next_token.nil?
|
@@ -19,7 +19,7 @@ module StackMaster
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def find_latest_ami(filters, owners = ['self'])
|
22
|
-
images = ec2.describe_images(owners: owners, filters: filters).images
|
22
|
+
images = ec2.describe_images({ owners: owners, filters: filters }).images
|
23
23
|
sorted_images = images.sort do |a, b|
|
24
24
|
Time.parse(a.creation_date) <=> Time.parse(b.creation_date)
|
25
25
|
end
|
@@ -29,8 +29,8 @@ module StackMaster
|
|
29
29
|
private
|
30
30
|
|
31
31
|
def ec2
|
32
|
-
@ec2 ||= Aws::EC2::Client.new(region: @region)
|
32
|
+
@ec2 ||= Aws::EC2::Client.new({ region: @region })
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
-
end
|
36
|
+
end
|
@@ -14,7 +14,7 @@ module StackMaster
|
|
14
14
|
end
|
15
15
|
|
16
16
|
@region = parameters['region'] || @stack_definition.region
|
17
|
-
ecr_client = Aws::ECR::Client.new(region: @region)
|
17
|
+
ecr_client = Aws::ECR::Client.new({ region: @region })
|
18
18
|
|
19
19
|
images = fetch_images(parameters['repository_name'], parameters['registry_id'], ecr_client)
|
20
20
|
|
@@ -11,11 +11,11 @@ module StackMaster
|
|
11
11
|
|
12
12
|
def resolve(value)
|
13
13
|
begin
|
14
|
-
ssm = Aws::SSM::Client.new(region: @stack_definition.region)
|
15
|
-
resp = ssm.get_parameter(
|
14
|
+
ssm = Aws::SSM::Client.new({ region: @stack_definition.region })
|
15
|
+
resp = ssm.get_parameter({
|
16
16
|
name: value,
|
17
17
|
with_decryption: true
|
18
|
-
)
|
18
|
+
})
|
19
19
|
rescue Aws::SSM::Errors::ParameterNotFound
|
20
20
|
raise ParameterNotFound, "Unable to find #{value} in Parameter Store"
|
21
21
|
end
|
@@ -53,7 +53,7 @@ module StackMaster
|
|
53
53
|
|
54
54
|
@stacks.fetch(stack_key) do
|
55
55
|
regional_cf = cf_for_region(unaliased_region)
|
56
|
-
cf_stack = regional_cf.describe_stacks(stack_name: stack_name).stacks.first
|
56
|
+
cf_stack = regional_cf.describe_stacks({ stack_name: stack_name }).stacks.first
|
57
57
|
@stacks[stack_key] = cf_stack
|
58
58
|
end
|
59
59
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
class ParameterValidator
|
5
|
+
def initialize(stack:, stack_definition:)
|
6
|
+
@stack = stack
|
7
|
+
@stack_definition = stack_definition
|
8
|
+
end
|
9
|
+
|
10
|
+
def error_message
|
11
|
+
return nil unless missing_parameters?
|
12
|
+
message = "Empty/blank parameters detected. Please provide values for these parameters:\n"
|
13
|
+
missing_parameters.each do |parameter_name|
|
14
|
+
message << " - #{parameter_name}\n"
|
15
|
+
end
|
16
|
+
if @stack_definition.parameter_files.empty?
|
17
|
+
message << message_for_parameter_globs
|
18
|
+
else
|
19
|
+
message << message_for_parameter_files
|
20
|
+
end
|
21
|
+
message
|
22
|
+
end
|
23
|
+
|
24
|
+
def missing_parameters?
|
25
|
+
missing_parameters.any?
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def message_for_parameter_files
|
31
|
+
"Parameters are configured to be read from the following files:\n".tap do |message|
|
32
|
+
@stack_definition.parameter_files.each do |parameter_file|
|
33
|
+
message << " - #{parameter_file}\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def message_for_parameter_globs
|
39
|
+
"Parameters will be read from files matching the following globs:\n".tap do |message|
|
40
|
+
base_dir = Pathname.new(@stack_definition.base_dir)
|
41
|
+
@stack_definition.parameter_file_globs.each do |glob|
|
42
|
+
parameter_file = Pathname.new(glob).relative_path_from(base_dir)
|
43
|
+
message << " - #{parameter_file}\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def missing_parameters
|
49
|
+
@missing_parameters ||=
|
50
|
+
@stack.parameters_with_defaults.select { |_key, value| value.nil? }.keys
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -44,10 +44,11 @@ module StackMaster
|
|
44
44
|
def assume_role_credentials(account, role)
|
45
45
|
credentials_key = "#{account}:#{role}"
|
46
46
|
@credentials.fetch(credentials_key) do
|
47
|
-
@credentials[credentials_key] = Aws::AssumeRoleCredentials.new(
|
47
|
+
@credentials[credentials_key] = Aws::AssumeRoleCredentials.new({
|
48
|
+
region: StackMaster.cloud_formation_driver.region,
|
48
49
|
role_arn: "arn:aws:iam::#{account}:role/#{role}",
|
49
50
|
role_session_name: "stack-master-role-assumer"
|
50
|
-
)
|
51
|
+
})
|
51
52
|
end
|
52
53
|
end
|
53
54
|
end
|
@@ -5,19 +5,6 @@ module StackMaster
|
|
5
5
|
module SparkleFormation
|
6
6
|
TemplateFileNotFound = ::Class.new(StandardError)
|
7
7
|
|
8
|
-
class SfEruby < Erubis::Eruby
|
9
|
-
include Erubis::ArrayEnhancer
|
10
|
-
|
11
|
-
def add_expr(src, code, indicator)
|
12
|
-
case indicator
|
13
|
-
when '='
|
14
|
-
src << " #{@bufvar} << (" << code << ');'
|
15
|
-
else
|
16
|
-
super
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
8
|
class TemplateContext < AttributeStruct
|
22
9
|
include ::SparkleFormation::SparkleAttribute
|
23
10
|
include ::SparkleFormation::SparkleAttribute::Aws
|
@@ -49,47 +36,12 @@ module StackMaster
|
|
49
36
|
end
|
50
37
|
end
|
51
38
|
|
52
|
-
# Splits up long strings with multiple lines in them to multiple strings
|
53
|
-
# in the CF array. Makes the compiled template and diffs more readable.
|
54
|
-
class CloudFormationLineFormatter
|
55
|
-
def self.format(template)
|
56
|
-
new(template).format
|
57
|
-
end
|
58
|
-
|
59
|
-
def initialize(template)
|
60
|
-
@template = template
|
61
|
-
end
|
62
|
-
|
63
|
-
def format
|
64
|
-
@template.flat_map do |lines|
|
65
|
-
lines = lines.to_s if Symbol === lines
|
66
|
-
if String === lines
|
67
|
-
newlines = []
|
68
|
-
lines.count("\n").times do
|
69
|
-
newlines << "\n"
|
70
|
-
end
|
71
|
-
newlines = lines.split("\n").map do |line|
|
72
|
-
"#{line}#{newlines.pop}"
|
73
|
-
end
|
74
|
-
if lines.start_with?("\n")
|
75
|
-
newlines.insert(0, "\n")
|
76
|
-
end
|
77
|
-
newlines
|
78
|
-
else
|
79
|
-
lines
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
39
|
module Template
|
86
40
|
def self.render(prefix, file_name, vars)
|
87
41
|
file_path = File.join(::SparkleFormation.sparkle_path, prefix, file_name)
|
88
|
-
template = File.read(file_path)
|
89
42
|
template_context = TemplateContext.build(vars, prefix)
|
90
|
-
|
91
|
-
|
92
|
-
rescue Errno::ENOENT => e
|
43
|
+
CloudFormationInterpolatingEruby.evaluate_file(file_path, template_context)
|
44
|
+
rescue Errno::ENOENT
|
93
45
|
Kernel.raise TemplateFileNotFound, "Could not find template file at path: #{file_path}"
|
94
46
|
end
|
95
47
|
end
|