stack_master 1.6.0-x64-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +548 -0
  3. data/bin/stack_master +17 -0
  4. data/lib/stack_master.rb +159 -0
  5. data/lib/stack_master/aws_driver/cloud_formation.rb +41 -0
  6. data/lib/stack_master/aws_driver/s3.rb +68 -0
  7. data/lib/stack_master/change_set.rb +109 -0
  8. data/lib/stack_master/cli.rb +208 -0
  9. data/lib/stack_master/command.rb +57 -0
  10. data/lib/stack_master/commands/apply.rb +221 -0
  11. data/lib/stack_master/commands/delete.rb +53 -0
  12. data/lib/stack_master/commands/diff.rb +31 -0
  13. data/lib/stack_master/commands/events.rb +39 -0
  14. data/lib/stack_master/commands/init.rb +111 -0
  15. data/lib/stack_master/commands/list_stacks.rb +20 -0
  16. data/lib/stack_master/commands/outputs.rb +31 -0
  17. data/lib/stack_master/commands/resources.rb +33 -0
  18. data/lib/stack_master/commands/status.rb +46 -0
  19. data/lib/stack_master/commands/terminal_helper.rb +28 -0
  20. data/lib/stack_master/commands/validate.rb +17 -0
  21. data/lib/stack_master/config.rb +133 -0
  22. data/lib/stack_master/ctrl_c.rb +4 -0
  23. data/lib/stack_master/paged_response_accumulator.rb +29 -0
  24. data/lib/stack_master/parameter_loader.rb +49 -0
  25. data/lib/stack_master/parameter_resolver.rb +98 -0
  26. data/lib/stack_master/parameter_resolvers/ami_finder.rb +36 -0
  27. data/lib/stack_master/parameter_resolvers/env.rb +18 -0
  28. data/lib/stack_master/parameter_resolvers/latest_ami.rb +19 -0
  29. data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +18 -0
  30. data/lib/stack_master/parameter_resolvers/parameter_store.rb +31 -0
  31. data/lib/stack_master/parameter_resolvers/secret.rb +52 -0
  32. data/lib/stack_master/parameter_resolvers/security_group.rb +22 -0
  33. data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +31 -0
  34. data/lib/stack_master/parameter_resolvers/stack_output.rb +76 -0
  35. data/lib/stack_master/prompter.rb +21 -0
  36. data/lib/stack_master/resolver_array.rb +35 -0
  37. data/lib/stack_master/security_group_finder.rb +28 -0
  38. data/lib/stack_master/sns_topic_finder.rb +26 -0
  39. data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +35 -0
  40. data/lib/stack_master/sparkle_formation/compile_time/allowed_values_validator.rb +37 -0
  41. data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +33 -0
  42. data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +32 -0
  43. data/lib/stack_master/sparkle_formation/compile_time/max_length_validator.rb +36 -0
  44. data/lib/stack_master/sparkle_formation/compile_time/max_size_validator.rb +36 -0
  45. data/lib/stack_master/sparkle_formation/compile_time/min_length_validator.rb +36 -0
  46. data/lib/stack_master/sparkle_formation/compile_time/min_size_validator.rb +36 -0
  47. data/lib/stack_master/sparkle_formation/compile_time/number_validator.rb +35 -0
  48. data/lib/stack_master/sparkle_formation/compile_time/parameters_validator.rb +27 -0
  49. data/lib/stack_master/sparkle_formation/compile_time/state_builder.rb +32 -0
  50. data/lib/stack_master/sparkle_formation/compile_time/string_validator.rb +33 -0
  51. data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +40 -0
  52. data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +40 -0
  53. data/lib/stack_master/sparkle_formation/compile_time/value_validator_factory.rb +41 -0
  54. data/lib/stack_master/sparkle_formation/template_file.rb +115 -0
  55. data/lib/stack_master/stack.rb +105 -0
  56. data/lib/stack_master/stack_definition.rb +103 -0
  57. data/lib/stack_master/stack_differ.rb +111 -0
  58. data/lib/stack_master/stack_events/fetcher.rb +38 -0
  59. data/lib/stack_master/stack_events/presenter.rb +27 -0
  60. data/lib/stack_master/stack_events/streamer.rb +68 -0
  61. data/lib/stack_master/stack_states.rb +34 -0
  62. data/lib/stack_master/stack_status.rb +61 -0
  63. data/lib/stack_master/template_compiler.rb +30 -0
  64. data/lib/stack_master/template_compilers/cfndsl.rb +13 -0
  65. data/lib/stack_master/template_compilers/json.rb +22 -0
  66. data/lib/stack_master/template_compilers/sparkle_formation.rb +71 -0
  67. data/lib/stack_master/template_compilers/yaml.rb +14 -0
  68. data/lib/stack_master/template_utils.rb +31 -0
  69. data/lib/stack_master/test_driver/cloud_formation.rb +193 -0
  70. data/lib/stack_master/test_driver/s3.rb +34 -0
  71. data/lib/stack_master/testing.rb +9 -0
  72. data/lib/stack_master/utils.rb +50 -0
  73. data/lib/stack_master/validator.rb +33 -0
  74. data/lib/stack_master/version.rb +3 -0
  75. 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