cuffsert 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5e2a534e878fbb69920af515d5829138351c4937
4
- data.tar.gz: 9697ef60c78c97f5823f089d8efd67f95723a411
3
+ metadata.gz: 77fc923eb6792bb5c118dbaaafaf161737395a4d
4
+ data.tar.gz: 444d658ff0249ef336e9871cfe60b481eb2cfbf7
5
5
  SHA512:
6
- metadata.gz: 37643076b4d503a37ac1d12f997c27230a90904386157fcad1b4a668b126b886091f775e4244c9442d62f027e0b40fac9b7cc94e6c5b60c219563cc46da6690d
7
- data.tar.gz: 46a6d4ba65176322eb3da8c3abb4fe5641ba0157d7885670410849b324880b5090b4c457251d6fed61feb2230e75a1e2e0126d2c49685b22ed28fc327ad702cc
6
+ metadata.gz: 06338fc5162660886d719014786818d31cdd576560607bd5f5ef09b867aab98d73512b34d8e8993a7b703d22f6b11b0b12a0fd64bd8f8fbeb522a36e09360d28
7
+ data.tar.gz: ad4322d3b92ac52271bb6dec5d535e3dc935c4021990b93ced699d34b08548043d5f59c0630650fc90e997b9bcb4e698bee55853b46957b00f08f782caa31ae4
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cuffsert (0.12.0)
4
+ cuffsert (0.13.0)
5
5
  aws-sdk-cloudformation (~> 1.3.0)
6
6
  aws-sdk-s3 (~> 1.8.0)
7
7
  colorize
8
+ hashdiff
8
9
  ruby-termios
9
10
  rx
10
11
 
@@ -33,7 +34,8 @@ GEM
33
34
  colorize (0.8.1)
34
35
  diff-lcs (1.2.5)
35
36
  docile (1.1.5)
36
- jmespath (1.4.0)
37
+ hashdiff (0.3.7)
38
+ jmespath (1.3.1)
37
39
  json (2.0.2)
38
40
  rspec (3.5.0)
39
41
  rspec-core (~> 3.5.0)
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.add_runtime_dependency 'aws-sdk-cloudformation', '~> 1.3.0'
19
19
  spec.add_runtime_dependency 'aws-sdk-s3', '~> 1.8.0'
20
20
  spec.add_runtime_dependency 'colorize'
21
+ spec.add_runtime_dependency 'hashdiff'
21
22
  spec.add_runtime_dependency 'ruby-termios'
22
23
  spec.add_runtime_dependency 'rx'
23
24
 
@@ -10,7 +10,7 @@ module CuffDown
10
10
  parser = OptionParser.new do |opts|
11
11
  opts.banner = 'Output CuffSert-formatted metadata from an existing stack.'
12
12
  opts.separator('')
13
- opts.separator('Usage: cuffdown stack-name')
13
+ opts.separator('Usage: cuffdown <stack-name>')
14
14
  CuffBase.shared_cli_args(opts, args)
15
15
  end
16
16
  stackname, _ = parser.parse(argv)
@@ -1,6 +1,7 @@
1
1
  require 'cuffsert/actions'
2
2
  require 'cuffsert/cfarguments'
3
3
  require 'cuffsert/messages'
4
+ require 'yaml'
4
5
  require 'rx'
5
6
 
6
7
  module CuffSert
@@ -34,6 +35,20 @@ module CuffSert
34
35
  end
35
36
  end
36
37
 
38
+ class MessageAction < BaseAction
39
+ def initialize(message)
40
+ super(nil, nil)
41
+ @message = message
42
+ end
43
+
44
+ def validate!
45
+ end
46
+
47
+ def as_observable
48
+ @message.as_observable
49
+ end
50
+ end
51
+
37
52
  class CreateStackAction < BaseAction
38
53
  def validate!
39
54
  if @meta.stack_uri.nil?
@@ -75,12 +90,31 @@ module CuffSert
75
90
  upload_uri, maybe_upload = upload_template_if_oversized(cfargs)
76
91
  cfargs[:template_url] = upload_uri if upload_uri
77
92
  maybe_upload
78
- .concat(@cfclient.prepare_update(cfargs).map {|change_set| CuffSert::ChangeSet.new(change_set) })
93
+ .concat(prepare_update(cfargs))
79
94
  .flat_map(&method(:on_event))
80
95
  end
81
96
 
82
97
  private
83
98
 
99
+ def prepare_update(cfargs)
100
+ @cfclient.get_template(@meta)
101
+ .map do |current_template|
102
+ pending_template = if cfargs[:template_body]
103
+ YAML.load(cfargs[:template_body])
104
+ elsif @meta.stack_uri && @meta.stack_uri.scheme == 'file'
105
+ CuffSert.load_template(@meta.stack_uri)
106
+ else
107
+ current_template
108
+ end
109
+ CuffSert::Templates.new([current_template, pending_template])
110
+ end.merge(
111
+ @cfclient.prepare_update(cfargs)
112
+ .map do |change_set|
113
+ CuffSert::ChangeSet.new(change_set)
114
+ end
115
+ )
116
+ end
117
+
84
118
  def on_event(event)
85
119
  Rx::Observable.concat(
86
120
  Rx::Observable.just(event),
@@ -1,5 +1,6 @@
1
1
  require 'open-uri'
2
2
  require 'yaml'
3
+ require 'cuffsert/yaml-ext'
3
4
 
4
5
  # TODO:
5
6
  # - propagate timeout here (from config?)
@@ -83,13 +84,13 @@ module CuffSert
83
84
  "https://#{host}/#{bucket}#{key}"
84
85
  end
85
86
 
86
- private_class_method
87
-
88
- def self.load_minified_template(file)
89
- template = open(file).read
90
- YAML.load(template).to_json
87
+ def self.load_template(stack_uri)
88
+ file = stack_uri.to_s.sub(/^file:\/+/, '/')
89
+ YAML.load(open(file).read)
91
90
  end
92
91
 
92
+ private_class_method
93
+
93
94
  def self.template_parameters(meta)
94
95
  template_parameters = {}
95
96
 
@@ -102,8 +103,7 @@ module CuffSert
102
103
  raise 'Only HTTPS URLs pointing to amazonaws.com supported.'
103
104
  end
104
105
  elsif meta.stack_uri.scheme == 'file'
105
- file = meta.stack_uri.to_s.sub(/^file:\/+/, '/')
106
- template = self.load_minified_template(file)
106
+ template = CuffSert.load_template(meta.stack_uri).to_json
107
107
  if template.size <= 51200
108
108
  template_parameters[:template_body] = template
109
109
  end
@@ -12,13 +12,13 @@ module CuffSert
12
12
  CREATE_COMPLETE
13
13
  ROLLBACK_COMPLETE
14
14
  UPDATE_COMPLETE
15
- UPDATE_ROLLBACK_COMPLETE
16
15
  DELETE_COMPLETE
17
16
  DELETE_SKIPPED
18
17
  ]
19
18
 
20
19
  BAD_STATES = %w[
21
20
  CREATE_FAILED
21
+ UPDATE_ROLLBACK_COMPLETE
22
22
  UPDATE_ROLLBACK_FAILED
23
23
  UPDATE_FAILED
24
24
  DELETE_FAILED
@@ -0,0 +1,4 @@
1
+ module CuffSert
2
+ class CuffSertError < RuntimeError ; end
3
+ class RxCFError < CuffSertError ; end
4
+ end
@@ -15,7 +15,8 @@ module CuffSert
15
15
  found = cfclient.find_stack_blocking(meta)
16
16
 
17
17
  if found && INPROGRESS_STATES.include?(found[:stack_status])
18
- action = Abort.new('Stack operation already in progress')
18
+ message = Abort.new('Stack operation already in progress')
19
+ action = MessageAction.new(message)
19
20
  else
20
21
  if found.nil?
21
22
  action = CreateStackAction.new(meta, nil)
@@ -23,5 +23,6 @@ module CuffSert
23
23
  super('Done.')
24
24
  end
25
25
  end
26
+ class Templates < Message ; end
26
27
  class ChangeSet < Message ; end
27
28
  end
@@ -1,7 +1,9 @@
1
1
  require 'aws-sdk-cloudformation'
2
2
  require 'colorize'
3
3
  require 'cuffsert/cfstates'
4
+ require 'cuffsert/errors'
4
5
  require 'cuffsert/messages'
6
+ require 'hashdiff'
5
7
  require 'rx'
6
8
 
7
9
  # TODO: Animate in-progress states
@@ -55,6 +57,8 @@ module CuffSert
55
57
 
56
58
  def on_event(event)
57
59
  case event
60
+ when ::CuffSert::Templates
61
+ @renderer.templates(*event.message)
58
62
  when Aws::CloudFormation::Types::StackEvent
59
63
  on_stack_event(event)
60
64
  when ::CuffSert::ChangeSet
@@ -73,6 +77,16 @@ module CuffSert
73
77
  end
74
78
  end
75
79
 
80
+ def on_error(err)
81
+ case err
82
+ when CuffSertError
83
+ @renderer.abort(err)
84
+ else
85
+ super(err)
86
+ end
87
+ exit(1)
88
+ end
89
+
76
90
  def on_complete
77
91
  end
78
92
 
@@ -134,6 +148,7 @@ module CuffSert
134
148
  @verbosity = options[:verbosity] || 1
135
149
  end
136
150
 
151
+ def templates(current, pending) ; end
137
152
  def change_set(change_set) ; end
138
153
  def event(event, resource) ; end
139
154
  def clear ; end
@@ -144,6 +159,13 @@ module CuffSert
144
159
  end
145
160
 
146
161
  class JsonRenderer < BaseRenderer
162
+ def templates(current, pending)
163
+ if @verbosity >= 1
164
+ @output.write(current.to_json)
165
+ @output.write(pending.to_json)
166
+ end
167
+ end
168
+
147
169
  def change_set(change_set)
148
170
  @output.write(change_set.to_h.to_json) unless @verbosity < 1
149
171
  end
@@ -182,11 +204,12 @@ module CuffSert
182
204
  ]
183
205
  end.map do |change|
184
206
  rc = change[:resource_change]
185
- sprintf("%s[%s] %-10s %s\n",
207
+ sprintf("%s[%s] %-10s %s\n%s",
186
208
  rc[:logical_resource_id],
187
209
  rc[:resource_type],
188
210
  action_color(action(rc)),
189
- scope_desc(rc)
211
+ scope_desc(rc),
212
+ change_details(rc)
190
213
  )
191
214
  end.each { |row| @output.write(row) }
192
215
  end
@@ -283,12 +306,22 @@ module CuffSert
283
306
  ))
284
307
  end
285
308
 
309
+ def templates(current, pending)
310
+ @current_template = current
311
+ @pending_template = pending
312
+ @template_changes = HashDiff.best_diff(current, pending, array_path: true)
313
+ @template_changes.each {|c| p c} if ENV['CUFFSERT_EXPERIMENTAL']
314
+ present_changes(extract_changes(@template_changes, 'Conditions'), 'Conditions') unless @verbosity < 1
315
+ present_changes(extract_changes(@template_changes, 'Parameters'), 'Parameters') unless @verbosity < 1
316
+ present_changes(extract_changes(@template_changes, 'Outputs'), 'Outputs') unless @verbosity < 1
317
+ end
318
+
286
319
  def report(event)
287
320
  @output.write(event.message.colorize(:white) + "\n") unless @verbosity < 2
288
321
  end
289
322
 
290
323
  def abort(event)
291
- @error.write(event.message.colorize(:red) + "\n") unless @verbosity < 1
324
+ @error.write("\n" + event.message.colorize(:red) + "\n") unless @verbosity < 1
292
325
  end
293
326
 
294
327
  def done(event)
@@ -297,6 +330,57 @@ module CuffSert
297
330
 
298
331
  private
299
332
 
333
+ def change_details(rc)
334
+ (rc[:details] || []).flat_map do |detail|
335
+ target_path = case detail[:target][:attribute]
336
+ when 'Properties'
337
+ [rc[:logical_resource_id], detail[:target][:attribute], detail[:target][:name]]
338
+ when 'Tags'
339
+ [rc[:logical_resource_id], 'Properties', detail[:target][:attribute]]
340
+ else
341
+ nil
342
+ end
343
+ extract_changes(@template_changes, 'Resources', *target_path)
344
+ end
345
+ .map do |(ch, path, l, r)|
346
+ format_change(ch, path[3..-1], l, r)
347
+ end
348
+ .join
349
+ end
350
+
351
+ def extract_changes(changes, type, *target_path)
352
+ changes
353
+ .select {|(_, path, _)| path[0..target_path.size] == [type, *target_path] }
354
+ .map {|(ch, path, *rest)| [ch, path, *rest] }
355
+ end
356
+
357
+ def present_changes(changes, type)
358
+ return unless changes.size > 0
359
+ @output.write("#{type}:\n")
360
+ changes.each do |(ch, path, l, r)|
361
+ @output.write(format_change(ch, path, l, r))
362
+ end
363
+ end
364
+
365
+ def format_change(ch, path, l, r = nil)
366
+ sprintf("%s %s: %s\n",
367
+ change_color(ch),
368
+ path.join('/'),
369
+ ch == '~' ? "#{l} -> #{r}" : l,
370
+ )
371
+ end
372
+
373
+ def change_color(ch)
374
+ ch.colorize(
375
+ case ch
376
+ when '-' then :red
377
+ when '+' then :green
378
+ when '~' then :yellow
379
+ else :white
380
+ end
381
+ )
382
+ end
383
+
300
384
  def interpret_states(resource)
301
385
  case resource[:states]
302
386
  when [:progress]
@@ -1,5 +1,7 @@
1
1
  require 'aws-sdk-cloudformation'
2
2
  require 'cuffsert/cfstates'
3
+ require 'cuffsert/errors'
4
+ require 'yaml'
3
5
  require 'rx'
4
6
 
5
7
  module CuffSert
@@ -19,6 +21,14 @@ module CuffSert
19
21
  nil
20
22
  end
21
23
 
24
+ def get_template(meta)
25
+ Rx::Observable.create do |observer|
26
+ template = @cf.get_template(:stack_name => meta.stackname)
27
+ observer.on_next(YAML.load(template[:template_body]))
28
+ observer.on_completed
29
+ end
30
+ end
31
+
22
32
  def create_stack(cfargs)
23
33
  Rx::Observable.create do |observer|
24
34
  start_time = record_start_time
@@ -48,8 +58,12 @@ module CuffSert
48
58
  Rx::Observable.create do |observer|
49
59
  start_time = record_start_time
50
60
  @cf.execute_change_set(change_set_name: change_set_id)
51
- stack_events(stack_id, start_time) do |event|
52
- observer.on_next(event)
61
+ begin
62
+ stack_events(stack_id, start_time) do |event|
63
+ observer.on_next(event)
64
+ end
65
+ rescue => e
66
+ observer.on_error(e)
53
67
  end
54
68
  observer.on_completed
55
69
  end
@@ -84,9 +98,8 @@ module CuffSert
84
98
  DateTime.now - 5.0 / 86400
85
99
  end
86
100
 
87
- def stack_finished?(stack_id, event)
88
- event[:physical_resource_id] == stack_id &&
89
- FINAL_STATES.include?(event[:resource_status])
101
+ def terminal_event?(stack_id, event)
102
+ event[:physical_resource_id] == stack_id && CuffSert.state_category(event[:resource_status]) != :progress
90
103
  end
91
104
 
92
105
  def flatten_events(stack_id)
@@ -101,16 +114,22 @@ module CuffSert
101
114
  eventid_cache = Set.new
102
115
  loop do
103
116
  events = []
104
- done = false
117
+ terminal_event = nil
105
118
  flatten_events(stack_id) do |event|
106
119
  break if event[:timestamp].to_datetime < start_time
107
120
  next unless eventid_cache.add?(event[:event_id])
108
121
  events.unshift(event)
109
- done = true if stack_finished?(stack_id, event)
122
+ terminal_event ||= event if terminal_event?(stack_id, event)
110
123
  end
111
124
  events.each { |event| yield event }
112
- break if done
113
- sleep(@pause)
125
+ case terminal_event && CuffSert.state_category(terminal_event[:resource_status])
126
+ when :good
127
+ break
128
+ when :bad
129
+ raise RxCFError, "Stack #{terminal_event.logical_resource_id} finished in state #{terminal_event.resource_status}"
130
+ else
131
+ sleep(@pause)
132
+ end
114
133
  end
115
134
  end
116
135
  end
@@ -1,3 +1,3 @@
1
1
  module CuffSert
2
- VERSION = '0.12.0'
2
+ VERSION = '0.13.0'
3
3
  end
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+
3
+ module YAML
4
+ %w[Ref].each do |name|
5
+ add_domain_type('cuffsert', name) do |tag, value|
6
+ {name => value}
7
+ end
8
+ end
9
+
10
+ add_domain_type('cuffsert', 'GetAtt') do |_, value|
11
+ if value.is_a? String
12
+ {'Fn::GetAtt' => value.split('.')}
13
+ else
14
+ {'Fn::GetAtt' => value}
15
+ end
16
+ end
17
+
18
+ %w[
19
+ Base64 Cidr FindInMap GetAZs ImportValue Join Select Split Sub Transform
20
+ And Equals If Not Or
21
+ ].each do |name|
22
+ add_domain_type('cuffsert', name) do |tag, value|
23
+ {['Fn', name].join('::') => value}
24
+ end
25
+ end
26
+ end
@@ -7,7 +7,10 @@ module CuffUp
7
7
  :output => '/dev/stdout'
8
8
  }
9
9
  parser = OptionParser.new do |opts|
10
- opts.on('--output metadata', '-o metadata', 'File to write metadata file to; decaults to stdout') do |f|
10
+ opts.banner = 'Output CuffSert-formatted metadata defaults based on a stack template.'
11
+ opts.separator('')
12
+ opts.separator('Usage: cuffup <template.json>')
13
+ opts.on('--output metadata', '-o metadata', 'File to write metadata file to; defaults to stdout') do |f|
11
14
  args[:output] = f
12
15
  end
13
16
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuffsert
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anders Qvist
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-18 00:00:00.000000000 Z
11
+ date: 2019-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-cloudformation
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: hashdiff
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: ruby-termios
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +193,7 @@ files:
179
193
  - lib/cuffsert/cfstates.rb
180
194
  - lib/cuffsert/cli_args.rb
181
195
  - lib/cuffsert/confirmation.rb
196
+ - lib/cuffsert/errors.rb
182
197
  - lib/cuffsert/main.rb
183
198
  - lib/cuffsert/messages.rb
184
199
  - lib/cuffsert/metadata.rb
@@ -186,6 +201,7 @@ files:
186
201
  - lib/cuffsert/rxcfclient.rb
187
202
  - lib/cuffsert/rxs3client.rb
188
203
  - lib/cuffsert/version.rb
204
+ - lib/cuffsert/yaml-ext.rb
189
205
  - lib/cuffup.rb
190
206
  homepage: https://github.com/bittrance/cuffsert
191
207
  licenses: