stacker-yaml 0.1.0
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.
- 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
|