stack_master 1.6.0-x64-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +548 -0
- data/bin/stack_master +17 -0
- data/lib/stack_master.rb +159 -0
- data/lib/stack_master/aws_driver/cloud_formation.rb +41 -0
- data/lib/stack_master/aws_driver/s3.rb +68 -0
- data/lib/stack_master/change_set.rb +109 -0
- data/lib/stack_master/cli.rb +208 -0
- data/lib/stack_master/command.rb +57 -0
- data/lib/stack_master/commands/apply.rb +221 -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 +111 -0
- data/lib/stack_master/commands/list_stacks.rb +20 -0
- data/lib/stack_master/commands/outputs.rb +31 -0
- data/lib/stack_master/commands/resources.rb +33 -0
- data/lib/stack_master/commands/status.rb +46 -0
- data/lib/stack_master/commands/terminal_helper.rb +28 -0
- data/lib/stack_master/commands/validate.rb +17 -0
- data/lib/stack_master/config.rb +133 -0
- data/lib/stack_master/ctrl_c.rb +4 -0
- data/lib/stack_master/paged_response_accumulator.rb +29 -0
- data/lib/stack_master/parameter_loader.rb +49 -0
- data/lib/stack_master/parameter_resolver.rb +98 -0
- data/lib/stack_master/parameter_resolvers/ami_finder.rb +36 -0
- data/lib/stack_master/parameter_resolvers/env.rb +18 -0
- data/lib/stack_master/parameter_resolvers/latest_ami.rb +19 -0
- data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +18 -0
- data/lib/stack_master/parameter_resolvers/parameter_store.rb +31 -0
- data/lib/stack_master/parameter_resolvers/secret.rb +52 -0
- data/lib/stack_master/parameter_resolvers/security_group.rb +22 -0
- data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +31 -0
- data/lib/stack_master/parameter_resolvers/stack_output.rb +76 -0
- data/lib/stack_master/prompter.rb +21 -0
- data/lib/stack_master/resolver_array.rb +35 -0
- data/lib/stack_master/security_group_finder.rb +28 -0
- data/lib/stack_master/sns_topic_finder.rb +26 -0
- data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +35 -0
- data/lib/stack_master/sparkle_formation/compile_time/allowed_values_validator.rb +37 -0
- data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +33 -0
- data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +32 -0
- data/lib/stack_master/sparkle_formation/compile_time/max_length_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/max_size_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/min_length_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/min_size_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/number_validator.rb +35 -0
- data/lib/stack_master/sparkle_formation/compile_time/parameters_validator.rb +27 -0
- data/lib/stack_master/sparkle_formation/compile_time/state_builder.rb +32 -0
- data/lib/stack_master/sparkle_formation/compile_time/string_validator.rb +33 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +40 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +40 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_validator_factory.rb +41 -0
- data/lib/stack_master/sparkle_formation/template_file.rb +115 -0
- data/lib/stack_master/stack.rb +105 -0
- data/lib/stack_master/stack_definition.rb +103 -0
- data/lib/stack_master/stack_differ.rb +111 -0
- data/lib/stack_master/stack_events/fetcher.rb +38 -0
- data/lib/stack_master/stack_events/presenter.rb +27 -0
- data/lib/stack_master/stack_events/streamer.rb +68 -0
- data/lib/stack_master/stack_states.rb +34 -0
- data/lib/stack_master/stack_status.rb +61 -0
- data/lib/stack_master/template_compiler.rb +30 -0
- data/lib/stack_master/template_compilers/cfndsl.rb +13 -0
- data/lib/stack_master/template_compilers/json.rb +22 -0
- data/lib/stack_master/template_compilers/sparkle_formation.rb +71 -0
- data/lib/stack_master/template_compilers/yaml.rb +14 -0
- data/lib/stack_master/template_utils.rb +31 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +193 -0
- data/lib/stack_master/test_driver/s3.rb +34 -0
- data/lib/stack_master/testing.rb +9 -0
- data/lib/stack_master/utils.rb +50 -0
- data/lib/stack_master/validator.rb +33 -0
- data/lib/stack_master/version.rb +3 -0
- metadata +457 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class AmiFinder
|
4
|
+
def initialize(region)
|
5
|
+
@region = region
|
6
|
+
end
|
7
|
+
|
8
|
+
def build_filters_from_string(value, prefix = nil)
|
9
|
+
filters = value.split(',').map do |name_with_value|
|
10
|
+
name, value = name_with_value.strip.split('=')
|
11
|
+
name = prefix ? "#{prefix}:#{name}" : name
|
12
|
+
{ name: name, values: [value] }
|
13
|
+
end
|
14
|
+
filters
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_filters_from_hash(hash)
|
18
|
+
hash.map { |key, value| {name: key, values: Array(value.to_s)}}
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_latest_ami(filters, owners = ['self'])
|
22
|
+
images = ec2.describe_images(owners: owners, filters: filters).images
|
23
|
+
sorted_images = images.sort do |a, b|
|
24
|
+
Time.parse(a.creation_date) <=> Time.parse(b.creation_date)
|
25
|
+
end
|
26
|
+
sorted_images.last
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def ec2
|
32
|
+
@ec2 ||= Aws::EC2::Client.new(region: @region)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class Env < Resolver
|
4
|
+
|
5
|
+
def initialize(config, stack_definition)
|
6
|
+
@config = config
|
7
|
+
@stack_definition = stack_definition
|
8
|
+
end
|
9
|
+
|
10
|
+
def resolve(value)
|
11
|
+
environment_variable = ENV[value]
|
12
|
+
raise ArgumentError, "The environment variable #{value} is not set" if environment_variable.nil?
|
13
|
+
environment_variable
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class LatestAmi < Resolver
|
4
|
+
array_resolver class_name: 'LatestAmis'
|
5
|
+
|
6
|
+
def initialize(config, stack_definition)
|
7
|
+
@config = config
|
8
|
+
@stack_definition = stack_definition
|
9
|
+
@ami_finder = AmiFinder.new(@stack_definition.region)
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(value)
|
13
|
+
owners = Array(value.fetch('owners', 'self').to_s)
|
14
|
+
filters = @ami_finder.build_filters_from_hash(value.fetch('filters'))
|
15
|
+
@ami_finder.find_latest_ami(filters, owners).try(:image_id)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class LatestAmiByTags < Resolver
|
4
|
+
array_resolver class_name: 'LatestAmisByTags'
|
5
|
+
|
6
|
+
def initialize(config, stack_definition)
|
7
|
+
@config = config
|
8
|
+
@stack_definition = stack_definition
|
9
|
+
@ami_finder = AmiFinder.new(@stack_definition.region)
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(value)
|
13
|
+
filters = @ami_finder.build_filters_from_string(value, prefix = "tag")
|
14
|
+
@ami_finder.find_latest_ami(filters).try(:image_id)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class ParameterStore < Resolver
|
4
|
+
|
5
|
+
ParameterNotFound = Class.new(StandardError)
|
6
|
+
|
7
|
+
def initialize(config, stack_definition)
|
8
|
+
@config = config
|
9
|
+
@stack_definition = stack_definition
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(value)
|
13
|
+
begin
|
14
|
+
resp = ssm.get_parameter(
|
15
|
+
name: value,
|
16
|
+
with_decryption: true
|
17
|
+
)
|
18
|
+
rescue Aws::SSM::Errors::ParameterNotFound
|
19
|
+
raise ParameterNotFound, "Unable to find #{value} in Parameter Store"
|
20
|
+
end
|
21
|
+
resp.parameter.value
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def ssm
|
27
|
+
@ssm ||= Aws::SSM::Client.new(region: @stack_definition.region)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'os'
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
module ParameterResolvers
|
5
|
+
class Secret < Resolver
|
6
|
+
SecretNotFound = Class.new(StandardError)
|
7
|
+
PlatformNotSupported = Class.new(StandardError)
|
8
|
+
|
9
|
+
unless OS.windows?
|
10
|
+
require 'dotgpg'
|
11
|
+
array_resolver
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(config, stack_definition)
|
15
|
+
@config = config
|
16
|
+
@stack_definition = stack_definition
|
17
|
+
end
|
18
|
+
|
19
|
+
def resolve(value)
|
20
|
+
raise PlatformNotSupported, "The GPG Secret Parameter Resolver does not support Windows" if OS.windows?
|
21
|
+
secret_key = value
|
22
|
+
raise ArgumentError, "No secret_file defined for stack definition #{@stack_definition.stack_name} in #{@stack_definition.region}" unless !@stack_definition.secret_file.nil?
|
23
|
+
raise ArgumentError, "Could not find secret file at #{secret_file_path}" unless File.exist?(secret_file_path)
|
24
|
+
secrets_hash.fetch(secret_key) do
|
25
|
+
raise SecretNotFound, "Unable to find key #{secret_key} in file #{secret_file_path}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def secrets_hash
|
32
|
+
@secrets_hash ||= YAML.load(decrypt_with_dotgpg)
|
33
|
+
end
|
34
|
+
|
35
|
+
def decrypt_with_dotgpg
|
36
|
+
Dotgpg.interactive = true
|
37
|
+
dir = Dotgpg::Dir.closest(secret_file_path)
|
38
|
+
stream = StringIO.new
|
39
|
+
dir.decrypt(secret_path_relative_to_base, stream)
|
40
|
+
stream.string
|
41
|
+
end
|
42
|
+
|
43
|
+
def secret_path_relative_to_base
|
44
|
+
@secret_path_relative_to_base ||= File.join('secrets', @stack_definition.secret_file)
|
45
|
+
end
|
46
|
+
|
47
|
+
def secret_file_path
|
48
|
+
@secret_file_path ||= File.join(@config.base_dir, secret_path_relative_to_base)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class SecurityGroup < Resolver
|
4
|
+
array_resolver
|
5
|
+
|
6
|
+
def initialize(config, stack_definition)
|
7
|
+
@config = config
|
8
|
+
@stack_definition = stack_definition
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolve(value)
|
12
|
+
security_group_finder.find(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def security_group_finder
|
18
|
+
StackMaster::SecurityGroupFinder.new(@stack_definition.region)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class SnsTopicName < Resolver
|
4
|
+
TopicNotFound = Class.new(StandardError)
|
5
|
+
|
6
|
+
array_resolver
|
7
|
+
|
8
|
+
def initialize(config, stack_definition)
|
9
|
+
@config = config
|
10
|
+
@stack_definition = stack_definition
|
11
|
+
@stacks = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve(value)
|
15
|
+
sns_topic_finder.find(value)
|
16
|
+
rescue StackMaster::SnsTopicFinder::TopicNotFound => e
|
17
|
+
raise TopicNotFound.new(e.message)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def cf
|
23
|
+
@cf ||= StackMaster.cloud_formation_driver
|
24
|
+
end
|
25
|
+
|
26
|
+
def sns_topic_finder
|
27
|
+
StackMaster::SnsTopicFinder.new(@stack_definition.region)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class StackOutput < Resolver
|
4
|
+
StackNotFound = Class.new(StandardError)
|
5
|
+
StackOutputNotFound = Class.new(StandardError)
|
6
|
+
|
7
|
+
array_resolver
|
8
|
+
|
9
|
+
def initialize(config, stack_definition)
|
10
|
+
@config = config
|
11
|
+
@stack_definition = stack_definition
|
12
|
+
@stacks = {}
|
13
|
+
@cf_drivers = {}
|
14
|
+
@output_regex = %r{(?:(?<region>[^:]+):)?(?<stack_name>[^:/]+)/(?<output_name>.+)}
|
15
|
+
end
|
16
|
+
|
17
|
+
def resolve(value)
|
18
|
+
region, stack_name, output_name = parse!(value)
|
19
|
+
stack = find_stack(stack_name, region)
|
20
|
+
if stack
|
21
|
+
output = stack.outputs.find { |stack_output| stack_output.output_key == output_name.camelize }
|
22
|
+
if output
|
23
|
+
output.output_value
|
24
|
+
else
|
25
|
+
raise StackOutputNotFound, "Stack exists (#{stack_name}), but output does not: #{output_name}"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
raise StackNotFound, "Stack in StackOutput not found: #{value}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def cf
|
35
|
+
@cf ||= StackMaster.cloud_formation_driver
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse!(value)
|
39
|
+
if !value.is_a?(String) || !(match = @output_regex.match(value))
|
40
|
+
raise ArgumentError, 'Stack output values must be in the form of [region:]stack-name/output_name'
|
41
|
+
end
|
42
|
+
|
43
|
+
[
|
44
|
+
match[:region] || cf.region,
|
45
|
+
match[:stack_name],
|
46
|
+
match[:output_name]
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_stack(stack_name, region)
|
51
|
+
unaliased_region = @config.unalias_region(region)
|
52
|
+
stack_key = stack_key(stack_name, unaliased_region)
|
53
|
+
|
54
|
+
@stacks.fetch(stack_key) do
|
55
|
+
regional_cf = cf_for_region(unaliased_region)
|
56
|
+
cf_stack = regional_cf.describe_stacks(stack_name: stack_name).stacks.first
|
57
|
+
@stacks[stack_key] = cf_stack
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def stack_key(stack_name, region)
|
62
|
+
"#{region}:#{stack_name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def cf_for_region(region)
|
66
|
+
return cf if cf.region == region
|
67
|
+
|
68
|
+
@cf_drivers.fetch(region) do
|
69
|
+
cloud_formation_driver = cf.class.new
|
70
|
+
cloud_formation_driver.set_region(region)
|
71
|
+
@cf_drivers[region] = cloud_formation_driver
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module Prompter
|
3
|
+
def ask?(question)
|
4
|
+
StackMaster.stdout.print question
|
5
|
+
answer = if StackMaster.interactive?
|
6
|
+
if StackMaster.stdin.tty? && StackMaster.stdout.tty?
|
7
|
+
StackMaster.stdin.getch.chomp
|
8
|
+
else
|
9
|
+
StackMaster.stdout.puts
|
10
|
+
StackMaster.stdout.puts "STDOUT or STDIN was not a TTY. Defaulting to no. To force yes use -y"
|
11
|
+
'n'
|
12
|
+
end
|
13
|
+
else
|
14
|
+
print StackMaster.non_interactive_answer
|
15
|
+
StackMaster.non_interactive_answer
|
16
|
+
end
|
17
|
+
StackMaster.stdout.puts
|
18
|
+
answer == 'y'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class ResolverArray
|
4
|
+
def initialize(config, stack_definition)
|
5
|
+
@config = config
|
6
|
+
@stack_definition = stack_definition
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolve(values)
|
10
|
+
Array(values).map do |value|
|
11
|
+
resolver_class.new(@config, @stack_definition).resolve(value)
|
12
|
+
end.join(',')
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolver_class
|
16
|
+
fail "Method resolver_class not implemented on #{self.class}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Resolver
|
21
|
+
def self.array_resolver(options = {})
|
22
|
+
resolver_class ||= Object.const_get(self.name)
|
23
|
+
array_resolver_class_name = options[:class_name] || resolver_class.to_s.demodulize.pluralize
|
24
|
+
|
25
|
+
klass = Class.new(ResolverArray) do
|
26
|
+
define_method('resolver_class') do
|
27
|
+
resolver_class
|
28
|
+
end
|
29
|
+
end
|
30
|
+
StackMaster::ParameterResolvers.const_set("#{array_resolver_class_name}", klass)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,28 @@
|
|
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
|
+
raise ArgumentError, 'Security group references must be non-empty strings' unless reference.is_a?(String) && !reference.empty?
|
12
|
+
|
13
|
+
groups = @resource.security_groups({
|
14
|
+
filters: [
|
15
|
+
{
|
16
|
+
name: "group-name",
|
17
|
+
values: [reference],
|
18
|
+
},
|
19
|
+
],
|
20
|
+
})
|
21
|
+
|
22
|
+
raise SecurityGroupNotFound, "No security group with name #{reference} found" unless groups.any?
|
23
|
+
raise MultipleSecurityGroupsFound, "More than one security group with name #{reference} found" if groups.count > 1
|
24
|
+
|
25
|
+
groups.first.id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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
|
+
raise ArgumentError, 'SNS topic references must be non-empty strings' unless reference.is_a?(String) && !reference.empty?
|
11
|
+
|
12
|
+
topic = @resource.topics.detect { |t| topic_name_from_arn(t.arn) == reference }
|
13
|
+
|
14
|
+
raise TopicNotFound, "No topic with name #{reference} found" unless topic
|
15
|
+
|
16
|
+
topic.arn
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def topic_name_from_arn(arn)
|
22
|
+
arn.split(":")[5]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'value_validator'
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
module SparkleFormation
|
5
|
+
module CompileTime
|
6
|
+
class AllowedPatternValidator < ValueValidator
|
7
|
+
|
8
|
+
KEY = :allowed_pattern
|
9
|
+
|
10
|
+
def initialize(name, definition, parameter)
|
11
|
+
@name = name
|
12
|
+
@definition = definition
|
13
|
+
@parameter = parameter
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def check_is_valid
|
19
|
+
return true unless @definition.key?(KEY)
|
20
|
+
invalid_values.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def invalid_values
|
24
|
+
values = build_values(@definition, @parameter)
|
25
|
+
values.reject { |value| value.to_s.match(%r{#{@definition[KEY]}}) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_error
|
29
|
+
"#{@name}:#{invalid_values} does not match #{KEY}:#{@definition[KEY]}"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|