stack_master 1.6.0-x64-mingw32

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.
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