docker-cake 0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c65060c27c01fce55231395ee5ad9200948a4be7
4
+ data.tar.gz: e9fc17acf691b35fbb212bb18fcd74b50d908308
5
+ SHA512:
6
+ metadata.gz: da38d0dffb2c6e0eee7de50ec7f2f870e539878f67f65377da781011ad8df84985b96d6ec32e1029740e21dd2b1512985438be6fc8f9cc11b49e1c0986eee33b
7
+ data.tar.gz: 31857508135384c7a536cb8ce8dc304d26d943916741827d227f81adc0cfdb069703864725230dac4d0b2f33ae7c39752ec0e124834730d5121853850d36e200
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ /target/
2
+ **/*.rs.bk
3
+ others
4
+ *.gem
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # docker-cake
2
+
3
+ Command line tool to compare image layers across tegs, build to optimize storage usage
4
+
5
+ ### Install:
6
+ ```sh
7
+ gem install docker-cake
8
+ ```
9
+
10
+ ### Example:
11
+
12
+ ```
13
+ docker-cake library/redmine
14
+
15
+ +-----------------+---------------------+------------+--------+--------------+--------------+------------+-----------+
16
+ | Tag | Date | Size | Layers | Reuse Layers | Extra Layers | Extra Size | Same as |
17
+ +-----------------+---------------------+------------+--------+--------------+--------------+------------+-----------+
18
+ | 3.4.1 | 2017-07-13 23:09:32 | 240.249 MB | 34 | 18 | 16 | 240.249 MB | |
19
+ | 3.4.1-passenger | 2017-07-13 23:09:58 | 258.997 MB | 38 | 36 | 2 | 18.748 MB | |
20
+ | 3.4 | 2017-08-07 23:17:29 | 240.502 MB | 34 | 19 | 15 | 240.502 MB | |
21
+ | 3.4.2 | 2017-08-07 23:17:29 | 240.502 MB | 34 | 34 | 0 | 0 Bytes | 3.4 |
22
+ | 3 | 2017-08-07 23:17:29 | 240.502 MB | 34 | 34 | 0 | 0 Bytes | 3.4 |
23
+ | latest | 2017-08-07 23:17:29 | 240.502 MB | 34 | 34 | 0 | 0 Bytes | 3.4 |
24
+ | passenger | 2017-08-07 23:18:15 | 259.285 MB | 38 | 36 | 2 | 18.784 MB | |
25
+ | 3.4-passenger | 2017-08-07 23:18:15 | 259.285 MB | 38 | 38 | 0 | 0 Bytes | passenger |
26
+ | 3.4.2-passenger | 2017-08-07 23:18:15 | 259.285 MB | 38 | 38 | 0 | 0 Bytes | passenger |
27
+ | 3.3 | 2017-08-07 23:22:16 | 251.748 MB | 34 | 22 | 12 | 188.982 MB | |
28
+ +-----------------+---------------------+------------+--------+--------------+--------------+------------+-----------+
29
+ ```
data/bin/docker-cake ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push(File.expand_path("../../lib", __FILE__))
4
+
5
+ require 'docker_cake/cli'
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.specification_version = 3
3
+
4
+ gem.name = 'docker-cake'
5
+ gem.version = "0.1"
6
+ gem.authors = ["Pavel Evstigneev"]
7
+ gem.email = ["pavel.evst@gmail.com"]
8
+ gem.license = 'MIT'
9
+ gem.date = '2017-08-08'
10
+ gem.summary = "Command line program to inspect docker images size"
11
+ gem.homepage = 'https://github.com/paxa/docker-cake'
12
+
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.require_path = 'lib'
15
+ gem.executables = ["docker-cake"]
16
+
17
+ gem.required_ruby_version = '>= 2.1'
18
+
19
+ gem.add_dependency 'terminal-table', ['>= 1.8.0', '<3']
20
+ end
@@ -0,0 +1 @@
1
+ require_relative 'docker_cake'
@@ -0,0 +1,133 @@
1
+ require_relative 'docker_cake/registry_api_client'
2
+ require 'time'
3
+ require 'terminal-table'
4
+
5
+ class DockerCake
6
+
7
+ attr_accessor :registry
8
+
9
+ def initialize(url: nil, user: nil, password: nil)
10
+ @registry ||= RegistryApiClient.new(user: user, password: password)
11
+ end
12
+
13
+ def repo_info(name, tag = 'latest')
14
+ manifest = registry.manifest_layers(name, tag)
15
+ puts print_manifest(manifest)
16
+ end
17
+
18
+ def compare_versions(repo_name, filter: /.+/, max: 10)
19
+ tags = registry.tags(repo_name, false)['tags']
20
+ tags.select! {|t| t =~ filter}
21
+
22
+ selected_tags = tags.last(max)
23
+
24
+ manifests = {}
25
+ procs = selected_tags.map do |tag|
26
+ lambda { manifests[tag] = registry.manifest_layers(repo_name, tag) }
27
+ end
28
+ registry.in_parallel(procs)
29
+ #selected_tags.each do |tag|
30
+ # manifests[tag] = registry.manifest_layers(repo_name, tag)
31
+ # #pp manifests[tag]
32
+ #end
33
+
34
+ manifests = manifests.sort_by do |tag, list|
35
+ list.map {|l| l['created'] }.max
36
+ end.to_h
37
+
38
+ images_map = {}
39
+ counted_layers = []
40
+ result = []
41
+
42
+ manifests.each do |tag, layers|
43
+ stats = {name: tag, size: 0, add_img: 0, reuse_img: 0, add_size: 0, layers: 0, date: Time.at(0).to_s}
44
+
45
+ map_key = manifests[tag].map {|l| l['blobSum']}.join(',')
46
+ if images_map[map_key]
47
+ stats[:same_as] = images_map[map_key]
48
+ end
49
+ images_map[map_key] ||= tag
50
+
51
+ manifests[tag].each do |layer|
52
+ stats[:size] += layer['size'] || 0
53
+ stats[:layers] += 1
54
+ stats[:date] = layer['created'] if layer['created'] > stats[:date]
55
+ if counted_layers.include?(layer['blobSum'])
56
+ stats[:reuse_img] += 1
57
+ else
58
+ stats[:add_img] += 1
59
+ stats[:add_size] += layer['size'] || 0
60
+ end
61
+
62
+ counted_layers << layer['blobSum']
63
+ end
64
+
65
+ #puts "#{tag} -> #{stats}"
66
+ result << stats
67
+ end
68
+
69
+ puts print_comparison(result)
70
+ end
71
+
72
+ def print_comparison(result)
73
+ rows = result.map do |stats|
74
+ [
75
+ stats[:name],
76
+ DateTime.parse(stats[:date]).strftime("%F %T"),
77
+ size_to_human(stats[:size]),
78
+ stats[:layers],
79
+ stats[:reuse_img],
80
+ stats[:add_img],
81
+ size_to_human(stats[:add_size]),
82
+ stats[:same_as]
83
+ ]
84
+ end
85
+
86
+ table = Terminal::Table.new(headings: ['Tag', 'Date', 'Size', 'Layers', 'Reuse Layers', 'Extra Layers', 'Extra Size', 'Same as'], rows: rows)
87
+ end
88
+
89
+ def print_manifest(manifest)
90
+ rows = manifest.map do |layer|
91
+ cmd = layer['container_config'] && layer['container_config']['Cmd'] || ['']
92
+ cmd = cmd.join(" ").gsub('\n', "\n").sub(/#{'/bin/sh -c'}\s+/, '')
93
+ if cmd.start_with?('#(nop)')
94
+ cmd.sub!(/#\(nop\)\s+/, '')
95
+ else
96
+ cmd = "RUN #{cmd}"
97
+ end
98
+
99
+ cmd.gsub!("\t", " ")
100
+
101
+ shorter = []
102
+ cmd.lines.each do |line|
103
+ shorter.push(*line.scan(/.{1,90}/))
104
+ end
105
+
106
+ [
107
+ layer['id'][0...8],
108
+ DateTime.parse(layer['created']).strftime("%F %T"),
109
+ shorter.join("\n"),
110
+ size_to_human(layer['size'])
111
+ ]
112
+ end
113
+
114
+ rows << :separator << [nil, nil, 'TOTAL', size_to_human(manifest.sum {|layer| layer['size'].to_i })]
115
+
116
+ table = Terminal::Table.new(headings: ['ID', 'Date', 'CMD', 'Size'], rows: rows)
117
+ end
118
+
119
+ def size_to_human(size)
120
+ return '0' unless size
121
+
122
+ if size > 1_000_000_000
123
+ "#{(size / 1_000_000_000.0).round(3)} GB"
124
+ elsif size > 1_000_000
125
+ "#{(size / 1_000_000.0).round(3)} MB"
126
+ elsif size > 1_000
127
+ "#{(size / 1_000.0).round(3)} KB"
128
+ else
129
+ "#{size} Bytes"
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,61 @@
1
+ require 'optparse'
2
+ require_relative '../docker_cake'
3
+
4
+ options = {}
5
+ parser = OptionParser.new do |opts|
6
+ opts.banner = "Usage: docker-cake [options] REPO"
7
+
8
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
9
+ options[:verbose] = v
10
+ end
11
+
12
+ opts.on("-u", "--user [USER]", "Username, required for private repos") do |v|
13
+ options[:user] = v
14
+ end
15
+
16
+ opts.on("-p", "--password [PASSWORD]", "Password, required for private repos") do |v|
17
+ options[:password] = v
18
+ end
19
+
20
+ opts.on("-r", "--registry [REGISTRY URL]", "Docker registry URL, default is https://registry.hub.docker.com") do |v|
21
+ options[:url] = v
22
+ end
23
+
24
+ opts.on("-l", "--layers", "Print layers of image") do |v|
25
+ options[:layers] = v
26
+ end
27
+
28
+ opts.on("-n", "--max [NUM TAGS]", "Number of tags to analyse, default is 10") do |v|
29
+ options[:max] = v
30
+ end
31
+
32
+ end
33
+
34
+ parser.parse!
35
+
36
+ if options[:verbose]
37
+ ENV['DEBUG'] = '1'
38
+ p options
39
+ end
40
+
41
+ repo = ARGV.first
42
+ tag = ARGV[1]
43
+
44
+ if !repo || repo == ''
45
+ puts "Repository name is required"
46
+ puts "Example:"
47
+ puts " docker-cake library/ruby"
48
+ puts " docker-cake --help"
49
+ exit(1)
50
+ end
51
+
52
+ connect_options = {user: options[:user], password: options[:password]}
53
+ connect_options[:url] = options[:url] if options[:url]
54
+
55
+ if options[:layers]
56
+ DockerCake.new(connect_options).repo_info(repo, tag || 'latest')
57
+ else
58
+ opts = {}
59
+ opts[:max] = options[:max].to_i if options[:max]
60
+ DockerCake.new(connect_options).compare_versions(repo, opts)
61
+ end
@@ -0,0 +1,459 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'socket'
4
+ require 'net/http'
5
+
6
+ class RegistryApiClient
7
+
8
+ class RegistryAuthenticationException < Exception
9
+ end
10
+
11
+ class RegistryAuthorizationException < Exception
12
+ end
13
+
14
+ class RegistryUnknownException < Exception
15
+ end
16
+
17
+ class RegistrySSLException < Exception
18
+ end
19
+
20
+ class ReauthenticatedException < Exception
21
+ end
22
+
23
+ class UnknownRegistryException < Exception
24
+ end
25
+
26
+ class InvalidMethod < Exception
27
+ end
28
+
29
+ class Waiter
30
+ def initialize
31
+ @queue = Queue.new
32
+ end
33
+
34
+ # Waiting for someone to call #notify
35
+ def wait
36
+ @queue.pop(false)
37
+ end
38
+
39
+ # Notify waiting side
40
+ def notify(data)
41
+ @queue.push(data)
42
+ end
43
+ end
44
+
45
+ # It's same as waiter but support multiple subscribers
46
+ class PubSub
47
+ def initialize
48
+ @waiters = []
49
+ end
50
+
51
+ def wait
52
+ waiter = Waiter.new
53
+ @waiters << waiter
54
+ waiter.wait
55
+ end
56
+
57
+ def notify(value)
58
+ while waiter = @waiters.shift
59
+ waiter.notify(value)
60
+ end
61
+ end
62
+ end
63
+
64
+ DEFAULT_REGISTRY = "https://registry.hub.docker.com"
65
+ DEFAULT_MANIFEST = "application/vnd.docker.distribution.manifest.v2+json"
66
+ FAT_MANIFEST = "application/vnd.docker.distribution.manifest.list.v2+json"
67
+
68
+ # @param [#to_s] base_uri Docker registry base URI
69
+ # @param [Hash] options Client options
70
+ # @option options [#to_s] :user User name for basic authentication
71
+ # @option options [#to_s] :password Password for basic authentication
72
+ def initialize(url: DEFAULT_REGISTRY, user: nil, password: nil)
73
+ @url = url
74
+ uri = URI.parse(url)
75
+ @base_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}"
76
+ @user = user
77
+ @password = password
78
+ @manifest_format = "application/vnd.docker.distribution.manifest.v2+json"
79
+ #@manifest_format = "application/vnd.docker.distribution.manifest.list.v2+json"
80
+ #@manifest_format = "application/vnd.docker.container.image.v1+json"
81
+ # make a ping connection
82
+ #ping
83
+ end
84
+
85
+ def http_get(url, manifest: nil, auth: nil, auth_header: nil)
86
+ http_req("get", url, manifest: manifest, auth: auth, auth_header: auth_header)
87
+ end
88
+
89
+ def http_delete(url)
90
+ http_req("delete", url)
91
+ end
92
+
93
+ def http_head(url)
94
+ http_req("head", url)
95
+ end
96
+
97
+ def ping
98
+ response = http_get('/v2/')
99
+ end
100
+
101
+ def search(query = '')
102
+ response = http_get "/v2/_catalog"
103
+ # parse the response
104
+ repos = JSON.parse(response)["repositories"]
105
+ if query.strip.length > 0
106
+ re = Regexp.new query
107
+ repos = repos.find_all {|e| re =~ e }
108
+ end
109
+ return repos
110
+ end
111
+
112
+ def tags(repo, withHashes = false)
113
+ response = http_get("/v2/#{repo}/tags/list")
114
+ # parse the response
115
+ resp = JSON.parse(response)
116
+ # do we include the hashes?
117
+ if withHashes then
118
+ useGet = false
119
+ resp["hashes"] = {}
120
+ resp["tags"].each {|tag|
121
+ if useGet then
122
+ head = http_get "/v2/#{repo}/manifests/#{tag}"
123
+ else
124
+ begin
125
+ head = http_head("/v2/#{repo}/manifests/#{tag}")
126
+ rescue InvalidMethod
127
+ # in case we are in a registry pre-2.3.0, which did not support manifest HEAD
128
+ useGet = true
129
+ head = http_get("/v2/#{repo}/manifests/#{tag}")
130
+ end
131
+ end
132
+ resp["hashes"][tag] = head.headers[:docker_content_digest]
133
+ }
134
+ end
135
+
136
+ return resp
137
+ end
138
+
139
+ # combines small output and fat output to get layer names and sizes
140
+ def manifest(repo, tag, manifest: nil)
141
+ if @url == DEFAULT_REGISTRY
142
+ auth_header = %{Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:#{repo}:pull"}
143
+ end
144
+ JSON.parse(http_get("/v2/#{repo}/manifests/#{tag}", manifest: manifest, auth: :bearer, auth_header: auth_header))
145
+ end
146
+
147
+ def manifest_layers(repo, tag)
148
+ basic_response = nil
149
+ fat_response = nil
150
+
151
+ resp = in_parallel(
152
+ basic: lambda { manifest(repo, tag) },
153
+ fat: lambda { manifest(repo, tag, manifest: FAT_MANIFEST) }
154
+ )
155
+
156
+ unless resp[:basic]['layers']
157
+ puts "Strange response"
158
+ p resp[:basic]
159
+ end
160
+
161
+ basic = resp[:basic]['layers'] || []
162
+ fat_response = resp[:fat]
163
+
164
+ result = []
165
+
166
+ fat_response['history'].each_with_index do |info, index|
167
+ result[index] = JSON.parse(info['v1Compatibility'])
168
+ result[index]['blobSum'] = fat_response['fsLayers'][index]['blobSum']
169
+ result[index]['size'] = basic.detect do |layer|
170
+ layer['digest'] == result[index]['blobSum']
171
+ end
172
+ result[index]['size'] = result[index]['size']['size'] if result[index]['size']
173
+ end
174
+
175
+ # require 'irb'
176
+ # binding.irb
177
+
178
+ result.reverse
179
+ end
180
+
181
+ def in_parallel(procs = {})
182
+ threads = []
183
+ result = {}
184
+ errors = []
185
+ procs.each do |key, fun|
186
+ # handle array
187
+ if fun == nil && key.is_a?(Proc)
188
+ fun = key
189
+ key = result.size
190
+ end
191
+ result[key] = nil
192
+ threads << Thread.new do
193
+ begin
194
+ result[key] = fun.call
195
+ rescue => error
196
+ puts "#{error.class}: #{error.message}"
197
+ puts error.backtrace
198
+ errors << error
199
+ end
200
+ end
201
+ end
202
+
203
+ threads.map(&:join)
204
+
205
+ if errors.size > 0
206
+ raise errors.first
207
+ end
208
+
209
+ result
210
+ end
211
+
212
+ # gets the size of a particular blob, given the repo and the content-addressable hash
213
+ # usually unneeded, since manifest includes it
214
+ def blob_size(repo, blobSum)
215
+ response = http_head("/v2/#{repo}/blobs/#{blobSum}")
216
+ Integer(response.headers[:content_length], 10)
217
+ end
218
+
219
+ def manifest_sum(manifest)
220
+ size = 0
221
+ manifest["layers"].each do |layer|
222
+ size += layer["size"]
223
+ end
224
+ size
225
+ end
226
+
227
+ private
228
+ def http_req(type, url, stream: nil, manifest: nil, auth: nil, auth_header: nil)
229
+ begin
230
+ if auth == :bearer && auth_header
231
+ return do_bearer_req(type, url, auth_header, stream: stream, manifest: manifest)
232
+ else
233
+ return req_no_auth(type, url, stream: stream, manifest: manifest)
234
+ end
235
+ rescue SocketError => e
236
+ p e
237
+ raise RegistryUnknownException
238
+ rescue HTTP::Unauthorized => e
239
+ header = e.response.headers[:www_authenticate]
240
+ method = header.downcase.split(' ')[0]
241
+ case method
242
+ when 'basic'
243
+ response = do_basic_req(type, url, stream: stream, manifest: manifest)
244
+ when 'bearer'
245
+ response = do_bearer_req(type, url, header, stream: stream, manifest: manifest)
246
+ else
247
+ raise RegistryUnknownException
248
+ end
249
+ end
250
+ return response
251
+ end
252
+
253
+ def req_no_auth(type, url, stream: nil, manifest: nil)
254
+ block = stream.nil? ? nil : proc do |response|
255
+ response.read_body do |chunk|
256
+ stream.write chunk
257
+ end
258
+ end
259
+ response = HTTP.execute(
260
+ method: type,
261
+ url: @base_uri + url,
262
+ headers: {Accept: manifest || @manifest_format},
263
+ block_response: block
264
+ )
265
+ end
266
+
267
+ def do_basic_req(type, url, stream: nil, manifest: nil)
268
+ begin
269
+ block = stream.nil? ? nil : proc { |response|
270
+ response.read_body do |chunk|
271
+ stream.write chunk
272
+ end
273
+ }
274
+ response = HTTP.execute(
275
+ method: type,
276
+ url: @base_uri + url,
277
+ user: @user,
278
+ password: @password,
279
+ headers: {Accept: manifest || @manifest_format},
280
+ block_response: block
281
+ )
282
+ rescue SocketError
283
+ raise RegistryUnknownException
284
+ rescue HTTP::Unauthorized
285
+ raise RegistryAuthenticationException
286
+ rescue MethodNotAllowed
287
+ raise InvalidMethod
288
+ end
289
+ return response
290
+ end
291
+
292
+ def do_bearer_req(type, url, header, stream: false, manifest: nil)
293
+ token = authenticate_bearer(header)
294
+ begin
295
+ block = stream.nil? ? nil : proc { |response|
296
+ response.read_body do |chunk|
297
+ stream.write chunk
298
+ end
299
+ }
300
+ response = HTTP.execute(
301
+ method: type,
302
+ url: @base_uri + url,
303
+ headers: {Authorization: 'Bearer ' + token, Accept: manifest || @manifest_format},
304
+ block_response: block
305
+ )
306
+ rescue SocketError
307
+ raise RegistryUnknownException
308
+ rescue HTTP::Unauthorized
309
+ raise RegistryAuthenticationException
310
+ rescue MethodNotAllowed
311
+ raise InvalidMethod
312
+ end
313
+
314
+ return response
315
+ end
316
+
317
+ AUTH_CACHE = {}
318
+
319
+ def authenticate_bearer(header)
320
+ # get the parts we need
321
+ target = split_auth_header(header)
322
+ scope = target[:params][:scope]
323
+
324
+ if AUTH_CACHE[scope].is_a?(String)
325
+ return AUTH_CACHE[scope]
326
+ elsif AUTH_CACHE[scope].is_a?(PubSub)
327
+ return AUTH_CACHE[scope].wait
328
+ else
329
+ AUTH_CACHE[scope] = PubSub.new
330
+ end
331
+
332
+ # did we have a username and password?
333
+ if defined? @user and @user.to_s.strip.length != 0
334
+ target[:params][:account] = @user
335
+ end
336
+ # authenticate against the realm
337
+ uri = URI.parse(target[:realm])
338
+ begin
339
+ response = HTTP.execute(
340
+ method: :get,
341
+ url: uri.to_s,
342
+ query: target[:params],
343
+ user: @user,
344
+ password: @password
345
+ )
346
+ rescue HTTP::Unauthorized
347
+ # bad authentication
348
+ raise RegistryAuthenticationException
349
+ end
350
+ # now save the web token
351
+ token = JSON.parse(response)["token"]
352
+ AUTH_CACHE[scope].notify(token)
353
+ AUTH_CACHE[scope] = token
354
+ return token
355
+ end
356
+
357
+ def split_auth_header(header = '')
358
+ h = Hash.new
359
+ h = {params: {}}
360
+ header.split(/[\s,]+/).each {|entry|
361
+ p = entry.split('=')
362
+ case p[0]
363
+ when 'Bearer'
364
+ when 'realm'
365
+ h[:realm] = p[1].gsub(/(^\"|\"$)/,'')
366
+ else
367
+ h[:params][p[0]] = p[1].gsub(/(^\"|\"$)/,'')
368
+ end
369
+ }
370
+ h
371
+ end
372
+
373
+ module HTTP
374
+ extend self
375
+
376
+ class Unauthorized < Exception
377
+ attr_accessor :response
378
+ def initialize(message, response)
379
+ super(message)
380
+ @response = response
381
+ end
382
+ end
383
+
384
+ class MethodNotAllowed < Exception
385
+ attr_accessor :response
386
+ def initialize(message, response)
387
+ super(message)
388
+ @response = response
389
+ end
390
+ end
391
+
392
+ def execute(method:, url:, headers: {}, user: nil, password: nil, block_response: nil, body: nil, query: nil)
393
+ if query
394
+ uri = URI.parse(url)
395
+ url += (uri.query ? "&" : "?") + URI.encode_www_form(query)
396
+ end
397
+ uri = URI.parse(url)
398
+ response = nil
399
+
400
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
401
+ http_klass = case method.to_sym
402
+ when :get then Net::HTTP::Get
403
+ when :post then Net::HTTP::Post
404
+ when :put then Net::HTTP::Put
405
+ when :patch then Net::HTTP::Patch
406
+ when :head then Net::HTTP::Head
407
+ when :delete then Net::HTTP::Delete
408
+ when :move then Net::HTTP::Move
409
+ when :copy then Net::HTTP::Copy
410
+ else Net::HTTP::Post
411
+ end
412
+
413
+ request = http_klass.new(uri)
414
+ headers.each do |key, value|
415
+ request[key.to_s] = value
416
+ end
417
+
418
+ if body
419
+ request.body = body
420
+ end
421
+
422
+ if user != nil || password != nil
423
+ request.basic_auth(user, password)
424
+ end
425
+
426
+ puts "HTTP req #{method} #{url} #{headers}" if ENV['DEBUG']
427
+
428
+ http_resp = http.request(request)
429
+
430
+ puts "-> HTTP status: #{http_resp.code} size: #{http_resp.body.size}" if ENV['DEBUG']
431
+
432
+ response = Response.new(http_resp)
433
+
434
+ if http_resp.code.to_s == "401"
435
+ raise Unauthorized.new(http_resp.body, response)
436
+ end
437
+
438
+ if http_resp.code.to_s == "405"
439
+ raise MethodNotAllowed.new(http_resp.body, response)
440
+ end
441
+
442
+ return response
443
+ end
444
+ end
445
+
446
+ class Response < String
447
+ attr_accessor :headers
448
+
449
+ def initialize(http_response)
450
+ @http_response = http_response
451
+ @headers = {}
452
+ @http_response.to_hash.each do |key, value|
453
+ @headers[key.gsub('-', '_').to_sym] = value.last
454
+ end
455
+ super(http_response.body)
456
+ end
457
+ end
458
+ end
459
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docker-cake
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Pavel Evstigneev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: terminal-table
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.8.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.8.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ description:
34
+ email:
35
+ - pavel.evst@gmail.com
36
+ executables:
37
+ - docker-cake
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - ".gitignore"
42
+ - README.md
43
+ - bin/docker-cake
44
+ - docker-cake.gemspec
45
+ - lib/docker-cake.rb
46
+ - lib/docker_cake.rb
47
+ - lib/docker_cake/cli.rb
48
+ - lib/docker_cake/registry_api_client.rb
49
+ homepage: https://github.com/paxa/docker-cake
50
+ licenses:
51
+ - MIT
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 2.6.8
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Command line program to inspect docker images size
73
+ test_files: []