stack_master 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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