stack_master 2.3.0 → 2.14.1
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/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
|