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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +208 -0
  7. data/Rakefile +11 -0
  8. data/apply_demo.gif +0 -0
  9. data/bin/stack_master +16 -0
  10. data/example/simple/Gemfile +3 -0
  11. data/example/simple/parameters/myapp_vpc.yml +1 -0
  12. data/example/simple/parameters/myapp_web.yml +2 -0
  13. data/example/simple/stack_master.yml +13 -0
  14. data/example/simple/templates/myapp_vpc.rb +39 -0
  15. data/example/simple/templates/myapp_web.rb +16 -0
  16. data/features/apply.feature +241 -0
  17. data/features/delete.feature +43 -0
  18. data/features/diff.feature +191 -0
  19. data/features/events.feature +38 -0
  20. data/features/init.feature +6 -0
  21. data/features/outputs.feature +49 -0
  22. data/features/region_aliases.feature +66 -0
  23. data/features/resources.feature +45 -0
  24. data/features/stack_defaults.feature +88 -0
  25. data/features/status.feature +124 -0
  26. data/features/step_definitions/stack_steps.rb +50 -0
  27. data/features/support/env.rb +14 -0
  28. data/lib/stack_master.rb +81 -0
  29. data/lib/stack_master/aws_driver/cloud_formation.rb +56 -0
  30. data/lib/stack_master/cli.rb +164 -0
  31. data/lib/stack_master/command.rb +13 -0
  32. data/lib/stack_master/commands/apply.rb +104 -0
  33. data/lib/stack_master/commands/delete.rb +53 -0
  34. data/lib/stack_master/commands/diff.rb +31 -0
  35. data/lib/stack_master/commands/events.rb +39 -0
  36. data/lib/stack_master/commands/init.rb +109 -0
  37. data/lib/stack_master/commands/list_stacks.rb +16 -0
  38. data/lib/stack_master/commands/outputs.rb +27 -0
  39. data/lib/stack_master/commands/resources.rb +33 -0
  40. data/lib/stack_master/commands/status.rb +47 -0
  41. data/lib/stack_master/commands/validate.rb +17 -0
  42. data/lib/stack_master/config.rb +86 -0
  43. data/lib/stack_master/ctrl_c.rb +4 -0
  44. data/lib/stack_master/parameter_loader.rb +17 -0
  45. data/lib/stack_master/parameter_resolver.rb +45 -0
  46. data/lib/stack_master/parameter_resolvers/secret.rb +42 -0
  47. data/lib/stack_master/parameter_resolvers/security_group.rb +20 -0
  48. data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +29 -0
  49. data/lib/stack_master/parameter_resolvers/stack_output.rb +53 -0
  50. data/lib/stack_master/prompter.rb +14 -0
  51. data/lib/stack_master/security_group_finder.rb +29 -0
  52. data/lib/stack_master/sns_topic_finder.rb +27 -0
  53. data/lib/stack_master/stack.rb +96 -0
  54. data/lib/stack_master/stack_definition.rb +49 -0
  55. data/lib/stack_master/stack_differ.rb +80 -0
  56. data/lib/stack_master/stack_events/fetcher.rb +45 -0
  57. data/lib/stack_master/stack_events/presenter.rb +27 -0
  58. data/lib/stack_master/stack_events/streamer.rb +55 -0
  59. data/lib/stack_master/stack_states.rb +34 -0
  60. data/lib/stack_master/template_compiler.rb +21 -0
  61. data/lib/stack_master/test_driver/cloud_formation.rb +139 -0
  62. data/lib/stack_master/testing.rb +7 -0
  63. data/lib/stack_master/utils.rb +31 -0
  64. data/lib/stack_master/validator.rb +25 -0
  65. data/lib/stack_master/version.rb +3 -0
  66. data/logo.png +0 -0
  67. data/script/buildkite/bundle.sh +5 -0
  68. data/script/buildkite/clean.sh +3 -0
  69. data/script/buildkite_rspec.sh +27 -0
  70. data/spec/fixtures/parameters/myapp_vpc.yml +1 -0
  71. data/spec/fixtures/stack_master.yml +35 -0
  72. data/spec/fixtures/templates/myapp_vpc.json +1 -0
  73. data/spec/spec_helper.rb +99 -0
  74. data/spec/stack_master/commands/apply_spec.rb +92 -0
  75. data/spec/stack_master/commands/delete_spec.rb +40 -0
  76. data/spec/stack_master/commands/init_spec.rb +17 -0
  77. data/spec/stack_master/commands/status_spec.rb +38 -0
  78. data/spec/stack_master/commands/validate_spec.rb +26 -0
  79. data/spec/stack_master/config_spec.rb +81 -0
  80. data/spec/stack_master/parameter_loader_spec.rb +81 -0
  81. data/spec/stack_master/parameter_resolver_spec.rb +58 -0
  82. data/spec/stack_master/parameter_resolvers/secret_spec.rb +66 -0
  83. data/spec/stack_master/parameter_resolvers/security_group_spec.rb +17 -0
  84. data/spec/stack_master/parameter_resolvers/sns_topic_name_spec.rb +43 -0
  85. data/spec/stack_master/parameter_resolvers/stack_output_spec.rb +77 -0
  86. data/spec/stack_master/security_group_finder_spec.rb +49 -0
  87. data/spec/stack_master/sns_topic_finder_spec.rb +25 -0
  88. data/spec/stack_master/stack_definition_spec.rb +37 -0
  89. data/spec/stack_master/stack_differ_spec.rb +34 -0
  90. data/spec/stack_master/stack_events/fetcher_spec.rb +65 -0
  91. data/spec/stack_master/stack_events/presenter_spec.rb +18 -0
  92. data/spec/stack_master/stack_events/streamer_spec.rb +33 -0
  93. data/spec/stack_master/stack_spec.rb +157 -0
  94. data/spec/stack_master/template_compiler_spec.rb +48 -0
  95. data/spec/stack_master/test_driver/cloud_formation_spec.rb +24 -0
  96. data/spec/stack_master/utils_spec.rb +30 -0
  97. data/spec/stack_master/validator_spec.rb +38 -0
  98. data/stack_master.gemspec +38 -0
  99. data/stacktemplates/parameter_region.yml +3 -0
  100. data/stacktemplates/parameter_stack_name.yml +3 -0
  101. data/stacktemplates/stack.json.erb +20 -0
  102. data/stacktemplates/stack_master.yml.erb +6 -0
  103. metadata +427 -0
@@ -0,0 +1,13 @@
1
+ module StackMaster
2
+ module Command
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def perform(*args)
9
+ new(*args).perform
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,104 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Apply
4
+ include Command
5
+ include Commander::UI
6
+ include StackMaster::Prompter
7
+
8
+ def initialize(config, stack_definition, options = {})
9
+ @config = config
10
+ @stack_definition = stack_definition
11
+ @from_time = Time.now
12
+ @updating = false
13
+ end
14
+
15
+ def perform
16
+ diff_stacks
17
+ unless ask?("Continue and apply the stack (y/n)? ")
18
+ StackMaster.stdout.puts "Stack update aborted"
19
+ return
20
+ end
21
+ begin
22
+ return if stack_too_big
23
+ create_or_update_stack
24
+ tail_stack_events
25
+ rescue StackMaster::CtrlC
26
+ cancel
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def cf
33
+ @cf ||= StackMaster.cloud_formation_driver
34
+ end
35
+
36
+ def stack
37
+ @stack ||= Stack.find(@stack_definition.region, @stack_definition.stack_name)
38
+ end
39
+
40
+ def proposed_stack
41
+ @proposed_stack ||= Stack.generate(@stack_definition, @config)
42
+ end
43
+
44
+ def stack_exists?
45
+ !stack.nil?
46
+ end
47
+
48
+ def diff_stacks
49
+ StackDiffer.new(proposed_stack, stack).output_diff
50
+ end
51
+
52
+ def cancel
53
+ if @updating
54
+ if ask?("Cancel stack update?")
55
+ StackMaster.stdout.puts "Attempting to cancel stack update"
56
+ cf.cancel_update_stack({stack_name: @stack_definition.stack_name})
57
+ tail_stack_events
58
+ end
59
+ end
60
+ end
61
+
62
+ def create_or_update_stack
63
+ if stack_exists?
64
+ update_stack
65
+ else
66
+ create_stack
67
+ end
68
+ end
69
+
70
+ def stack_too_big
71
+ if proposed_stack.too_big?
72
+ StackMaster.stdout.puts 'The (space compressed) stack is larger than the limit set by AWS. See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html'
73
+ true
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ def update_stack
80
+ @updating = true
81
+ cf.update_stack(stack_options)
82
+ end
83
+
84
+ def create_stack
85
+ cf.create_stack(stack_options.merge(tags: proposed_stack.aws_tags))
86
+ end
87
+
88
+ def stack_options
89
+ {
90
+ stack_name: @stack_definition.stack_name,
91
+ template_body: proposed_stack.maybe_compressed_template_body,
92
+ parameters: proposed_stack.aws_parameters,
93
+ capabilities: ['CAPABILITY_IAM'],
94
+ notification_arns: proposed_stack.notification_arns,
95
+ stack_policy_body: proposed_stack.stack_policy_body
96
+ }
97
+ end
98
+
99
+ def tail_stack_events
100
+ StackEvents::Streamer.stream(@stack_definition.stack_name, @stack_definition.region, io: StackMaster.stdout, from: @from_time)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,53 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Delete
4
+ include Command
5
+ include StackMaster::Prompter
6
+
7
+ def initialize(region, stack_name)
8
+ @region = region
9
+ @stack_name = stack_name
10
+ @from_time = Time.now
11
+ end
12
+
13
+ def perform
14
+
15
+ return unless check_exists
16
+
17
+ unless ask?("Really delete stack (y/n)? ")
18
+ StackMaster.stdout.puts "Stack update aborted"
19
+ return
20
+ end
21
+
22
+ delete_stack
23
+ tail_stack_events
24
+ end
25
+
26
+ private
27
+
28
+ def delete_stack
29
+ cf.delete_stack({stack_name: @stack_name})
30
+ end
31
+
32
+ def check_exists
33
+ cf.describe_stacks({stack_name: @stack_name})
34
+ true
35
+ rescue Aws::CloudFormation::Errors::ValidationError
36
+ StackMaster.stdout.puts "Stack does not exist"
37
+ false
38
+ end
39
+
40
+ def cf
41
+ StackMaster.cloud_formation_driver
42
+ end
43
+
44
+ def tail_stack_events
45
+ StackEvents::Streamer.stream(@stack_name, @region, io: StackMaster.stdout, from: @from_time)
46
+ StackMaster.stdout.puts "Stack deleted"
47
+ rescue Aws::CloudFormation::Errors::ValidationError
48
+ # Unfortunately the stack as a tendency of going away before we get the final delete event.
49
+ StackMaster.stdout.puts "Stack deleted"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Diff
4
+ include Command
5
+ include Commander::UI
6
+
7
+ def initialize(config, stack_definition, options = {})
8
+ @config = config
9
+ @stack_definition = stack_definition
10
+ end
11
+
12
+ def perform
13
+ StackMaster::StackDiffer.new(proposed_stack, stack).output_diff
14
+ end
15
+
16
+ private
17
+
18
+ def stack_definition
19
+ @stack_definition ||= @config.find_stack(@region, @stack_name)
20
+ end
21
+
22
+ def stack
23
+ @stack ||= Stack.find(@stack_definition.region, @stack_definition.stack_name)
24
+ end
25
+
26
+ def proposed_stack
27
+ @proposed_stack ||= Stack.generate(stack_definition, @config)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Events
4
+ include Command
5
+ include Commander::UI
6
+
7
+ def initialize(config, stack_definition, options = {})
8
+ @config = config
9
+ @stack_definition = stack_definition
10
+ @options = options
11
+ end
12
+
13
+ def perform
14
+ events = StackEvents::Fetcher.fetch(@stack_definition.stack_name, @stack_definition.region)
15
+ filter_events(events).each do |event|
16
+ StackEvents::Presenter.print_event(StackMaster.stdout, event)
17
+ end
18
+ if @options.tail
19
+ StackEvents::Streamer.stream(@stack_definition.stack_name, @stack_definition.region, io: StackMaster.stdout)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def filter_events(events)
26
+ if @options.all
27
+ events
28
+ else
29
+ n = @options.number || 25
30
+ from = events.count - n
31
+ if from < 0
32
+ from = 0
33
+ end
34
+ events[from..-1]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,109 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Init
4
+ include Command
5
+
6
+ def initialize(overwrite, region, stack_name)
7
+ @overwrite = overwrite
8
+ @region = region
9
+ @stack_name = stack_name
10
+ end
11
+
12
+ def perform
13
+ if check_files
14
+ create_stack_master_yml
15
+ create_stack_json_yml
16
+ create_parameters_yml
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def check_files
23
+ @stack_master_filename = "stack_master.yml"
24
+ @stack_json_filename = "templates/#{@stack_name}.json"
25
+ @parameters_filename = File.join("parameters", "#{underscored_stack_name}.yml")
26
+ @region_parameters_filename = File.join("parameters", @region, "#{underscored_stack_name}.yml")
27
+
28
+ if !@overwrite
29
+ [@stack_master_filename, @stack_json_filename, @parameters_filename, @region_parameters_filename].each do |filename|
30
+ if File.exists?(filename)
31
+ StackMaster.stderr.puts("Aborting: #{filename} already exists. Use --overwrite to force overwriting file.")
32
+ return false
33
+ end
34
+ end
35
+ end
36
+ true
37
+ end
38
+
39
+ def create_stack_json_yml
40
+ StackMaster.stdout.puts "Writing #{@stack_json_filename}"
41
+ FileUtils.mkdir_p(File.dirname(@stack_json_filename))
42
+ IO.write(@stack_json_filename, stack_json_output)
43
+ end
44
+
45
+ def stack_json_output
46
+ render ERB.new(File.read(stack_json_template))
47
+ end
48
+
49
+ def stack_json_template
50
+ File.join(StackMaster.base_dir, "stacktemplates", "stack.json.erb")
51
+ end
52
+
53
+ def create_stack_master_yml
54
+ StackMaster.stdout.puts "Writing #{@stack_master_filename}"
55
+ IO.write("#{@stack_master_filename}", stack_master_yml_output)
56
+ end
57
+
58
+ def stack_master_yml_output
59
+ render ERB.new(File.read(stack_master_template))
60
+ end
61
+
62
+ def stack_master_template
63
+ File.join(StackMaster.base_dir, "stacktemplates", "stack_master.yml.erb")
64
+ end
65
+
66
+ def create_parameters_yml
67
+ StackMaster.stdout.puts "Writing #{@parameters_filename}"
68
+ StackMaster.stdout.puts "Writing #{@region_parameters_filename}"
69
+ FileUtils.mkdir_p("parameters/#{@region}")
70
+ IO.write(@parameters_filename, parameter_stack_name_yml_output)
71
+ IO.write(@region_parameters_filename, parameter_region_yml_output)
72
+ end
73
+
74
+ def parameter_stack_name_yml_output
75
+ File.read(parameter_stack_name_template)
76
+ end
77
+
78
+ def parameter_region_yml_output
79
+ File.read(parameter_region_template)
80
+ end
81
+
82
+ def parameter_stack_name_template
83
+ File.join(StackMaster.base_dir, "stacktemplates", "parameter_stack_name.yml")
84
+ end
85
+
86
+ def parameter_region_template
87
+ File.join(StackMaster.base_dir, "stacktemplates", "parameter_region.yml")
88
+ end
89
+
90
+ def underscored_stack_name
91
+ @stack_name.gsub('-', '_')
92
+ end
93
+
94
+ def render(renderer)
95
+ binding = InitBinding.new(region: @region, stack_name: @stack_name).get_binding
96
+ renderer.result(binding)
97
+ end
98
+
99
+ class InitBinding
100
+ def initialize(region:, stack_name:)
101
+ @region = region
102
+ @stack_name = stack_name
103
+ end
104
+
105
+ attr_reader :region, :stack_name
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,16 @@
1
+ module StackMaster
2
+ module Commands
3
+ class ListStacks
4
+ include Command
5
+ include Commander::UI
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def perform
12
+ tp @config.stacks, :region, :stack_name
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Outputs
4
+ include Command
5
+ include Commander::UI
6
+
7
+ def initialize(config, stack_definition, options = {})
8
+ @config = config
9
+ @stack_definition = stack_definition
10
+ end
11
+
12
+ def perform
13
+ if stack
14
+ tp stack.outputs, :output_key, :output_value, :description
15
+ else
16
+ StackMaster.stdout.puts "Stack doesn't exist"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def stack
23
+ @stack ||= Stack.find(@stack_definition.region, @stack_definition.stack_name)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Resources
4
+ include Command
5
+ include Commander::UI
6
+
7
+ def initialize(config, stack_definition, options = {})
8
+ @config = config
9
+ @stack_definition = stack_definition
10
+ end
11
+
12
+ def perform
13
+ if stack_resources
14
+ tp stack_resources, :logical_resource_id, :resource_type, :timestamp, :resource_status, :resource_status_reason, :description
15
+ else
16
+ StackMaster.stdout.puts "Stack doesn't exist"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def stack_resources
23
+ @stack_resources = cf.describe_stack_resources(stack_name: @stack_definition.stack_name).stack_resources
24
+ rescue Aws::CloudFormation::Errors::ValidationError
25
+ nil
26
+ end
27
+
28
+ def cf
29
+ @cf ||= StackMaster.cloud_formation_driver
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ module StackMaster
2
+ module Commands
3
+ class Status
4
+ include Command
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def perform
11
+ tp.set :io, StackMaster.stdout
12
+ tp @config.stacks.map { |stack_definition| get_status(stack_definition) }
13
+ end
14
+
15
+ private
16
+
17
+ def sort_params(hash)
18
+ hash.sort.to_h
19
+ end
20
+
21
+ def get_status(stack_definition)
22
+ region = stack_definition.region
23
+ stack_name = stack_definition.stack_name
24
+ begin
25
+ driver = StackMaster.cloud_formation_driver
26
+ driver.set_region(region)
27
+ stack = Stack.find(region, stack_name)
28
+ if stack
29
+ proposed_stack = Stack.generate(stack_definition, @config)
30
+ differ = StackMaster::StackDiffer.new(proposed_stack, stack)
31
+ different = differ.body_different? || differ.params_different?
32
+ stack_status = stack.stack_status
33
+ else
34
+ different = true
35
+ stack_status = nil
36
+ end
37
+ rescue Aws::CloudFormation::Errors::ValidationError
38
+ stack_status = "missing"
39
+ different = true
40
+ end
41
+
42
+ { region: region, stack_name: stack_name, stack_status: stack_status, different: different ? "Yes" : "No" }
43
+ end
44
+
45
+ end
46
+ end
47
+ end