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,49 @@
1
+ module StackMaster
2
+ class StackDefinition
3
+ include Virtus.value_object(strict: true, required: false)
4
+
5
+ values do
6
+ attribute :region, String
7
+ attribute :stack_name, String
8
+ attribute :template, String
9
+ attribute :tags, Hash
10
+ attribute :notification_arns, Array[String]
11
+ attribute :base_dir, String
12
+ attribute :secret_file, String
13
+ attribute :stack_policy_file, String
14
+ attribute :additional_parameter_lookup_dirs, Array[String]
15
+ end
16
+
17
+ def template_file_path
18
+ File.join(base_dir, 'templates', template)
19
+ end
20
+
21
+ def parameter_files
22
+ [ default_parameter_file_path, region_parameter_file_path ] + additional_parameter_lookup_file_paths
23
+ end
24
+
25
+ def stack_policy_file_path
26
+ File.join(base_dir, 'policies', stack_policy_file) if stack_policy_file
27
+ end
28
+
29
+ private
30
+
31
+ def additional_parameter_lookup_file_paths
32
+ additional_parameter_lookup_dirs.map do |a|
33
+ File.join(base_dir, 'parameters', a, "#{underscored_stack_name}.yml")
34
+ end
35
+ end
36
+
37
+ def region_parameter_file_path
38
+ File.join(base_dir, 'parameters', "#{region}", "#{underscored_stack_name}.yml")
39
+ end
40
+
41
+ def default_parameter_file_path
42
+ File.join(base_dir, 'parameters', "#{underscored_stack_name}.yml")
43
+ end
44
+
45
+ def underscored_stack_name
46
+ stack_name.gsub('-', '_')
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,80 @@
1
+ module StackMaster
2
+ class StackDiffer
3
+ def initialize(proposed_stack, current_stack)
4
+ @proposed_stack = proposed_stack
5
+ @current_stack = current_stack
6
+ end
7
+
8
+ def proposed_template
9
+ JSON.pretty_generate(JSON.parse(@proposed_stack.template_body))
10
+ end
11
+
12
+ def current_template
13
+ JSON.pretty_generate(@current_stack.template_hash)
14
+ end
15
+
16
+ def current_parameters
17
+ YAML.dump(sort_params(@current_stack.parameters_with_defaults))
18
+ end
19
+
20
+ def proposed_parameters
21
+ YAML.dump(sort_params(@proposed_stack.parameters_with_defaults))
22
+ end
23
+
24
+ def body_different?
25
+ Diffy::Diff.new(current_template, proposed_template, {}).to_s != ''
26
+ end
27
+
28
+ def params_different?
29
+ Diffy::Diff.new(current_parameters, proposed_parameters, {}).to_s != ''
30
+ end
31
+
32
+ def output_diff
33
+ if @current_stack
34
+ text_diff('Stack', current_template, proposed_template, context: 7, include_diff_info: true)
35
+ text_diff('Parameters', current_parameters, proposed_parameters)
36
+ else
37
+ text_diff('Stack', '', proposed_template)
38
+ text_diff('Parameters', '', proposed_parameters)
39
+ StackMaster.stdout.puts "No stack found"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def text_diff(thing, current, proposed, diff_opts = {})
46
+ diff = Diffy::Diff.new(current, proposed, diff_opts).to_s
47
+ StackMaster.stdout.print "#{thing} diff: "
48
+ if diff == ''
49
+ StackMaster.stdout.puts "No changes"
50
+ else
51
+ StackMaster.stdout.puts
52
+ diff.each_line do |line|
53
+ if line.start_with?('+')
54
+ StackMaster.stdout.print colorize(line, :green)
55
+ elsif line.start_with?('-')
56
+ StackMaster.stdout.print colorize(line, :red)
57
+ else
58
+ StackMaster.stdout.print line
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def sort_params(hash)
65
+ hash.sort.to_h
66
+ end
67
+
68
+ def colorize(text, color)
69
+ if colorize?
70
+ text.colorize(color)
71
+ else
72
+ text
73
+ end
74
+ end
75
+
76
+ def colorize?
77
+ ENV.fetch('COLORIZE') { 'true' } == 'true'
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,45 @@
1
+ module StackMaster
2
+ class StackEvents
3
+ class Fetcher
4
+ def self.fetch(*args)
5
+ new(*args).fetch
6
+ end
7
+
8
+ def initialize(stack_name, region, from: nil)
9
+ @stack_name = stack_name
10
+ @region = region
11
+ @from = from
12
+ end
13
+
14
+ def fetch
15
+ events = fetch_events
16
+ if @from
17
+ filter_old_events(events)
18
+ else
19
+ events
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def cf
26
+ @cf ||= StackMaster.cloud_formation_driver
27
+ end
28
+
29
+ def filter_old_events(events)
30
+ events.select { |event| event.timestamp > @from }
31
+ end
32
+
33
+ def fetch_events
34
+ events = []
35
+ next_token = nil
36
+ begin
37
+ response = cf.describe_stack_events(stack_name: @stack_name, next_token: next_token)
38
+ next_token = response.next_token
39
+ events += response.stack_events
40
+ end while !next_token.nil?
41
+ events.reverse
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module StackMaster
2
+ class StackEvents
3
+ class Presenter
4
+ def self.print_event(io, event)
5
+ new(io).print_event(event)
6
+ end
7
+
8
+ def initialize(io)
9
+ @io = io
10
+ end
11
+
12
+ def print_event(event)
13
+ @io.puts "#{event.timestamp.localtime} #{event.logical_resource_id} #{event.resource_type} #{event.resource_status} #{event.resource_status_reason}".colorize(event_colour(event))
14
+ end
15
+
16
+ def event_colour(event)
17
+ if StackStates.failure_state?(event.resource_status)
18
+ :red
19
+ elsif StackStates.success_state?(event.resource_status)
20
+ :green
21
+ else
22
+ :yellow
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ module StackMaster
2
+ class StackEvents
3
+ class Streamer
4
+ def self.stream(*args, &block)
5
+ new(*args, &block).stream
6
+ end
7
+
8
+ def initialize(stack_name, region, from: Time.now, break_on_finish_state: true, sleep_between_fetches: 1, io: nil, &block)
9
+ @stack_name = stack_name
10
+ @region = region
11
+ @block = block
12
+ @seen_events = Set.new
13
+ @from = from
14
+ @break_on_finish_state = break_on_finish_state
15
+ @sleep_between_fetches = sleep_between_fetches
16
+ @io = io
17
+ end
18
+
19
+ def stream
20
+ catch(:halt) do
21
+ loop do
22
+ events = Fetcher.fetch(@stack_name, @region, from: @from)
23
+ unseen_events(events).each do |event|
24
+ @block.call(event) if @block
25
+ Presenter.print_event(@io, event) if @io
26
+ if @break_on_finish_state && finish_state?(event)
27
+ throw :halt
28
+ end
29
+ end
30
+ sleep @sleep_between_fetches
31
+ end
32
+ end
33
+ rescue Interrupt
34
+ end
35
+
36
+ private
37
+
38
+ def unseen_events(events)
39
+ [].tap do |unseen_events|
40
+ events.each do |event|
41
+ next if @seen_events.include?(event.event_id)
42
+ @seen_events << event.event_id
43
+ unseen_events << event
44
+ end
45
+ end
46
+ end
47
+
48
+ def finish_state?(event)
49
+ StackStates.finish_state?(event.resource_status) &&
50
+ event.resource_type == 'AWS::CloudFormation::Stack' &&
51
+ event.logical_resource_id == @stack_name
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ module StackMaster
2
+ module StackStates
3
+ SUCCESS_STATES = %w[
4
+ CREATE_COMPLETE
5
+ UPDATE_COMPLETE
6
+ DELETE_COMPLETE
7
+ ].freeze
8
+ FAILURE_STATES = %w[
9
+ CREATE_FAILED
10
+ DELETE_FAILED
11
+ UPDATE_ROLLBACK_FAILED
12
+ ROLLBACK_FAILED
13
+ ROLLBACK_COMPLETE
14
+ ROLLBACK_FAILED
15
+ UPDATE_ROLLBACK_COMPLETE
16
+ UPDATE_ROLLBACK_FAILED
17
+ ].freeze
18
+ FINISH_STATES = (SUCCESS_STATES + FAILURE_STATES).freeze
19
+
20
+ extend self
21
+
22
+ def finish_state?(state)
23
+ FINISH_STATES.include?(state)
24
+ end
25
+
26
+ def failure_state?(state)
27
+ FAILURE_STATES.include?(state)
28
+ end
29
+
30
+ def success_state?(state)
31
+ SUCCESS_STATES.include?(state)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module StackMaster
2
+ class TemplateCompiler
3
+
4
+ MAX_TEMPLATE_SIZE = 51200
5
+
6
+ def self.compile(template_file_path)
7
+ if template_file_path.ends_with?('.rb')
8
+ SparkleFormation.sparkle_path = File.dirname(template_file_path)
9
+ JSON.pretty_generate(SparkleFormation.compile(template_file_path))
10
+ else
11
+ template_body = File.read(template_file_path)
12
+ if template_body.size > MAX_TEMPLATE_SIZE
13
+ # Parse the json and rewrite compressed
14
+ JSON.dump(JSON.parse(template_body))
15
+ else
16
+ template_body
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,139 @@
1
+ module StackMaster
2
+ module TestDriver
3
+ class Stack
4
+ include Virtus.model
5
+ attribute :stack_id, String
6
+ attribute :stack_name, String
7
+ attribute :description, String
8
+ attribute :parameters, Array[OpenStruct]
9
+ attribute :creation_time, String
10
+ attribute :last_update_time, String
11
+ attribute :stack_status, String
12
+ attribute :stack_status_reason, String
13
+ attribute :disable_rollback, String
14
+ attribute :notification_arns, Array
15
+ attribute :timeout_in_minutes, Integer
16
+ attribute :capabilities, Array
17
+ attribute :outputs, Array[OpenStruct]
18
+ attribute :tags, Array[OpenStruct]
19
+ end
20
+
21
+ class StackEvent
22
+ include Virtus.model
23
+ attribute :stack_id, String
24
+ attribute :event_id, String
25
+ attribute :stack_name, String
26
+ attribute :logical_resource_id, String
27
+ attribute :physical_resource_id, String
28
+ attribute :resource_type, String
29
+ attribute :timestamp, Time
30
+ attribute :resource_status, String
31
+ attribute :resource_status_reason, String
32
+ attribute :resource_properties, String
33
+ end
34
+
35
+ class StackResource
36
+ include Virtus.model
37
+ attribute :stack_name, String
38
+ attribute :stack_id, String
39
+ attribute :logical_resource_id, String
40
+ attribute :physical_resource_id, String
41
+ attribute :resource_type, String
42
+ attribute :timestamp, Time
43
+ attribute :resource_status, String
44
+ attribute :resource_status_reason, String
45
+ attribute :description, String
46
+ end
47
+
48
+ class CloudFormation
49
+ def initialize
50
+ reset
51
+ end
52
+
53
+ def set_region(region)
54
+ @region = region
55
+ end
56
+
57
+ def reset
58
+ @stacks = {}
59
+ @templates = {}
60
+ @stack_events = {}
61
+ @stack_resources = {}
62
+ @stack_policies = {}
63
+ end
64
+
65
+ def describe_stacks(options = {})
66
+ stack_name = options[:stack_name]
67
+ stacks = if stack_name
68
+ if @stacks[stack_name]
69
+ [@stacks[stack_name]]
70
+ else
71
+ raise Aws::CloudFormation::Errors::ValidationError.new('', 'Stack does not exist')
72
+ end
73
+ else
74
+ @stacks.values
75
+ end
76
+ OpenStruct.new(stacks: stacks, next_token: nil)
77
+ end
78
+
79
+ def describe_stack_resources(options = {})
80
+ @stacks.fetch(options.fetch(:stack_name)) { raise Aws::CloudFormation::Errors::ValidationError.new('', 'Stack does not exist') }
81
+ OpenStruct.new(stack_resources: @stack_resources[options.fetch(:stack_name)])
82
+ end
83
+
84
+ def get_template(options)
85
+ template_body = @templates[options[:stack_name]] || nil
86
+ OpenStruct.new(template_body: template_body)
87
+ end
88
+
89
+ def get_stack_policy(options)
90
+ OpenStruct.new(stack_policy_body: @stack_policies[options.fetch(:stack_name)])
91
+ end
92
+
93
+ def describe_stack_events(options)
94
+ events = @stack_events[options.fetch(:stack_name)] || []
95
+ OpenStruct.new(stack_events: events, next_token: nil)
96
+ end
97
+
98
+ def update_stack(options)
99
+ stack_name = options.fetch(:stack_name)
100
+ @stacks[stack_name].attributes = options
101
+ @stack_policies[stack_name] = options[:stack_policy_body]
102
+ end
103
+
104
+ def create_stack(options)
105
+ stack_name = options.fetch(:stack_name)
106
+ add_stack(options)
107
+ @stack_policies[stack_name] = options[:stack_policy_body]
108
+ end
109
+
110
+ def delete_stack(options)
111
+ stack_name = options.fetch(:stack_name)
112
+ @stacks.delete(stack_name)
113
+ end
114
+
115
+ def validate_template(options)
116
+ true
117
+ end
118
+
119
+ def add_stack(stack)
120
+ @stacks[stack.fetch(:stack_name)] = Stack.new(stack)
121
+ end
122
+
123
+ def add_stack_resource(options)
124
+ @stack_resources[options.fetch(:stack_name)] ||= []
125
+ @stack_resources[options.fetch(:stack_name)] << StackResource.new(options)
126
+ end
127
+
128
+ def set_template(stack_name, template)
129
+ @templates[stack_name] = template
130
+ end
131
+
132
+ def add_stack_event(event)
133
+ stack_name = event.fetch(:stack_name)
134
+ @stack_events[stack_name] ||= []
135
+ @stack_events[stack_name] << StackEvent.new(event)
136
+ end
137
+ end
138
+ end
139
+ end