stack_master 0.0.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 +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +208 -0
- data/Rakefile +11 -0
- data/apply_demo.gif +0 -0
- data/bin/stack_master +16 -0
- data/example/simple/Gemfile +3 -0
- data/example/simple/parameters/myapp_vpc.yml +1 -0
- data/example/simple/parameters/myapp_web.yml +2 -0
- data/example/simple/stack_master.yml +13 -0
- data/example/simple/templates/myapp_vpc.rb +39 -0
- data/example/simple/templates/myapp_web.rb +16 -0
- data/features/apply.feature +241 -0
- data/features/delete.feature +43 -0
- data/features/diff.feature +191 -0
- data/features/events.feature +38 -0
- data/features/init.feature +6 -0
- data/features/outputs.feature +49 -0
- data/features/region_aliases.feature +66 -0
- data/features/resources.feature +45 -0
- data/features/stack_defaults.feature +88 -0
- data/features/status.feature +124 -0
- data/features/step_definitions/stack_steps.rb +50 -0
- data/features/support/env.rb +14 -0
- data/lib/stack_master.rb +81 -0
- data/lib/stack_master/aws_driver/cloud_formation.rb +56 -0
- data/lib/stack_master/cli.rb +164 -0
- data/lib/stack_master/command.rb +13 -0
- data/lib/stack_master/commands/apply.rb +104 -0
- data/lib/stack_master/commands/delete.rb +53 -0
- data/lib/stack_master/commands/diff.rb +31 -0
- data/lib/stack_master/commands/events.rb +39 -0
- data/lib/stack_master/commands/init.rb +109 -0
- data/lib/stack_master/commands/list_stacks.rb +16 -0
- data/lib/stack_master/commands/outputs.rb +27 -0
- data/lib/stack_master/commands/resources.rb +33 -0
- data/lib/stack_master/commands/status.rb +47 -0
- data/lib/stack_master/commands/validate.rb +17 -0
- data/lib/stack_master/config.rb +86 -0
- data/lib/stack_master/ctrl_c.rb +4 -0
- data/lib/stack_master/parameter_loader.rb +17 -0
- data/lib/stack_master/parameter_resolver.rb +45 -0
- data/lib/stack_master/parameter_resolvers/secret.rb +42 -0
- data/lib/stack_master/parameter_resolvers/security_group.rb +20 -0
- data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +29 -0
- data/lib/stack_master/parameter_resolvers/stack_output.rb +53 -0
- data/lib/stack_master/prompter.rb +14 -0
- data/lib/stack_master/security_group_finder.rb +29 -0
- data/lib/stack_master/sns_topic_finder.rb +27 -0
- data/lib/stack_master/stack.rb +96 -0
- data/lib/stack_master/stack_definition.rb +49 -0
- data/lib/stack_master/stack_differ.rb +80 -0
- data/lib/stack_master/stack_events/fetcher.rb +45 -0
- data/lib/stack_master/stack_events/presenter.rb +27 -0
- data/lib/stack_master/stack_events/streamer.rb +55 -0
- data/lib/stack_master/stack_states.rb +34 -0
- data/lib/stack_master/template_compiler.rb +21 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +139 -0
- data/lib/stack_master/testing.rb +7 -0
- data/lib/stack_master/utils.rb +31 -0
- data/lib/stack_master/validator.rb +25 -0
- data/lib/stack_master/version.rb +3 -0
- data/logo.png +0 -0
- data/script/buildkite/bundle.sh +5 -0
- data/script/buildkite/clean.sh +3 -0
- data/script/buildkite_rspec.sh +27 -0
- data/spec/fixtures/parameters/myapp_vpc.yml +1 -0
- data/spec/fixtures/stack_master.yml +35 -0
- data/spec/fixtures/templates/myapp_vpc.json +1 -0
- data/spec/spec_helper.rb +99 -0
- data/spec/stack_master/commands/apply_spec.rb +92 -0
- data/spec/stack_master/commands/delete_spec.rb +40 -0
- data/spec/stack_master/commands/init_spec.rb +17 -0
- data/spec/stack_master/commands/status_spec.rb +38 -0
- data/spec/stack_master/commands/validate_spec.rb +26 -0
- data/spec/stack_master/config_spec.rb +81 -0
- data/spec/stack_master/parameter_loader_spec.rb +81 -0
- data/spec/stack_master/parameter_resolver_spec.rb +58 -0
- data/spec/stack_master/parameter_resolvers/secret_spec.rb +66 -0
- data/spec/stack_master/parameter_resolvers/security_group_spec.rb +17 -0
- data/spec/stack_master/parameter_resolvers/sns_topic_name_spec.rb +43 -0
- data/spec/stack_master/parameter_resolvers/stack_output_spec.rb +77 -0
- data/spec/stack_master/security_group_finder_spec.rb +49 -0
- data/spec/stack_master/sns_topic_finder_spec.rb +25 -0
- data/spec/stack_master/stack_definition_spec.rb +37 -0
- data/spec/stack_master/stack_differ_spec.rb +34 -0
- data/spec/stack_master/stack_events/fetcher_spec.rb +65 -0
- data/spec/stack_master/stack_events/presenter_spec.rb +18 -0
- data/spec/stack_master/stack_events/streamer_spec.rb +33 -0
- data/spec/stack_master/stack_spec.rb +157 -0
- data/spec/stack_master/template_compiler_spec.rb +48 -0
- data/spec/stack_master/test_driver/cloud_formation_spec.rb +24 -0
- data/spec/stack_master/utils_spec.rb +30 -0
- data/spec/stack_master/validator_spec.rb +38 -0
- data/stack_master.gemspec +38 -0
- data/stacktemplates/parameter_region.yml +3 -0
- data/stacktemplates/parameter_stack_name.yml +3 -0
- data/stacktemplates/stack.json.erb +20 -0
- data/stacktemplates/stack_master.yml.erb +6 -0
- metadata +427 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
module Commands
|
|
3
|
+
class Validate
|
|
4
|
+
include Command
|
|
5
|
+
include Commander::UI
|
|
6
|
+
|
|
7
|
+
def initialize(config, stack_definition)
|
|
8
|
+
@config = config
|
|
9
|
+
@stack_definition = stack_definition
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def perform
|
|
13
|
+
Validator.perform(@stack_definition)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'deep_merge/rails_compat'
|
|
2
|
+
require 'active_support/core_ext/object/deep_dup'
|
|
3
|
+
|
|
4
|
+
module StackMaster
|
|
5
|
+
class Config
|
|
6
|
+
def self.load!(config_file = 'stack_master.yml')
|
|
7
|
+
config = YAML.load(File.read(config_file))
|
|
8
|
+
base_dir = File.dirname(File.expand_path(config_file))
|
|
9
|
+
new(config, base_dir)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_accessor :stacks,
|
|
13
|
+
:base_dir,
|
|
14
|
+
:stack_defaults,
|
|
15
|
+
:region_defaults,
|
|
16
|
+
:region_aliases
|
|
17
|
+
|
|
18
|
+
def initialize(config, base_dir)
|
|
19
|
+
@config = config
|
|
20
|
+
@base_dir = base_dir
|
|
21
|
+
@stack_defaults = config.fetch('stack_defaults', {})
|
|
22
|
+
@region_aliases = Utils.underscore_keys_to_hyphen(config.fetch('region_aliases', {}))
|
|
23
|
+
@region_to_aliases = @region_aliases.inject({}) do |hash, (key, value)|
|
|
24
|
+
hash[value] ||= []
|
|
25
|
+
hash[value] << key
|
|
26
|
+
hash
|
|
27
|
+
end
|
|
28
|
+
@region_defaults = normalise_region_defaults(config.fetch('region_defaults', {}))
|
|
29
|
+
@stacks = []
|
|
30
|
+
load_config
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_stack(region, stack_name)
|
|
34
|
+
@stacks.find do |s|
|
|
35
|
+
(s.region == region || s.region == region.gsub('_', '-')) &&
|
|
36
|
+
(s.stack_name == stack_name || s.stack_name == stack_name.gsub('_', '-'))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unalias_region(region)
|
|
41
|
+
@region_aliases.fetch(region) { region }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def load_config
|
|
47
|
+
unaliased_stacks = resolve_region_aliases(@config.fetch('stacks'))
|
|
48
|
+
load_stacks(unaliased_stacks)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_region_aliases(stacks)
|
|
52
|
+
stacks.inject({}) do |hash, (region, attributes)|
|
|
53
|
+
hash[unalias_region(region)] = attributes
|
|
54
|
+
hash
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def load_stacks(stacks)
|
|
59
|
+
stacks.each do |region, stacks_for_region|
|
|
60
|
+
region = Utils.underscore_to_hyphen(region)
|
|
61
|
+
stacks_for_region.each do |stack_name, attributes|
|
|
62
|
+
stack_name = Utils.underscore_to_hyphen(stack_name)
|
|
63
|
+
stack_attributes = build_stack_defaults(region).deeper_merge(attributes).merge(
|
|
64
|
+
'region' => region,
|
|
65
|
+
'stack_name' => stack_name,
|
|
66
|
+
'base_dir' => @base_dir,
|
|
67
|
+
'additional_parameter_lookup_dirs' => @region_to_aliases[region])
|
|
68
|
+
@stacks << StackDefinition.new(stack_attributes)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_stack_defaults(region)
|
|
74
|
+
region_defaults = @region_defaults.fetch(region, {}).deep_dup
|
|
75
|
+
@stack_defaults.deep_dup.deeper_merge(region_defaults)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalise_region_defaults(region_defaults)
|
|
79
|
+
region_defaults.inject({}) do |normalised_aliases, (region_or_alias, value)|
|
|
80
|
+
region = unalias_region(region_or_alias)
|
|
81
|
+
normalised_aliases[Utils.underscore_to_hyphen(region)] = value
|
|
82
|
+
normalised_aliases
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
class ParameterLoader
|
|
3
|
+
def self.load(parameter_files)
|
|
4
|
+
parameter_files.reduce({}) do |hash, file_name|
|
|
5
|
+
parameters = if File.exists?(file_name)
|
|
6
|
+
YAML.load(File.read(file_name)) || {}
|
|
7
|
+
else
|
|
8
|
+
{}
|
|
9
|
+
end
|
|
10
|
+
parameters.each do |key, value|
|
|
11
|
+
hash[key.camelize] = value
|
|
12
|
+
end
|
|
13
|
+
hash
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
class ParameterResolver
|
|
3
|
+
ResolverNotFound = Class.new(StandardError)
|
|
4
|
+
InvalidParameter = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def self.resolve(config, stack_definition, parameters)
|
|
7
|
+
new(config, stack_definition, parameters).resolve
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(config, stack_definition, parameters)
|
|
11
|
+
@config = config
|
|
12
|
+
@stack_definition = stack_definition
|
|
13
|
+
@parameters = parameters
|
|
14
|
+
@resolvers = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def resolve
|
|
18
|
+
@parameters.reduce({}) do |parameters, (key, value)|
|
|
19
|
+
parameters[key] = resolve_parameter_value(value)
|
|
20
|
+
parameters
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def resolve_parameter_value(parameter_value)
|
|
27
|
+
return parameter_value if String === parameter_value || parameter_value.nil?
|
|
28
|
+
raise InvalidParameter, parameter_value unless Hash === parameter_value
|
|
29
|
+
raise InvalidParameter, parameter_value unless parameter_value.keys.size == 1
|
|
30
|
+
resolver_class_name = parameter_value.keys.first.to_s.camelize
|
|
31
|
+
value = parameter_value.values.first
|
|
32
|
+
resolver_class(resolver_class_name).resolve(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolver_class(class_name)
|
|
36
|
+
@resolvers.fetch(class_name) do
|
|
37
|
+
begin
|
|
38
|
+
@resolvers[class_name] = Kernel.const_get("StackMaster::ParameterResolvers::#{class_name}").new(@config, @stack_definition)
|
|
39
|
+
rescue NameError
|
|
40
|
+
raise ResolverNotFound, "Could not find parameter resolver called #{class_name}, please double check your configuration"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
module ParameterResolvers
|
|
3
|
+
class Secret
|
|
4
|
+
SecretNotFound = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def initialize(config, stack_definition)
|
|
7
|
+
@config = config
|
|
8
|
+
@stack_definition = stack_definition
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resolve(value)
|
|
12
|
+
secret_key = value
|
|
13
|
+
raise ArgumentError, "No secret_file defined for stack definition #{@stack_definition.stack_name} in #{@stack_definition.region}" unless !@stack_definition.secret_file.nil?
|
|
14
|
+
raise ArgumentError, "Could not find secret file at #{secret_file_path}" unless File.exist?(secret_file_path)
|
|
15
|
+
secrets_hash.fetch(secret_key) do
|
|
16
|
+
raise SecretNotFound, "Unable to find key #{secret_key} in file #{secret_file_path}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def secrets_hash
|
|
23
|
+
@secrets_hash ||= YAML.load(decrypt_with_dotgpg)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def decrypt_with_dotgpg
|
|
27
|
+
dir = Dotgpg::Dir.closest(secret_file_path)
|
|
28
|
+
stream = StringIO.new
|
|
29
|
+
dir.decrypt(secret_path_relative_to_base, stream)
|
|
30
|
+
stream.string
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def secret_path_relative_to_base
|
|
34
|
+
@secret_path_relative_to_base ||= File.join('secrets', @stack_definition.secret_file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def secret_file_path
|
|
38
|
+
@secret_file_path ||= File.join(@config.base_dir, secret_path_relative_to_base)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
module ParameterResolvers
|
|
3
|
+
class SecurityGroup
|
|
4
|
+
def initialize(config, stack_definition)
|
|
5
|
+
@config = config
|
|
6
|
+
@stack_definition = stack_definition
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def resolve(value)
|
|
10
|
+
security_group_finder.find(value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def security_group_finder
|
|
16
|
+
StackMaster::SecurityGroupFinder.new(@stack_definition.region)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
module ParameterResolvers
|
|
3
|
+
class SnsTopicName
|
|
4
|
+
TopicNotFound = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def initialize(config, stack_definition)
|
|
7
|
+
@config = config
|
|
8
|
+
@stack_definition = stack_definition
|
|
9
|
+
@stacks = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(value)
|
|
13
|
+
sns_topic_finder.find(value)
|
|
14
|
+
rescue StackMaster::SnsTopicFinder::TopicNotFound => e
|
|
15
|
+
raise TopicNotFound.new(e.message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def cf
|
|
21
|
+
@cf ||= StackMaster.cloud_formation_driver
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sns_topic_finder
|
|
25
|
+
StackMaster::SnsTopicFinder.new(@stack_definition.region)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
module ParameterResolvers
|
|
3
|
+
class StackOutput
|
|
4
|
+
StackNotFound = Class.new(StandardError)
|
|
5
|
+
StackOutputNotFound = Class.new(StandardError)
|
|
6
|
+
|
|
7
|
+
def initialize(config, stack_definition)
|
|
8
|
+
@config = config
|
|
9
|
+
@stack_definition = stack_definition
|
|
10
|
+
@stacks = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(value)
|
|
14
|
+
validate_value!(value)
|
|
15
|
+
stack_name, output_name = value.split('/')
|
|
16
|
+
stack = find_stack(stack_name)
|
|
17
|
+
if stack
|
|
18
|
+
output = stack.outputs.find { |output| output.output_key == output_name.camelize }
|
|
19
|
+
if output
|
|
20
|
+
output.output_value
|
|
21
|
+
else
|
|
22
|
+
raise StackOutputNotFound, "Stack exists (#{stack_name}), but output does not: #{output_name}"
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
raise StackNotFound, "Stack in StackOutput not found: #{value}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def cf
|
|
32
|
+
@cf ||= StackMaster.cloud_formation_driver
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_value!(value)
|
|
36
|
+
if !value.is_a?(String) || !value.include?('/')
|
|
37
|
+
raise ArgumentError, 'Stack output values must be in the form of stack-name/output-name'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find_stack(stack_name)
|
|
42
|
+
@stacks.fetch(stack_name) do
|
|
43
|
+
cf_stack = cf.describe_stacks(stack_name: stack_name).stacks.first
|
|
44
|
+
@stacks[stack_name] = cf_stack
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cf
|
|
49
|
+
@cf ||= StackMaster.cloud_formation_driver
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
class SecurityGroupFinder
|
|
3
|
+
SecurityGroupNotFound = Class.new(StandardError)
|
|
4
|
+
MultipleSecurityGroupsFound = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def initialize(region)
|
|
7
|
+
@resource = Aws::EC2::Resource.new(region: region)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find(reference)
|
|
11
|
+
STDERR.puts "Resolving security group reference '#{reference}'"
|
|
12
|
+
raise ArgumentError, 'Security group references must be non-empty strings' unless reference.is_a?(String) && !reference.empty?
|
|
13
|
+
|
|
14
|
+
groups = @resource.security_groups({
|
|
15
|
+
filters: [
|
|
16
|
+
{
|
|
17
|
+
name: "group-name",
|
|
18
|
+
values: [reference],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
raise SecurityGroupNotFound, "No security group with name #{reference} found" unless groups.any?
|
|
24
|
+
raise MultipleSecurityGroupsFound, "More than one security group with name #{reference} found" if groups.count > 1
|
|
25
|
+
|
|
26
|
+
groups.first.id
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
class SnsTopicFinder
|
|
3
|
+
TopicNotFound = Class.new(StandardError)
|
|
4
|
+
|
|
5
|
+
def initialize(region)
|
|
6
|
+
@resource = Aws::SNS::Resource.new(region: region)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def find(reference)
|
|
10
|
+
$stderr.puts "Resolving SNS topic reference '#{reference}'"
|
|
11
|
+
raise ArgumentError, 'SNS topic references must be non-empty strings' unless reference.is_a?(String) && !reference.empty?
|
|
12
|
+
|
|
13
|
+
topic = @resource.topics.detect { |t| topic_name_from_arn(t.arn) == reference }
|
|
14
|
+
|
|
15
|
+
raise TopicNotFound, "No topic with name #{reference} found" unless topic
|
|
16
|
+
|
|
17
|
+
topic.arn
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def topic_name_from_arn(arn)
|
|
23
|
+
arn.split(":")[5]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module StackMaster
|
|
2
|
+
class Stack
|
|
3
|
+
MAX_TEMPLATE_SIZE = 51200
|
|
4
|
+
|
|
5
|
+
include Virtus.model
|
|
6
|
+
|
|
7
|
+
attribute :stack_name, String
|
|
8
|
+
attribute :region, String
|
|
9
|
+
attribute :stack_id, String
|
|
10
|
+
attribute :stack_status, String
|
|
11
|
+
attribute :parameters, Hash
|
|
12
|
+
attribute :template_body, String
|
|
13
|
+
attribute :notification_arns, Array[String]
|
|
14
|
+
attribute :outputs, Array
|
|
15
|
+
attribute :stack_policy_body, String
|
|
16
|
+
attribute :tags, Hash
|
|
17
|
+
|
|
18
|
+
def template_hash
|
|
19
|
+
if template_body
|
|
20
|
+
@template_hash ||= JSON.parse(template_body)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def maybe_compressed_template_body
|
|
25
|
+
if template_body.size > MAX_TEMPLATE_SIZE
|
|
26
|
+
@compressed_template_body ||= JSON.dump(template_hash)
|
|
27
|
+
else
|
|
28
|
+
template_body
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def template_default_parameters
|
|
33
|
+
template_hash.fetch('Parameters', {}).inject({}) do |result, (parameter_name, description)|
|
|
34
|
+
result[parameter_name] = description['Default']
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parameters_with_defaults
|
|
40
|
+
template_default_parameters.merge(parameters)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.find(region, stack_name)
|
|
44
|
+
cf = StackMaster.cloud_formation_driver
|
|
45
|
+
cf_stack = cf.describe_stacks(stack_name: stack_name).stacks.first
|
|
46
|
+
return unless cf_stack
|
|
47
|
+
parameters = cf_stack.parameters.inject({}) do |params_hash, param_struct|
|
|
48
|
+
params_hash[param_struct.parameter_key] = param_struct.parameter_value
|
|
49
|
+
params_hash
|
|
50
|
+
end
|
|
51
|
+
template_body ||= cf.get_template(stack_name: stack_name).template_body
|
|
52
|
+
stack_policy_body ||= cf.get_stack_policy(stack_name: stack_name).stack_policy_body
|
|
53
|
+
outputs = cf_stack.outputs
|
|
54
|
+
|
|
55
|
+
new(region: region,
|
|
56
|
+
stack_name: stack_name,
|
|
57
|
+
stack_id: cf_stack.stack_id,
|
|
58
|
+
parameters: parameters,
|
|
59
|
+
template_body: template_body,
|
|
60
|
+
outputs: outputs,
|
|
61
|
+
notification_arns: cf_stack.notification_arns,
|
|
62
|
+
stack_policy_body: stack_policy_body,
|
|
63
|
+
stack_status: cf_stack.stack_status)
|
|
64
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.generate(stack_definition, config)
|
|
69
|
+
parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
|
|
70
|
+
template_body = TemplateCompiler.compile(stack_definition.template_file_path)
|
|
71
|
+
parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash)
|
|
72
|
+
stack_policy_body = if stack_definition.stack_policy_file_path
|
|
73
|
+
File.read(stack_definition.stack_policy_file_path)
|
|
74
|
+
end
|
|
75
|
+
new(region: stack_definition.region,
|
|
76
|
+
stack_name: stack_definition.stack_name,
|
|
77
|
+
tags: stack_definition.tags,
|
|
78
|
+
parameters: parameters,
|
|
79
|
+
template_body: template_body,
|
|
80
|
+
notification_arns: stack_definition.notification_arns,
|
|
81
|
+
stack_policy_body: stack_policy_body)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def too_big?
|
|
85
|
+
maybe_compressed_template_body.size > MAX_TEMPLATE_SIZE
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def aws_parameters
|
|
89
|
+
Utils.hash_to_aws_parameters(parameters)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def aws_tags
|
|
93
|
+
Utils.hash_to_aws_tags(tags)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|