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 +4 -4
- data/Gemfile.lock +4 -2
- data/cuffsert.gemspec +1 -0
- data/lib/cuffdown/main.rb +1 -1
- data/lib/cuffsert/actions.rb +35 -1
- data/lib/cuffsert/cfarguments.rb +7 -7
- data/lib/cuffsert/cfstates.rb +1 -1
- data/lib/cuffsert/errors.rb +4 -0
- data/lib/cuffsert/main.rb +2 -1
- data/lib/cuffsert/messages.rb +1 -0
- data/lib/cuffsert/presenters.rb +87 -3
- data/lib/cuffsert/rxcfclient.rb +28 -9
- data/lib/cuffsert/version.rb +1 -1
- data/lib/cuffsert/yaml-ext.rb +26 -0
- data/lib/cuffup.rb +4 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77fc923eb6792bb5c118dbaaafaf161737395a4d
|
4
|
+
data.tar.gz: 444d658ff0249ef336e9871cfe60b481eb2cfbf7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06338fc5162660886d719014786818d31cdd576560607bd5f5ef09b867aab98d73512b34d8e8993a7b703d22f6b11b0b12a0fd64bd8f8fbeb522a36e09360d28
|
7
|
+
data.tar.gz: ad4322d3b92ac52271bb6dec5d535e3dc935c4021990b93ced699d34b08548043d5f59c0630650fc90e997b9bcb4e698bee55853b46957b00f08f782caa31ae4
|
data/Gemfile.lock
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cuffsert (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
|
-
|
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)
|
data/cuffsert.gemspec
CHANGED
@@ -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
|
|
data/lib/cuffdown/main.rb
CHANGED
@@ -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)
|
data/lib/cuffsert/actions.rb
CHANGED
@@ -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(
|
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),
|
data/lib/cuffsert/cfarguments.rb
CHANGED
@@ -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
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
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
|
data/lib/cuffsert/cfstates.rb
CHANGED
@@ -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
|
data/lib/cuffsert/main.rb
CHANGED
@@ -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
|
-
|
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)
|
data/lib/cuffsert/messages.rb
CHANGED
data/lib/cuffsert/presenters.rb
CHANGED
@@ -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]
|
data/lib/cuffsert/rxcfclient.rb
CHANGED
@@ -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
|
-
|
52
|
-
|
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
|
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
|
-
|
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
|
-
|
122
|
+
terminal_event ||= event if terminal_event?(stack_id, event)
|
110
123
|
end
|
111
124
|
events.each { |event| yield event }
|
112
|
-
|
113
|
-
|
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
|
data/lib/cuffsert/version.rb
CHANGED
@@ -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
|
data/lib/cuffup.rb
CHANGED
@@ -7,7 +7,10 @@ module CuffUp
|
|
7
7
|
:output => '/dev/stdout'
|
8
8
|
}
|
9
9
|
parser = OptionParser.new do |opts|
|
10
|
-
opts.
|
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.
|
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:
|
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:
|