cuffsert 0.9.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/.gitignore +5 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +29 -0
- data/README.md +86 -0
- data/bin/cuffdown +55 -0
- data/bin/cuffsert +5 -0
- data/bin/cuffup +46 -0
- data/cuffsert.gemspec +24 -0
- data/lib/cuffsert/cfarguments.rb +61 -0
- data/lib/cuffsert/cfstates.rb +41 -0
- data/lib/cuffsert/cli_args.rb +96 -0
- data/lib/cuffsert/confirmation.rb +50 -0
- data/lib/cuffsert/main.rb +129 -0
- data/lib/cuffsert/messages.rb +18 -0
- data/lib/cuffsert/metadata.rb +90 -0
- data/lib/cuffsert/presenters.rb +313 -0
- data/lib/cuffsert/rxcfclient.rb +116 -0
- metadata +193 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'cuffsert/cfarguments'
|
2
|
+
require 'cuffsert/cfstates'
|
3
|
+
require 'cuffsert/cli_args'
|
4
|
+
require 'cuffsert/confirmation'
|
5
|
+
require 'cuffsert/messages'
|
6
|
+
require 'cuffsert/metadata'
|
7
|
+
require 'cuffsert/presenters'
|
8
|
+
require 'cuffsert/rxcfclient'
|
9
|
+
require 'rx'
|
10
|
+
require 'uri'
|
11
|
+
|
12
|
+
# TODO:
|
13
|
+
# - Stop using file: that we anyway need to special-case in cfarguments
|
14
|
+
# - default value for meta.metadata when stack_path is local file
|
15
|
+
# - selector and metadata are mandatory and need guards accordingly
|
16
|
+
# - validate_and_urlify belongs in metadata.rb
|
17
|
+
# - execute should use helpers and not know details of statuses
|
18
|
+
# - update 'abort' should delete cheangeset and emit the result
|
19
|
+
|
20
|
+
module CuffSert
|
21
|
+
def self.validate_and_urlify(stack_path)
|
22
|
+
if stack_path =~ /^[A-Za-z0-9]+:/
|
23
|
+
stack_uri = URI.parse(stack_path)
|
24
|
+
else
|
25
|
+
normalized = File.expand_path(stack_path)
|
26
|
+
unless File.exist?(normalized)
|
27
|
+
raise "Local file #{normalized} does not exist"
|
28
|
+
end
|
29
|
+
stack_uri = URI.join('file:///', normalized)
|
30
|
+
end
|
31
|
+
unless ['s3', 'file'].include?(stack_uri.scheme)
|
32
|
+
raise "Uri #{stack_uri.scheme} is not supported"
|
33
|
+
end
|
34
|
+
stack_uri
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.create_stack(client, meta, confirm_create)
|
38
|
+
cfargs = CuffSert.as_create_stack_args(meta)
|
39
|
+
Rx::Observable.concat(
|
40
|
+
Rx::Observable.of([:create, meta.stackname]),
|
41
|
+
Rx::Observable.defer do
|
42
|
+
if confirm_create.call(meta, :create, nil)
|
43
|
+
client.create_stack(cfargs)
|
44
|
+
else
|
45
|
+
Abort.new('User abort!').as_observable
|
46
|
+
end
|
47
|
+
end
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.update_stack(client, meta, confirm_update)
|
52
|
+
cfargs = CuffSert.as_update_change_set(meta)
|
53
|
+
client.prepare_update(cfargs)
|
54
|
+
.last
|
55
|
+
.flat_map do |change_set|
|
56
|
+
Rx::Observable.concat(
|
57
|
+
Rx::Observable.of(change_set),
|
58
|
+
Rx::Observable.defer {
|
59
|
+
if change_set[:status] == 'FAILED'
|
60
|
+
client.abort_update(change_set[:change_set_id])
|
61
|
+
elsif confirm_update.call(meta, :update, change_set)
|
62
|
+
client.update_stack(change_set[:stack_id], change_set[:change_set_id])
|
63
|
+
else
|
64
|
+
Rx::Observable.concat(
|
65
|
+
client.abort_update(change_set[:change_set_id]),
|
66
|
+
Abort.new('User abort!').as_observable
|
67
|
+
)
|
68
|
+
end
|
69
|
+
}
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.recreate_stack(client, stack, meta, confirm_recreate)
|
75
|
+
crt_args = CuffSert.as_create_stack_args(meta)
|
76
|
+
del_args = CuffSert.as_delete_stack_args(stack)
|
77
|
+
Rx::Observable.concat(
|
78
|
+
Rx::Observable.of([:recreate, stack]),
|
79
|
+
Rx::Observable.defer do
|
80
|
+
if confirm_recreate.call(meta, :recreate, stack)
|
81
|
+
Rx::Observable.concat(
|
82
|
+
client.delete_stack(del_args),
|
83
|
+
client.create_stack(crt_args)
|
84
|
+
)
|
85
|
+
else
|
86
|
+
CuffSert::Abort.new('User abort!').as_observable
|
87
|
+
end
|
88
|
+
end
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.execute(meta, confirm_update, force_replace: false, client: RxCFClient.new)
|
93
|
+
sources = []
|
94
|
+
found = client.find_stack_blocking(meta)
|
95
|
+
|
96
|
+
if found && INPROGRESS_STATES.include?(found[:stack_status])
|
97
|
+
sources << Abort.new('Stack operation already in progress').as_observable
|
98
|
+
elsif found.nil?
|
99
|
+
sources << self.create_stack(client, meta, confirm_update)
|
100
|
+
elsif found[:stack_status] == 'ROLLBACK_COMPLETE' || force_replace
|
101
|
+
sources << self.recreate_stack(client, found, meta, confirm_update)
|
102
|
+
else
|
103
|
+
sources << self.update_stack(client, meta, confirm_update)
|
104
|
+
end
|
105
|
+
Rx::Observable.concat(*sources)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.make_renderer(cli_args)
|
109
|
+
if cli_args[:output] == :json
|
110
|
+
JsonRenderer.new(STDOUT, STDERR, cli_args)
|
111
|
+
else
|
112
|
+
ProgressbarRenderer.new(STDOUT, STDERR, cli_args)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.run(argv)
|
117
|
+
cli_args = CuffSert.parse_cli_args(argv)
|
118
|
+
meta = CuffSert.build_meta(cli_args)
|
119
|
+
if cli_args[:stack_path].nil? || cli_args[:stack_path].size != 1
|
120
|
+
raise 'Requires exactly one stack path'
|
121
|
+
end
|
122
|
+
stack_path = cli_args[:stack_path][0]
|
123
|
+
meta.stack_uri = CuffSert.validate_and_urlify(stack_path)
|
124
|
+
events = CuffSert.execute(meta, CuffSert.method(:confirmation),
|
125
|
+
force_replace: cli_args[:force_replace])
|
126
|
+
renderer = CuffSert.make_renderer(cli_args)
|
127
|
+
RendererPresenter.new(events, renderer)
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CuffSert
|
2
|
+
class Abort
|
3
|
+
attr_reader :message
|
4
|
+
|
5
|
+
def initialize(message)
|
6
|
+
@message = message
|
7
|
+
end
|
8
|
+
|
9
|
+
def ===(other)
|
10
|
+
# For the benefit of value_matches? and regex
|
11
|
+
other.is_a?(Abort) && (other.message === @message || @message === other.message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def as_observable
|
15
|
+
Rx::Observable.just(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module CuffSert
|
4
|
+
class StackConfig
|
5
|
+
attr_accessor :stackname, :selected_path, :op_mode, :stack_uri
|
6
|
+
attr_accessor :suffix, :parameters, :tags
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@selected_path = []
|
10
|
+
@op_mode = :normal
|
11
|
+
@parameters = {}
|
12
|
+
@tags = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def append_path(lmnt)
|
16
|
+
@selected_path << lmnt
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_from(metadata)
|
20
|
+
@stackname = metadata[:stackname] || @stackname
|
21
|
+
@suffix = metadata[:suffix] || @suffix
|
22
|
+
@parameters.merge!(metadata[:parameters] || {})
|
23
|
+
@tags.merge!(metadata[:tags] || {})
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def stackname
|
28
|
+
@stackname || (@selected_path + [*@suffix]).join('-')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.load_config(io)
|
33
|
+
config = YAML.load(io)
|
34
|
+
raise 'config does not seem to be a YAML hash?' unless config.is_a?(Hash)
|
35
|
+
config = symbolize_keys(config)
|
36
|
+
format = config.delete(:format)
|
37
|
+
raise 'Please include Format: v1' if format.nil? || format.downcase != 'v1'
|
38
|
+
config
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.meta_for_path(metadata, path, target = StackConfig.new)
|
42
|
+
target.update_from(metadata)
|
43
|
+
candidate, path = path
|
44
|
+
key = candidate || metadata[:defaultpath]
|
45
|
+
variants = metadata[:variants]
|
46
|
+
if key.nil?
|
47
|
+
raise "No DefaultPath found for #{variants.keys}" unless variants.nil?
|
48
|
+
return target
|
49
|
+
end
|
50
|
+
target.append_path(key)
|
51
|
+
|
52
|
+
raise "Missing variants section as expected by #{key}" if variants.nil?
|
53
|
+
new_meta = variants[key.to_sym]
|
54
|
+
raise "#{key.inspect} not found in variants" if new_meta.nil?
|
55
|
+
self.meta_for_path(new_meta, path, target)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.build_meta(cli_args)
|
59
|
+
io = open(cli_args[:metadata])
|
60
|
+
config = CuffSert.load_config(io)
|
61
|
+
default = self.meta_defaults(cli_args)
|
62
|
+
meta = CuffSert.meta_for_path(config, cli_args[:selector], default)
|
63
|
+
CuffSert.cli_overrides(meta, cli_args)
|
64
|
+
end
|
65
|
+
|
66
|
+
private_class_method
|
67
|
+
|
68
|
+
def self.meta_defaults(cli_args)
|
69
|
+
default = StackConfig.new
|
70
|
+
default.suffix = File.basename(cli_args[:metadata], '.yml')
|
71
|
+
default
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.cli_overrides(meta, cli_args)
|
75
|
+
meta.update_from(cli_args[:overrides])
|
76
|
+
meta.op_mode = cli_args[:op_mode] || meta.op_mode
|
77
|
+
meta
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.symbolize_keys(hash)
|
81
|
+
hash.each_with_object({}) do |(k, v), h|
|
82
|
+
k = k.downcase.to_sym
|
83
|
+
if k == :tags || k == :parameters
|
84
|
+
h[k] = v.each_with_object({}) { |e, h| h[e['Name']] = e['Value'] }
|
85
|
+
else
|
86
|
+
h[k] = v.is_a?(Hash) ? symbolize_keys(v) : v
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,313 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'colorize'
|
3
|
+
require 'cuffsert/cfstates'
|
4
|
+
require 'cuffsert/messages'
|
5
|
+
require 'rx'
|
6
|
+
|
7
|
+
# TODO: Animate in-progress states
|
8
|
+
# - Introduce a Done message and stop printing in on_complete
|
9
|
+
# - Present the error message in change_set properly - and abort
|
10
|
+
# - badness goes to stderr
|
11
|
+
# - change sets should present modification details indented under each entry
|
12
|
+
# - property direct modification
|
13
|
+
# - properties through parameter change
|
14
|
+
# - indirect change through other resource ("causing_entity": "Lb.DNSName")
|
15
|
+
|
16
|
+
module CuffSert
|
17
|
+
class BasePresenter
|
18
|
+
def initialize(events)
|
19
|
+
events.subscribe(
|
20
|
+
method(:on_event),
|
21
|
+
method(:on_error),
|
22
|
+
method(:on_complete)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_error(err)
|
27
|
+
STDERR.puts'Error:'
|
28
|
+
STDERR.puts err
|
29
|
+
STDERR.puts err.backtrace.join("\n\t")
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_complete
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_width(width)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class RawPresenter < BasePresenter
|
40
|
+
def on_event(event)
|
41
|
+
puts event.inspect
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_complete
|
45
|
+
puts 'Done.'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class RendererPresenter < BasePresenter
|
50
|
+
def initialize(events, renderer)
|
51
|
+
@resources = []
|
52
|
+
@index = {}
|
53
|
+
@renderer = renderer
|
54
|
+
super(events)
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_event(event)
|
58
|
+
# Workaround for now
|
59
|
+
event = event.data if event.class == Seahorse::Client::Response
|
60
|
+
|
61
|
+
case event
|
62
|
+
when Aws::CloudFormation::Types::StackEvent
|
63
|
+
on_stack_event(event)
|
64
|
+
when Aws::CloudFormation::Types::DescribeChangeSetOutput
|
65
|
+
on_change_set(event)
|
66
|
+
# when [:recreate, Aws::CloudFormation::Types::Stack]
|
67
|
+
when Array
|
68
|
+
on_stack(*event)
|
69
|
+
when ::CuffSert::Abort
|
70
|
+
@renderer.abort(event)
|
71
|
+
else
|
72
|
+
puts event
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_complete
|
77
|
+
@renderer.done
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def on_change_set(change_set)
|
83
|
+
@renderer.change_set(change_set.to_h)
|
84
|
+
end
|
85
|
+
|
86
|
+
def on_stack_event(event)
|
87
|
+
resource = lookup_stack_resource(event)
|
88
|
+
update_resource_states(resource, event)
|
89
|
+
@renderer.event(event, resource)
|
90
|
+
@renderer.clear
|
91
|
+
@resources.each { |resource| @renderer.resource(resource) }
|
92
|
+
clear_resources if is_completed_stack_event(event)
|
93
|
+
end
|
94
|
+
|
95
|
+
def on_stack(event, stack)
|
96
|
+
@renderer.stack(event, stack)
|
97
|
+
end
|
98
|
+
|
99
|
+
def lookup_stack_resource(event)
|
100
|
+
rid = event[:logical_resource_id]
|
101
|
+
unless (pos = @index[rid])
|
102
|
+
pos = @index[rid] = @resources.size
|
103
|
+
@resources << make_resource(event)
|
104
|
+
end
|
105
|
+
@resources[pos]
|
106
|
+
end
|
107
|
+
|
108
|
+
def make_resource(event)
|
109
|
+
event.to_h
|
110
|
+
.reject { |k, _| k == :timestamp }
|
111
|
+
.merge!(:states => [])
|
112
|
+
end
|
113
|
+
|
114
|
+
def update_resource_states(resource, event)
|
115
|
+
resource[:states] = resource[:states]
|
116
|
+
.reject { |state| state == :progress }
|
117
|
+
.take(1) << CuffSert.state_category(event[:resource_status])
|
118
|
+
end
|
119
|
+
|
120
|
+
def is_completed_stack_event(event)
|
121
|
+
event[:resource_type] == 'AWS::CloudFormation::Stack' &&
|
122
|
+
FINAL_STATES.include?(event[:resource_status])
|
123
|
+
end
|
124
|
+
|
125
|
+
def clear_resources
|
126
|
+
@resources.clear
|
127
|
+
@index.clear
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class BaseRenderer
|
132
|
+
def initialize(output = STDOUT, error = STDERR, options = {})
|
133
|
+
@output = output
|
134
|
+
@error = error
|
135
|
+
@verbosity = options[:verbosity] || 1
|
136
|
+
end
|
137
|
+
|
138
|
+
def change_set(change_set) ; end
|
139
|
+
def event(event, resource) ; end
|
140
|
+
def clear ; end
|
141
|
+
def resource(resource) ; end
|
142
|
+
def abort(message) ; end
|
143
|
+
def done ; end
|
144
|
+
end
|
145
|
+
|
146
|
+
class JsonRenderer < BaseRenderer
|
147
|
+
def change_set(change_set)
|
148
|
+
@output.write(change_set.to_h.to_json) unless @verbosity < 1
|
149
|
+
end
|
150
|
+
|
151
|
+
def event(event, resource)
|
152
|
+
@output.write(event.to_h.to_json) unless @verbosity < 1
|
153
|
+
end
|
154
|
+
|
155
|
+
def stack(event, stack)
|
156
|
+
@output.write(stack.to_json) unless @verbosity < 1
|
157
|
+
end
|
158
|
+
|
159
|
+
def abort(event)
|
160
|
+
@error.write(event.message + "\n") unless @verbosity < 1
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
ACTION_ORDER = ['Add', 'Modify', 'Replace?', 'Replace!', 'Remove']
|
165
|
+
|
166
|
+
class ProgressbarRenderer < BaseRenderer
|
167
|
+
def change_set(change_set)
|
168
|
+
@output.write(sprintf("Updating stack %s\n", change_set[:stack_name]))
|
169
|
+
change_set[:changes].sort do |l, r|
|
170
|
+
lr = l[:resource_change]
|
171
|
+
rr = r[:resource_change]
|
172
|
+
[
|
173
|
+
ACTION_ORDER.index(action(lr)),
|
174
|
+
lr[:logical_resource_id]
|
175
|
+
] <=> [
|
176
|
+
ACTION_ORDER.index(action(rr)),
|
177
|
+
rr[:logical_resource_id]
|
178
|
+
]
|
179
|
+
end.map do |change|
|
180
|
+
rc = change[:resource_change]
|
181
|
+
sprintf("%s[%s] %-10s %s\n",
|
182
|
+
rc[:logical_resource_id],
|
183
|
+
rc[:resource_type],
|
184
|
+
action_color(action(rc)),
|
185
|
+
scope_desc(rc)
|
186
|
+
)
|
187
|
+
end.each { |row| @output.write(row) }
|
188
|
+
end
|
189
|
+
|
190
|
+
def action(rc)
|
191
|
+
if rc[:action] == 'Modify'
|
192
|
+
if ['True', 'Always'].include?(rc[:replacement])
|
193
|
+
'Replace!'
|
194
|
+
elsif ['False', 'Never'].include?(rc[:replacement])
|
195
|
+
'Modify'
|
196
|
+
elsif rc[:replacement] == 'Conditional'
|
197
|
+
'Replace?'
|
198
|
+
else
|
199
|
+
"#{rc[:action]}/#{rc[:replacement]}"
|
200
|
+
end
|
201
|
+
else
|
202
|
+
rc[:action]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def action_color(action)
|
207
|
+
action.colorize(
|
208
|
+
case action
|
209
|
+
when 'Add' then :green
|
210
|
+
when 'Modify' then :yellow
|
211
|
+
else :red
|
212
|
+
end
|
213
|
+
)
|
214
|
+
end
|
215
|
+
|
216
|
+
def scope_desc(rc)
|
217
|
+
(rc[:scope] || []).map do |scope|
|
218
|
+
case scope
|
219
|
+
when 'Properties'
|
220
|
+
properties = rc[:details]
|
221
|
+
.select { |detail| detail[:target][:attribute] == 'Properties' }
|
222
|
+
.map { |detail| detail[:target][:name] }
|
223
|
+
.uniq
|
224
|
+
.join(", ")
|
225
|
+
sprintf("Properties: %s", properties)
|
226
|
+
else
|
227
|
+
rc[:scope]
|
228
|
+
end
|
229
|
+
end
|
230
|
+
.join("; ")
|
231
|
+
end
|
232
|
+
|
233
|
+
def event(event, resource)
|
234
|
+
return if @verbosity == 0
|
235
|
+
return if resource[:states][-1] != :bad && @verbosity <= 1
|
236
|
+
color, _ = interpret_states(resource)
|
237
|
+
message = sprintf('%s %s %s[%s] %s',
|
238
|
+
event[:resource_status],
|
239
|
+
event[:timestamp].strftime('%H:%M:%S%z'),
|
240
|
+
event[:logical_resource_id],
|
241
|
+
event[:resource_type].sub(/.*::/, ''),
|
242
|
+
event[:resource_status_reason] || ""
|
243
|
+
).colorize(color)
|
244
|
+
@output.write("\r#{message}\n")
|
245
|
+
end
|
246
|
+
|
247
|
+
def stack(event, stack)
|
248
|
+
case event
|
249
|
+
when :create
|
250
|
+
@output.write("Creating stack #{stack}\n")
|
251
|
+
when :recreate
|
252
|
+
message = sprintf(
|
253
|
+
"Deleting and re-creating stack %s",
|
254
|
+
stack[:stack_name]
|
255
|
+
)
|
256
|
+
@output.write(message.colorize(:red) + "\n")
|
257
|
+
else
|
258
|
+
puts event, stack
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def clear
|
263
|
+
@output.write("\r") unless @verbosity < 1
|
264
|
+
end
|
265
|
+
|
266
|
+
def resource(resource)
|
267
|
+
return if @verbosity < 1
|
268
|
+
color, symbol = interpret_states(resource)
|
269
|
+
table = {
|
270
|
+
:check => "+",
|
271
|
+
:tripple_dot => ".", # "\u2026"
|
272
|
+
:cross => "!",
|
273
|
+
:qmark => "?",
|
274
|
+
}
|
275
|
+
|
276
|
+
@output.write(table[symbol].colorize(
|
277
|
+
:color => :white,
|
278
|
+
:background => color
|
279
|
+
))
|
280
|
+
end
|
281
|
+
|
282
|
+
def abort(event)
|
283
|
+
@error.write(event.message.colorize(:red) + "\n") unless @verbosity < 1
|
284
|
+
end
|
285
|
+
|
286
|
+
def done
|
287
|
+
@output.write("\nDone.\n".colorize(:green)) unless @verbosity < 1
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
def interpret_states(resource)
|
293
|
+
case resource[:states]
|
294
|
+
when [:progress]
|
295
|
+
[:yellow, :tripple_dot]
|
296
|
+
when [:good]
|
297
|
+
[:green, :check]
|
298
|
+
when [:bad]
|
299
|
+
[:red, :cross]
|
300
|
+
when [:good, :progress]
|
301
|
+
[:light_white, :tripple_dot]
|
302
|
+
when [:bad, :progress]
|
303
|
+
[:red, :tripple_dot]
|
304
|
+
when [:good, :good], [:bad, :good]
|
305
|
+
[:light_white, :check]
|
306
|
+
when [:good, :bad], [:bad, :bad]
|
307
|
+
[:red, :qmark]
|
308
|
+
else
|
309
|
+
raise "Unexpected :states in #{resource.inspect}"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'cuffsert/cfstates'
|
3
|
+
require 'rx'
|
4
|
+
|
5
|
+
module CuffSert
|
6
|
+
class RxCFClient
|
7
|
+
def initialize(
|
8
|
+
aws_cf = Aws::CloudFormation::Client.new(retry_limit: 8),
|
9
|
+
pause: 5,
|
10
|
+
max_items: 1000)
|
11
|
+
@cf = aws_cf
|
12
|
+
@max_items = max_items
|
13
|
+
@pause = pause
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_stack_blocking(meta)
|
17
|
+
name = meta.stackname
|
18
|
+
@cf.describe_stacks(stack_name: name)[:stacks][0]
|
19
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_stack(cfargs)
|
24
|
+
Rx::Observable.create do |observer|
|
25
|
+
start_time = record_start_time
|
26
|
+
stack_id = @cf.create_stack(cfargs)[:stack_id]
|
27
|
+
stack_events(stack_id, start_time) do |event|
|
28
|
+
observer.on_next(event)
|
29
|
+
end
|
30
|
+
observer.on_completed
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare_update(cfargs)
|
35
|
+
Rx::Observable.create do |observer|
|
36
|
+
change_set_id = @cf.create_change_set(cfargs)[:id]
|
37
|
+
loop do
|
38
|
+
change_set = @cf.describe_change_set(change_set_name: change_set_id)
|
39
|
+
observer.on_next(change_set)
|
40
|
+
break if FINAL_STATES.include?(change_set[:status])
|
41
|
+
end
|
42
|
+
observer.on_completed
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_stack(stack_id, change_set_id)
|
47
|
+
Rx::Observable.create do |observer|
|
48
|
+
start_time = record_start_time
|
49
|
+
@cf.execute_change_set(change_set_name: change_set_id)
|
50
|
+
stack_events(stack_id, start_time) do |event|
|
51
|
+
observer.on_next(event)
|
52
|
+
end
|
53
|
+
observer.on_completed
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def abort_update(change_set_id)
|
58
|
+
Rx::Observable.create do |observer|
|
59
|
+
@cf.delete_change_set(change_set_name: change_set_id)
|
60
|
+
observer.on_completed
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete_stack(cfargs)
|
65
|
+
eventid_cache = Set.new
|
66
|
+
Rx::Observable.create do |observer|
|
67
|
+
start_time = record_start_time
|
68
|
+
@cf.delete_stack(cfargs)
|
69
|
+
stack_events(cfargs[:stack_name], start_time) do |event|
|
70
|
+
observer.on_next(event)
|
71
|
+
end
|
72
|
+
observer.on_completed
|
73
|
+
end
|
74
|
+
.select do |event|
|
75
|
+
eventid_cache.add?(event[:event_id])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def record_start_time
|
82
|
+
# Please make sure your machine has NTP :p
|
83
|
+
DateTime.now - 5.0 / 86400
|
84
|
+
end
|
85
|
+
|
86
|
+
def stack_finished?(stack_id, event)
|
87
|
+
event[:physical_resource_id] == stack_id &&
|
88
|
+
FINAL_STATES.include?(event[:resource_status])
|
89
|
+
end
|
90
|
+
|
91
|
+
def flatten_events(stack_id)
|
92
|
+
@cf.describe_stack_events(stack_name: stack_id).each do |events|
|
93
|
+
for event in events[:stack_events]
|
94
|
+
yield event
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def stack_events(stack_id, start_time)
|
100
|
+
eventid_cache = Set.new
|
101
|
+
loop do
|
102
|
+
events = []
|
103
|
+
done = false
|
104
|
+
flatten_events(stack_id) do |event|
|
105
|
+
break if event[:timestamp].to_datetime < start_time
|
106
|
+
next unless eventid_cache.add?(event[:event_id])
|
107
|
+
events.unshift(event)
|
108
|
+
done = true if stack_finished?(stack_id, event)
|
109
|
+
end
|
110
|
+
events.each { |event| yield event }
|
111
|
+
break if done
|
112
|
+
sleep(@pause)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|