cuffsert 0.12.0 → 0.13.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 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: