stacker-yaml 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/stacker +9 -0
- data/lib/stacker.rb +4 -0
- data/lib/stacker/cli.rb +271 -0
- data/lib/stacker/differ.rb +31 -0
- data/lib/stacker/logging.rb +62 -0
- data/lib/stacker/region.rb +39 -0
- data/lib/stacker/resolvers/file_resolver.rb +15 -0
- data/lib/stacker/resolvers/resolver.rb +20 -0
- data/lib/stacker/resolvers/stack_output_resolver.rb +17 -0
- data/lib/stacker/stack.rb +275 -0
- data/lib/stacker/stack/capabilities.rb +19 -0
- data/lib/stacker/stack/component.rb +22 -0
- data/lib/stacker/stack/errors.rb +87 -0
- data/lib/stacker/stack/parameter.rb +102 -0
- data/lib/stacker/stack/parameters.rb +76 -0
- data/lib/stacker/stack/template.rb +126 -0
- data/lib/stacker/version.rb +3 -0
- metadata +273 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'stacker/resolvers/resolver'
|
2
|
+
|
3
|
+
module Stacker
|
4
|
+
module Resolvers
|
5
|
+
|
6
|
+
class StackOutputResolver < Resolver
|
7
|
+
|
8
|
+
def resolve
|
9
|
+
prefix = region.options.fetch(:stack_prefix, '')
|
10
|
+
stack = region.stack "#{prefix}#{ref.fetch('Stack')}"
|
11
|
+
stack.outputs.fetch ref.fetch('Output')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
require 'active_support/core_ext/hash/slice'
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'aws-sdk'
|
5
|
+
require 'memoist'
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
require 'stacker/stack/errors'
|
9
|
+
require 'stacker/stack/capabilities'
|
10
|
+
require 'stacker/stack/parameters'
|
11
|
+
require 'stacker/stack/template'
|
12
|
+
|
13
|
+
module Stacker
|
14
|
+
class Stack
|
15
|
+
|
16
|
+
extend Memoist
|
17
|
+
|
18
|
+
CLIENT_METHODS = %w[
|
19
|
+
creation_time
|
20
|
+
description
|
21
|
+
last_updated_time
|
22
|
+
status_reason
|
23
|
+
]
|
24
|
+
|
25
|
+
SAFE_UPDATE_POLICY = <<-JSON
|
26
|
+
{
|
27
|
+
"Statement" : [
|
28
|
+
{
|
29
|
+
"Effect" : "Deny",
|
30
|
+
"Action" : ["Update:Replace", "Update:Delete"],
|
31
|
+
"Principal" : "*",
|
32
|
+
"Resource" : "*"
|
33
|
+
},
|
34
|
+
{
|
35
|
+
"Effect" : "Allow",
|
36
|
+
"Action" : "Update:*",
|
37
|
+
"Principal" : "*",
|
38
|
+
"Resource" : "*"
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
JSON
|
43
|
+
|
44
|
+
STATUS_COMPLETE_REGEX = /(ROLLBACK|CREATE|UPDATE)_(COMPLETE|FAILED)/
|
45
|
+
|
46
|
+
attr_reader :region, :name, :options
|
47
|
+
|
48
|
+
def initialize region, name, options = {}
|
49
|
+
@region, @name, @options = region, name, options
|
50
|
+
end
|
51
|
+
|
52
|
+
def client
|
53
|
+
res = region.client.describe_stacks(stack_name: name)
|
54
|
+
res.stacks.first
|
55
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def exists?
|
60
|
+
!!client
|
61
|
+
end
|
62
|
+
|
63
|
+
def status
|
64
|
+
if client
|
65
|
+
client.stack_status
|
66
|
+
else
|
67
|
+
"#{name}:\nStack with id #{name} does not exist"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
delegate *CLIENT_METHODS, to: :client
|
72
|
+
memoize *CLIENT_METHODS
|
73
|
+
|
74
|
+
%w[complete failed in_progress].each do |stage|
|
75
|
+
define_method(:"#{stage}?") { status =~ /#{stage.upcase}/ }
|
76
|
+
end
|
77
|
+
|
78
|
+
def template
|
79
|
+
@template ||= Template.new self
|
80
|
+
end
|
81
|
+
|
82
|
+
def parameters
|
83
|
+
@parameters ||= Parameters.new self
|
84
|
+
end
|
85
|
+
|
86
|
+
def capabilities
|
87
|
+
@capabilities ||= Capabilities.new self
|
88
|
+
end
|
89
|
+
|
90
|
+
def outputs
|
91
|
+
@outputs ||= begin
|
92
|
+
return {} unless complete?
|
93
|
+
Hash[client.outputs.map do |output|
|
94
|
+
[ output.output_key, output.output_value ]
|
95
|
+
end]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def create blocking = true
|
100
|
+
if exists?
|
101
|
+
Stacker.logger.warn 'Stack already exists'
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
if parameters.missing.any?
|
106
|
+
raise MissingParameters.new(
|
107
|
+
"Required parameters missing: #{parameters.missing.join ', '}"
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
Stacker.logger.info 'Creating stack'
|
112
|
+
|
113
|
+
params = parameters.resolved.map do |k, v|
|
114
|
+
{
|
115
|
+
parameter_key: k,
|
116
|
+
parameter_value: v
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
region.client.create_stack(
|
121
|
+
stack_name: name,
|
122
|
+
template_body: template.local_raw,
|
123
|
+
parameters: params,
|
124
|
+
capabilities: capabilities.local
|
125
|
+
)
|
126
|
+
|
127
|
+
wait_until_complete if blocking
|
128
|
+
rescue Aws::CloudFormation::Errors::ValidationError, ArgumentError => err
|
129
|
+
raise Error.new err.message
|
130
|
+
end
|
131
|
+
|
132
|
+
def update options = {}
|
133
|
+
options.assert_valid_keys(:blocking, :allow_destructive)
|
134
|
+
|
135
|
+
blocking = options.fetch(:blocking, true)
|
136
|
+
allow_destructive = options.fetch(:allow_destructive, false)
|
137
|
+
|
138
|
+
if parameters.missing.any?
|
139
|
+
raise MissingParameters.new(
|
140
|
+
"Required parameters missing: #{parameters.missing.join ', '}"
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
Stacker.logger.info 'Updating stack'
|
145
|
+
|
146
|
+
unless allow_destructive
|
147
|
+
raise StackPolicyError if describe_change_set.any? do |c|
|
148
|
+
c[:change][:replacement] == 'True' ||
|
149
|
+
c[:change][:action] =~ /remove/i
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
region.client.execute_change_set(
|
154
|
+
change_set_name: change_set,
|
155
|
+
stack_name: name
|
156
|
+
)
|
157
|
+
|
158
|
+
if blocking
|
159
|
+
sleep 4 # Wait a bit for the stack to begin updating
|
160
|
+
wait_until_complete
|
161
|
+
end
|
162
|
+
rescue Aws::CloudFormation::Errors::ValidationError => err
|
163
|
+
case err.message
|
164
|
+
when /does not exist/
|
165
|
+
raise DoesNotExistError.new err.message
|
166
|
+
when /No updates/
|
167
|
+
raise UpToDateError.new err.message
|
168
|
+
else
|
169
|
+
raise Error.new err.message
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def describe_change_set
|
174
|
+
retries = 6
|
175
|
+
changes = []
|
176
|
+
while changes.empty?
|
177
|
+
resp = region.client.describe_change_set(
|
178
|
+
change_set_name: change_set,
|
179
|
+
stack_name: name
|
180
|
+
)
|
181
|
+
changes = resp.changes
|
182
|
+
if changes.empty?
|
183
|
+
raise CannotDescribeChangeSet.new 'Empty change set' if retries == 0
|
184
|
+
retries -= 1
|
185
|
+
sleep 1
|
186
|
+
end
|
187
|
+
end
|
188
|
+
changes.map do |c|
|
189
|
+
rc = c.resource_change
|
190
|
+
{
|
191
|
+
type: c.type,
|
192
|
+
change: {
|
193
|
+
logical_resource_id: rc.logical_resource_id,
|
194
|
+
action: rc.action,
|
195
|
+
replacement: rc.replacement,
|
196
|
+
}
|
197
|
+
}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
memoize :describe_change_set
|
201
|
+
|
202
|
+
def pretty_change_set
|
203
|
+
riw = describe_change_set.map do |c|
|
204
|
+
c[:change][:logical_resource_id].length
|
205
|
+
end.max
|
206
|
+
fmt = "%-6s %-#{riw}s %-5s"
|
207
|
+
|
208
|
+
([fmt % ['Action', 'Resource', 'Replacement?'], '='*(riw+20)] +
|
209
|
+
describe_change_set.map do |c|
|
210
|
+
change = c[:change]
|
211
|
+
fmt % [
|
212
|
+
change[:action],
|
213
|
+
change[:logical_resource_id],
|
214
|
+
change[:replacement]
|
215
|
+
]
|
216
|
+
end).join("\n")
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
def change_set_name
|
222
|
+
"stacker-#{SecureRandom.hex}"
|
223
|
+
end
|
224
|
+
memoize :change_set_name
|
225
|
+
|
226
|
+
def change_set
|
227
|
+
change_set_name.tap do |csname|
|
228
|
+
region.client.create_change_set(
|
229
|
+
stack_name: name,
|
230
|
+
template_body: template.local_raw,
|
231
|
+
parameters: parameters.resolved.map do |k, v|
|
232
|
+
{
|
233
|
+
parameter_key: k,
|
234
|
+
parameter_value: v
|
235
|
+
}
|
236
|
+
end,
|
237
|
+
capabilities: capabilities.local,
|
238
|
+
change_set_name: csname
|
239
|
+
)
|
240
|
+
end
|
241
|
+
rescue Aws::CloudFormation::Errors::ValidationError => err
|
242
|
+
raise Error.new err.message
|
243
|
+
end
|
244
|
+
memoize :change_set
|
245
|
+
|
246
|
+
def report_status
|
247
|
+
case status
|
248
|
+
when /_COMPLETE$/
|
249
|
+
Stacker.logger.info "#{name} Status => #{status}"
|
250
|
+
when /_ROLLBACK_IN_PROGRESS$/
|
251
|
+
failure_event = client.events.enum(limit: 30).find do |event|
|
252
|
+
event.resource_status =~ /_FAILED$/
|
253
|
+
end
|
254
|
+
failure_reason = failure_event.resource_status_reason
|
255
|
+
if failure_reason =~ /stack policy/
|
256
|
+
raise StackPolicyError.new failure_reason
|
257
|
+
else
|
258
|
+
Stacker.logger.fatal "#{name} Status => #{status}"
|
259
|
+
raise Error.new "Failure Reason: #{failure_reason}"
|
260
|
+
end
|
261
|
+
else
|
262
|
+
Stacker.logger.debug "#{name} Status => #{status}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def wait_until_complete
|
267
|
+
while !(status =~ STATUS_COMPLETE_REGEX)
|
268
|
+
report_status
|
269
|
+
sleep 5
|
270
|
+
end
|
271
|
+
report_status
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'stacker/stack/component'
|
2
|
+
|
3
|
+
module Stacker
|
4
|
+
class Stack
|
5
|
+
class Capabilities < Component
|
6
|
+
|
7
|
+
def local
|
8
|
+
@local ||= Array(stack.options.fetch 'capabilities', [])
|
9
|
+
end
|
10
|
+
|
11
|
+
def remote
|
12
|
+
# `capabilities` actually returns a
|
13
|
+
# !ruby/array:Aws::Xml::DefaultList
|
14
|
+
@remote ||= client.capabilities.to_a
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'stacker/stack'
|
2
|
+
|
3
|
+
module Stacker
|
4
|
+
class Stack
|
5
|
+
# an abstract base class for stack components (template, parameters)
|
6
|
+
class Component
|
7
|
+
|
8
|
+
attr_reader :stack
|
9
|
+
|
10
|
+
def initialize stack
|
11
|
+
@stack = stack
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def client
|
17
|
+
stack.client
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'jsonlint'
|
2
|
+
require 'yamllint'
|
3
|
+
|
4
|
+
module Stacker
|
5
|
+
class Stack
|
6
|
+
|
7
|
+
class Error < StandardError; end
|
8
|
+
class StackPolicyError < Error; end
|
9
|
+
class DoesNotExistError < Error; end
|
10
|
+
class MissingParameters < Error; end
|
11
|
+
class UpToDateError < Error; end
|
12
|
+
class CannotDescribeChangeSet < Error; end
|
13
|
+
|
14
|
+
class StackUndeclared < Error
|
15
|
+
|
16
|
+
def initialize(name)
|
17
|
+
@name = name
|
18
|
+
end
|
19
|
+
|
20
|
+
def message
|
21
|
+
"Stack with id #{@name} is not declared"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class ParameterResolutionError < Error
|
27
|
+
|
28
|
+
def initialize(value, error)
|
29
|
+
@value = value
|
30
|
+
@error = error
|
31
|
+
end
|
32
|
+
|
33
|
+
def message
|
34
|
+
"Failed to resolve reference parameter: #{@value}\nError: #{@error}"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class TemplateDoesNotExistError < Error
|
40
|
+
|
41
|
+
def initialize(name)
|
42
|
+
@name = name
|
43
|
+
end
|
44
|
+
|
45
|
+
def message
|
46
|
+
"No template found with name '#{@name}'"
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class TemplateSyntaxError < Error
|
52
|
+
|
53
|
+
def initialize(path)
|
54
|
+
@path = path
|
55
|
+
end
|
56
|
+
|
57
|
+
def message
|
58
|
+
<<END_MSG
|
59
|
+
Syntax error(s) in template.
|
60
|
+
#{path}:
|
61
|
+
#{errors}
|
62
|
+
END_MSG
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
attr_reader :path
|
68
|
+
|
69
|
+
def errors
|
70
|
+
@errors ||= begin
|
71
|
+
linter.check path
|
72
|
+
linter.errors.values.join "\n"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def linter
|
77
|
+
linter ||= if path.end_with? '.json'
|
78
|
+
JsonLint::Linter.new
|
79
|
+
else
|
80
|
+
YamlLint::Linter.new
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'stacker/resolvers/stack_output_resolver'
|
2
|
+
require 'stacker/resolvers/file_resolver'
|
3
|
+
|
4
|
+
module Stacker
|
5
|
+
class Stack
|
6
|
+
|
7
|
+
# A Parameter represents a stack parameter. A parameter can be
|
8
|
+
# either a literal value (e.g. a string, number, or array) or a
|
9
|
+
# reference, otherwise known as a dependency. References parameter
|
10
|
+
# values are represented as hashes with a single key indicating
|
11
|
+
# the reference type. There is one exception to this rule;
|
12
|
+
# references to stack outputs are expressed as hashes with two
|
13
|
+
# keys, "Stack" and "Output".
|
14
|
+
class Parameter
|
15
|
+
|
16
|
+
extend Memoist
|
17
|
+
|
18
|
+
attr_reader :value, :region
|
19
|
+
|
20
|
+
def initialize value, region
|
21
|
+
@region = region
|
22
|
+
@value = if value.is_a?(Array)
|
23
|
+
value.map { |v| Parameter.new v, region }
|
24
|
+
else
|
25
|
+
value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def dependency?
|
30
|
+
value.is_a?(Hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependencies
|
34
|
+
if dependency?
|
35
|
+
[ to_s ]
|
36
|
+
elsif value.is_a?(Array)
|
37
|
+
value.map(&:dependencies).flatten
|
38
|
+
else
|
39
|
+
[ ]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def resolved
|
44
|
+
if dependency?
|
45
|
+
begin
|
46
|
+
resolver.resolve
|
47
|
+
rescue => err
|
48
|
+
raise ParameterResolutionError.new value, err
|
49
|
+
end
|
50
|
+
elsif value.is_a?(Array)
|
51
|
+
value.map(&:resolved).join ','
|
52
|
+
else
|
53
|
+
value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
memoize :resolved
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
if dependency?
|
60
|
+
value.values.map(&:to_s).sort.join('.')
|
61
|
+
else
|
62
|
+
value.to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ==(parameter)
|
67
|
+
parameter.value == value && parameter.region == region
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def stack_output?
|
73
|
+
dependency? && value['Stack'] && value['Output']
|
74
|
+
end
|
75
|
+
|
76
|
+
def resolver_class_name
|
77
|
+
type = if stack_output?
|
78
|
+
'StackOutput'
|
79
|
+
elsif value.keys.size == 1
|
80
|
+
value.keys.first
|
81
|
+
else
|
82
|
+
raise ReferenceError.new 'Too many top-level keys in reference value.'
|
83
|
+
end
|
84
|
+
|
85
|
+
"Stacker::Resolvers::#{type}Resolver"
|
86
|
+
end
|
87
|
+
|
88
|
+
def reference_value
|
89
|
+
if stack_output?
|
90
|
+
value
|
91
|
+
else
|
92
|
+
value.values.first
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def resolver
|
97
|
+
resolver_class_name.constantize.new reference_value, region
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|