docker_registry2 1.12.0 → 1.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/docker_registry2.gemspec +16 -10
- data/lib/docker_registry2.rb +10 -9
- data/lib/registry/blob.rb +3 -1
- data/lib/registry/exceptions.rb +15 -14
- data/lib/registry/manifest.rb +3 -4
- data/lib/registry/registry.rb +294 -280
- data/lib/registry/version.rb +3 -1
- metadata +45 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d2c4b1b61074554ed7e15a5d3c662ebeaae19594cb8c4e3e3ba3dc78e67f0c5
|
4
|
+
data.tar.gz: a1dea179ace034fff0914bbc2a1eb6bcd0dd24cd274f01379a6ffd8215e56c7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7778fc311ce302caf634887a0801490043e7550d819dde8926312b0496134736dfeec474b045ecc5e74a7f29b7195a5977c52b530496edf6a54d187be0ce039e
|
7
|
+
data.tar.gz: 7ff181d7d3b7fe9ba0f87e2cdf342044a1e4442e55fe760ea5cf351994f41c0bf5d6316b33d1e920533b5ee7032dcb10a4230b5ca5d469dbedbe47919eb4063d
|
data/docker_registry2.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'registry/version'
|
5
6
|
|
@@ -7,24 +8,29 @@ Gem::Specification.new do |spec|
|
|
7
8
|
spec.name = 'docker_registry2'
|
8
9
|
spec.version = DockerRegistry2::VERSION
|
9
10
|
spec.authors = [
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
'Avi Deitcher https://github.com/deitch',
|
12
|
+
'Jonathan Hurter https://github.com/johnsudaar',
|
13
|
+
'Dmitry Fleytman https://github.com/dmitryfleytman',
|
14
|
+
'Grey Baker https://github.com/greysteil'
|
15
|
+
]
|
15
16
|
spec.summary = 'Docker v2 registry HTTP API client'
|
16
17
|
spec.description = 'Docker v2 registry HTTP API client with support for token authentication'
|
17
18
|
spec.homepage = 'https://github.com/deitch/docker_registry2'
|
18
19
|
spec.license = 'MIT'
|
19
20
|
|
20
|
-
spec.files = %w
|
21
|
-
|
22
|
-
|
21
|
+
spec.files = %w[README.md] + Dir.glob('*.gemspec') + Dir.glob('{lib}/**/*', File::FNM_DOTMATCH).reject do |f|
|
22
|
+
File.directory?(f)
|
23
|
+
end
|
24
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
25
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
26
|
spec.require_paths = ['lib']
|
24
27
|
|
25
28
|
spec.add_development_dependency 'bundler'
|
26
29
|
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
27
31
|
spec.add_development_dependency 'rubocop', '>= 0.26.0'
|
32
|
+
spec.add_development_dependency 'vcr', '~> 6'
|
33
|
+
spec.add_development_dependency 'webmock'
|
28
34
|
|
29
35
|
spec.add_dependency 'rest-client', '>= 1.8.0'
|
30
36
|
end
|
data/lib/docker_registry2.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
|
2
|
-
require File.dirname(__FILE__) + '/registry/registry'
|
3
|
-
require File.dirname(__FILE__) + '/registry/exceptions'
|
4
|
-
require File.dirname(__FILE__) + '/registry/manifest'
|
5
|
-
require File.dirname(__FILE__) + '/registry/blob'
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
3
|
+
require "#{File.dirname(__FILE__)}/registry/version"
|
4
|
+
require "#{File.dirname(__FILE__)}/registry/registry"
|
5
|
+
require "#{File.dirname(__FILE__)}/registry/exceptions"
|
6
|
+
require "#{File.dirname(__FILE__)}/registry/manifest"
|
7
|
+
require "#{File.dirname(__FILE__)}/registry/blob"
|
7
8
|
|
8
9
|
module DockerRegistry2
|
9
|
-
def self.connect(uri=
|
10
|
-
@reg = DockerRegistry2::Registry.new(uri,opts)
|
10
|
+
def self.connect(uri = 'https://registry.hub.docker.com', opts = {})
|
11
|
+
@reg = DockerRegistry2::Registry.new(uri, opts)
|
11
12
|
end
|
12
13
|
|
13
14
|
def self.search(query = '')
|
@@ -18,7 +19,7 @@ module DockerRegistry2
|
|
18
19
|
@reg.tags(repository)
|
19
20
|
end
|
20
21
|
|
21
|
-
def self.manifest(repository,tag)
|
22
|
-
@reg.manifest(repository,tag)
|
22
|
+
def self.manifest(repository, tag)
|
23
|
+
@reg.manifest(repository, tag)
|
23
24
|
end
|
24
25
|
end
|
data/lib/registry/blob.rb
CHANGED
data/lib/registry/exceptions.rb
CHANGED
@@ -1,32 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DockerRegistry2
|
2
4
|
class Exception < RuntimeError
|
3
|
-
|
4
5
|
end
|
5
|
-
|
6
|
-
class RegistryAuthenticationException <
|
6
|
+
|
7
|
+
class RegistryAuthenticationException < StandardError
|
7
8
|
end
|
8
9
|
|
9
|
-
class RegistryAuthorizationException <
|
10
|
+
class RegistryAuthorizationException < StandardError
|
10
11
|
end
|
11
12
|
|
12
|
-
class RegistryUnknownException <
|
13
|
+
class RegistryUnknownException < StandardError
|
13
14
|
end
|
14
15
|
|
15
|
-
class RegistrySSLException <
|
16
|
+
class RegistrySSLException < StandardError
|
16
17
|
end
|
17
18
|
|
18
|
-
class RegistryVersionException <
|
19
|
+
class RegistryVersionException < StandardError
|
19
20
|
end
|
20
|
-
|
21
|
-
class ReauthenticatedException <
|
21
|
+
|
22
|
+
class ReauthenticatedException < StandardError
|
22
23
|
end
|
23
|
-
|
24
|
-
class UnknownRegistryException <
|
24
|
+
|
25
|
+
class UnknownRegistryException < StandardError
|
25
26
|
end
|
26
27
|
|
27
|
-
class NotFound <
|
28
|
+
class NotFound < StandardError
|
28
29
|
end
|
29
30
|
|
30
|
-
class InvalidMethod <
|
31
|
+
class InvalidMethod < StandardError
|
31
32
|
end
|
32
|
-
end
|
33
|
+
end
|
data/lib/registry/manifest.rb
CHANGED
data/lib/registry/registry.rb
CHANGED
@@ -1,288 +1,295 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'fileutils'
|
2
4
|
require 'rest-client'
|
3
5
|
require 'json'
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
7
|
+
module DockerRegistry2
|
8
|
+
class Registry
|
9
|
+
# @param [#to_s] base_uri Docker registry base URI
|
10
|
+
# @param [Hash] options Client options
|
11
|
+
# @option options [#to_s] :user User name for basic authentication
|
12
|
+
# @option options [#to_s] :password Password for basic authentication
|
13
|
+
# @option options [#to_s] :open_timeout Time to wait for a connection with a registry.
|
14
|
+
# It is ignored if http_options[:open_timeout] is also specified.
|
15
|
+
# @option options [#to_s] :read_timeout Time to wait for data from a registry.
|
16
|
+
# It is ignored if http_options[:read_timeout] is also specified.
|
17
|
+
# @option options [Hash] :http_options Extra options for RestClient::Request.execute.
|
18
|
+
def initialize(uri, options = {})
|
19
|
+
@uri = URI.parse(uri)
|
20
|
+
@base_uri = "#{@uri.scheme}://#{@uri.host}:#{@uri.port}"
|
21
|
+
@user = options[:user]
|
22
|
+
@password = options[:password]
|
23
|
+
@http_options = options[:http_options] || {}
|
24
|
+
@http_options[:open_timeout] ||= options[:open_timeout] || 2
|
25
|
+
@http_options[:read_timeout] ||= options[:read_timeout] || 5
|
26
|
+
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
def doget(url)
|
29
|
+
doreq 'get', url
|
30
|
+
end
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
def doput(url, payload = nil)
|
33
|
+
doreq 'put', url, nil, payload
|
34
|
+
end
|
32
35
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
def dodelete(url)
|
37
|
+
doreq 'delete', url
|
38
|
+
end
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
def dohead(url)
|
41
|
+
doreq 'head', url
|
42
|
+
end
|
43
|
+
|
44
|
+
# When a result set is too large, the Docker registry returns only the first items and adds a Link header in the
|
45
|
+
# response with the URL of the next page. See <https://docs.docker.com/registry/spec/api/#pagination>. This method
|
46
|
+
# iterates over the pages and calls the given block with each response.
|
47
|
+
def paginate_doget(url)
|
48
|
+
while url
|
49
|
+
response = doget(url)
|
50
|
+
yield response
|
40
51
|
|
41
|
-
|
42
|
-
# response with the URL of the next page. See <https://docs.docker.com/registry/spec/api/#pagination>. This method
|
43
|
-
# iterates over the pages and calls the given block with each response.
|
44
|
-
def paginate_doget(url)
|
45
|
-
while url
|
46
|
-
response = doget(url)
|
47
|
-
yield response
|
52
|
+
break unless (link = response.headers[:link])
|
48
53
|
|
49
|
-
if (link = response.headers[:link])
|
50
54
|
url = parse_link_header(link)[:next]
|
51
|
-
|
52
|
-
break
|
55
|
+
|
53
56
|
end
|
54
57
|
end
|
55
|
-
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
def search(query = '')
|
60
|
+
all_repos = []
|
61
|
+
paginate_doget '/v2/_catalog' do |response|
|
62
|
+
# parse the response
|
63
|
+
repos = JSON.parse(response)['repositories']
|
64
|
+
if query.strip.length.positive?
|
65
|
+
re = Regexp.new query
|
66
|
+
repos = repos.find_all { |e| re =~ e }
|
67
|
+
end
|
68
|
+
all_repos += repos
|
65
69
|
end
|
66
|
-
all_repos
|
70
|
+
all_repos
|
67
71
|
end
|
68
|
-
all_repos
|
69
|
-
end
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
head = doget "/v2/#{repo}/manifests/#{tag}"
|
93
|
-
else
|
94
|
-
begin
|
95
|
-
head = dohead "/v2/#{repo}/manifests/#{tag}"
|
96
|
-
rescue DockerRegistry2::InvalidMethod
|
97
|
-
# in case we are in a registry pre-2.3.0, which did not support manifest HEAD
|
98
|
-
useGet = true
|
73
|
+
def tags(repo, count = nil, last = '', withHashes = false, auto_paginate: false)
|
74
|
+
# create query params
|
75
|
+
params = []
|
76
|
+
params.push(['last', last]) if last && last != ''
|
77
|
+
params.push(['n', count]) unless count.nil?
|
78
|
+
|
79
|
+
query_vars = ''
|
80
|
+
query_vars = "?#{URI.encode_www_form(params)}" if params.length.positive?
|
81
|
+
|
82
|
+
response = doget "/v2/#{repo}/tags/list#{query_vars}"
|
83
|
+
# parse the response
|
84
|
+
resp = JSON.parse response
|
85
|
+
# parse out next page link if necessary
|
86
|
+
resp['last'] = last(response.headers[:link]) if response.headers[:link]
|
87
|
+
|
88
|
+
# do we include the hashes?
|
89
|
+
if withHashes
|
90
|
+
useGet = false
|
91
|
+
resp['hashes'] = {}
|
92
|
+
resp['tags'].each do |tag|
|
93
|
+
if useGet
|
99
94
|
head = doget "/v2/#{repo}/manifests/#{tag}"
|
95
|
+
else
|
96
|
+
begin
|
97
|
+
head = dohead "/v2/#{repo}/manifests/#{tag}"
|
98
|
+
rescue DockerRegistry2::InvalidMethod
|
99
|
+
# in case we are in a registry pre-2.3.0, which did not support manifest HEAD
|
100
|
+
useGet = true
|
101
|
+
head = doget "/v2/#{repo}/manifests/#{tag}"
|
102
|
+
end
|
100
103
|
end
|
104
|
+
resp['hashes'][tag] = head.headers[:docker_content_digest]
|
101
105
|
end
|
102
|
-
resp["hashes"][tag] = head.headers[:docker_content_digest]
|
103
106
|
end
|
104
|
-
end
|
105
107
|
|
106
|
-
|
108
|
+
return resp unless auto_paginate
|
107
109
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
110
|
+
while (last_tag = resp.delete('last'))
|
111
|
+
additional_tags = tags(repo, count, last_tag, withHashes)
|
112
|
+
resp['last'] = additional_tags['last']
|
113
|
+
resp['tags'] += additional_tags['tags']
|
114
|
+
resp['tags'] = resp['tags'].uniq
|
115
|
+
resp['hashes'].merge!(additional_tags['hashes']) if withHashes
|
116
|
+
end
|
117
|
+
|
118
|
+
resp
|
114
119
|
end
|
115
120
|
|
116
|
-
|
117
|
-
|
121
|
+
def manifest(repo, tag)
|
122
|
+
# first get the manifest
|
123
|
+
response = doget "/v2/#{repo}/manifests/#{tag}"
|
124
|
+
parsed = JSON.parse response.body
|
125
|
+
manifest = DockerRegistry2::Manifest[parsed]
|
126
|
+
manifest.body = response.body
|
127
|
+
manifest.headers = response.headers
|
128
|
+
manifest
|
129
|
+
end
|
118
130
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
131
|
+
def blob(repo, digest, outpath = nil)
|
132
|
+
blob_url = "/v2/#{repo}/blobs/#{digest}"
|
133
|
+
if outpath.nil?
|
134
|
+
response = doget(blob_url)
|
135
|
+
DockerRegistry2::Blob.new(response.headers, response.body)
|
136
|
+
else
|
137
|
+
File.open(outpath, 'w') do |fd|
|
138
|
+
doreq('get', blob_url, fd)
|
139
|
+
end
|
128
140
|
|
129
|
-
|
130
|
-
blob_url = "/v2/#{repo}/blobs/#{digest}"
|
131
|
-
if outpath.nil?
|
132
|
-
response = doget(blob_url)
|
133
|
-
DockerRegistry2::Blob.new(response.headers, response.body)
|
134
|
-
else
|
135
|
-
File.open(outpath, 'w') do |fd|
|
136
|
-
doreq('get', blob_url, fd)
|
141
|
+
outpath
|
137
142
|
end
|
138
|
-
|
139
|
-
outpath
|
140
143
|
end
|
141
|
-
end
|
142
144
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
145
|
+
def digest(repo, tag)
|
146
|
+
tag_path = "/v2/#{repo}/manifests/#{tag}"
|
147
|
+
dohead(tag_path).headers[:docker_content_digest]
|
148
|
+
rescue DockerRegistry2::InvalidMethod
|
149
|
+
# Pre-2.3.0 registries didn't support manifest HEAD requests
|
150
|
+
doget(tag_path).headers[:docker_content_digest]
|
151
|
+
end
|
150
152
|
|
151
|
-
|
152
|
-
|
153
|
-
|
153
|
+
def rmtag(image, tag)
|
154
|
+
# TODO: Need full response back. Rewrite other manifests() calls without JSON?
|
155
|
+
reference = doget("/v2/#{image}/manifests/#{tag}").headers[:docker_content_digest]
|
154
156
|
|
155
|
-
|
156
|
-
|
157
|
+
dodelete("/v2/#{image}/manifests/#{reference}").code
|
158
|
+
end
|
157
159
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
160
|
+
def pull(repo, tag, dir)
|
161
|
+
# make sure the directory exists
|
162
|
+
FileUtils.mkdir_p dir
|
163
|
+
# get the manifest
|
164
|
+
m = manifest repo, tag
|
165
|
+
# puts "pulling #{repo}:#{tag} into #{dir}"
|
166
|
+
# manifest can contain multiple manifests one for each API version
|
167
|
+
downloaded_layers = []
|
168
|
+
downloaded_layers += _pull_v2(repo, m, dir) if m['schemaVersion'] == 2
|
169
|
+
downloaded_layers += _pull_v1(repo, m, dir) if m['schemaVersion'] == 1
|
170
|
+
# return downloaded_layers
|
171
|
+
downloaded_layers
|
172
|
+
end
|
171
173
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
174
|
+
def _pull_v2(repo, manifest, dir)
|
175
|
+
# make sure the directory exists
|
176
|
+
FileUtils.mkdir_p dir
|
177
|
+
return false unless manifest['schemaVersion'] == 2
|
178
|
+
|
179
|
+
# pull each of the layers
|
180
|
+
manifest['layers'].each do |layer|
|
181
|
+
# define path of file to save layer in
|
182
|
+
layer_file = "#{dir}/#{layer['digest']}"
|
183
|
+
# skip layer if we already got it
|
184
|
+
next if File.file? layer_file
|
185
|
+
|
186
|
+
# download layer
|
187
|
+
# puts "getting layer (v2) #{layer['digest']}"
|
188
|
+
blob(repo, layer['digest'], layer_file)
|
189
|
+
layer_file
|
190
|
+
end
|
186
191
|
end
|
187
|
-
end
|
188
192
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
193
|
+
def _pull_v1(repo, manifest, dir)
|
194
|
+
# make sure the directory exists
|
195
|
+
FileUtils.mkdir_p dir
|
196
|
+
return false unless manifest['schemaVersion'] == 1
|
197
|
+
|
198
|
+
# pull each of the layers
|
199
|
+
manifest['fsLayers'].each do |layer|
|
200
|
+
# define path of file to save layer in
|
201
|
+
layer_file = "#{dir}/#{layer['blobSum']}"
|
202
|
+
# skip layer if we already got it
|
203
|
+
next if File.file? layer_file
|
204
|
+
|
205
|
+
# download layer
|
206
|
+
# puts "getting layer (v1) #{layer['blobSum']}"
|
207
|
+
blob(repo, layer['blobSum'], layer_file)
|
208
|
+
# return layer file
|
209
|
+
layer_file
|
210
|
+
end
|
204
211
|
end
|
205
|
-
end
|
206
212
|
|
207
|
-
|
208
|
-
end
|
213
|
+
def push(manifest, dir); end
|
209
214
|
|
210
|
-
|
211
|
-
|
215
|
+
def tag(repo, tag, newrepo, newtag)
|
216
|
+
manifest = manifest(repo, tag)
|
217
|
+
|
218
|
+
raise DockerRegistry2::RegistryVersionException unless manifest['schemaVersion'] == 2
|
212
219
|
|
213
|
-
if manifest['schemaVersion'] == 2
|
214
220
|
doput "/v2/#{newrepo}/manifests/#{newtag}", manifest.to_json
|
215
|
-
else
|
216
|
-
raise DockerRegistry2::RegistryVersionException
|
217
221
|
end
|
218
|
-
end
|
219
222
|
|
220
|
-
|
221
|
-
end
|
223
|
+
def copy(repo, tag, newregistry, newrepo, newtag); end
|
222
224
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
225
|
+
# gets the size of a particular blob, given the repo and the content-addressable hash
|
226
|
+
# usually unneeded, since manifest includes it
|
227
|
+
def blob_size(repo, blobSum)
|
228
|
+
response = dohead "/v2/#{repo}/blobs/#{blobSum}"
|
229
|
+
Integer(response.headers[:content_length], 10)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Parse the value of the Link HTTP header and return a Hash whose keys are the rel values turned into symbols, and
|
233
|
+
# the values are URLs. For example, `{ next: '/v2/_catalog?n=100&last=x' }`.
|
234
|
+
def parse_link_header(header)
|
235
|
+
last = ''
|
236
|
+
parts = header.split(',')
|
237
|
+
links = {}
|
238
|
+
|
239
|
+
# Parse each part into a named link
|
240
|
+
parts.each do |part, _index|
|
241
|
+
section = part.split(';')
|
242
|
+
url = section[0][/<(.*)>/, 1]
|
243
|
+
name = section[1][/rel="?([^"]*)"?/, 1].to_sym
|
244
|
+
links[name] = url
|
245
|
+
end
|
229
246
|
|
230
|
-
|
231
|
-
# the values are URLs. For example, `{ next: '/v2/_catalog?n=100&last=x' }`.
|
232
|
-
def parse_link_header(header)
|
233
|
-
last=''
|
234
|
-
parts = header.split(',')
|
235
|
-
links = Hash.new
|
236
|
-
|
237
|
-
# Parse each part into a named link
|
238
|
-
parts.each do |part, index|
|
239
|
-
section = part.split(';')
|
240
|
-
url = section[0][/<(.*)>/,1]
|
241
|
-
name = section[1][/rel="?([^"]*)"?/,1].to_sym
|
242
|
-
links[name] = url
|
247
|
+
links
|
243
248
|
end
|
244
249
|
|
245
|
-
|
246
|
-
|
250
|
+
def last(header)
|
251
|
+
links = parse_link_header(header)
|
252
|
+
if links[:next]
|
253
|
+
query = URI(links[:next]).query
|
254
|
+
link_key = @uri.host.eql?('quay.io') ? 'next_page' : 'last'
|
255
|
+
last = URI.decode_www_form(query).to_h[link_key]
|
247
256
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
query=URI(links[:next]).query
|
252
|
-
link_key = @uri.host.eql?('quay.io') ? 'next_page' : 'last'
|
253
|
-
last=URI::decode_www_form(query).to_h[link_key]
|
257
|
+
end
|
258
|
+
last
|
259
|
+
end
|
254
260
|
|
261
|
+
def manifest_sum(manifest)
|
262
|
+
size = 0
|
263
|
+
manifest['layers'].each do |layer|
|
264
|
+
size += layer['size']
|
265
|
+
end
|
266
|
+
size
|
255
267
|
end
|
256
|
-
last
|
257
|
-
end
|
258
268
|
|
259
|
-
|
260
|
-
size = 0
|
261
|
-
manifest["layers"].each { |layer|
|
262
|
-
size += layer["size"]
|
263
|
-
}
|
264
|
-
size
|
265
|
-
end
|
269
|
+
private
|
266
270
|
|
267
|
-
|
268
|
-
def doreq(type,url,stream=nil,payload=nil)
|
271
|
+
def doreq(type, url, stream = nil, payload = nil)
|
269
272
|
begin
|
270
|
-
block = stream.nil?
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
273
|
+
block = if stream.nil?
|
274
|
+
nil
|
275
|
+
else
|
276
|
+
proc { |response|
|
277
|
+
response.read_body do |chunk|
|
278
|
+
stream.write chunk
|
279
|
+
end
|
280
|
+
}
|
281
|
+
end
|
275
282
|
response = RestClient::Request.execute(@http_options.merge(
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
283
|
+
method: type,
|
284
|
+
url: @base_uri + url,
|
285
|
+
headers: headers(payload: payload),
|
286
|
+
block_response: block,
|
287
|
+
payload: payload
|
288
|
+
))
|
282
289
|
rescue SocketError
|
283
290
|
raise DockerRegistry2::RegistryUnknownException
|
284
|
-
rescue RestClient::NotFound
|
285
|
-
raise DockerRegistry2::NotFound,
|
291
|
+
rescue RestClient::NotFound
|
292
|
+
raise DockerRegistry2::NotFound, "Image not found at #{@uri.host}"
|
286
293
|
rescue RestClient::Unauthorized => e
|
287
294
|
header = e.response.headers[:www_authenticate]
|
288
295
|
method = header.to_s.downcase.split(' ')[0]
|
@@ -295,96 +302,102 @@ class DockerRegistry2::Registry
|
|
295
302
|
raise DockerRegistry2::RegistryUnknownException
|
296
303
|
end
|
297
304
|
end
|
298
|
-
|
305
|
+
response
|
299
306
|
end
|
300
307
|
|
301
|
-
def do_basic_req(type, url, stream=nil, payload=nil)
|
308
|
+
def do_basic_req(type, url, stream = nil, payload = nil)
|
302
309
|
begin
|
303
|
-
block = stream.nil?
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
310
|
+
block = if stream.nil?
|
311
|
+
nil
|
312
|
+
else
|
313
|
+
proc { |response|
|
314
|
+
response.read_body do |chunk|
|
315
|
+
stream.write chunk
|
316
|
+
end
|
317
|
+
}
|
318
|
+
end
|
308
319
|
response = RestClient::Request.execute(@http_options.merge(
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
320
|
+
method: type,
|
321
|
+
url: @base_uri + url,
|
322
|
+
user: @user,
|
323
|
+
password: @password,
|
324
|
+
headers: headers(payload: payload),
|
325
|
+
block_response: block,
|
326
|
+
payload: payload
|
327
|
+
))
|
317
328
|
rescue SocketError
|
318
329
|
raise DockerRegistry2::RegistryUnknownException
|
319
330
|
rescue RestClient::Unauthorized
|
320
331
|
raise DockerRegistry2::RegistryAuthenticationException
|
321
332
|
rescue RestClient::MethodNotAllowed
|
322
333
|
raise DockerRegistry2::InvalidMethod
|
323
|
-
rescue RestClient::NotFound =>
|
324
|
-
raise DockerRegistry2::NotFound,
|
334
|
+
rescue RestClient::NotFound => e
|
335
|
+
raise DockerRegistry2::NotFound, e
|
325
336
|
end
|
326
|
-
|
337
|
+
response
|
327
338
|
end
|
328
339
|
|
329
|
-
def do_bearer_req(type, url, header, stream=false, payload=nil)
|
340
|
+
def do_bearer_req(type, url, header, stream = false, payload = nil)
|
330
341
|
token = authenticate_bearer(header)
|
331
342
|
begin
|
332
|
-
block = stream.nil?
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
343
|
+
block = if stream.nil?
|
344
|
+
nil
|
345
|
+
else
|
346
|
+
proc { |response|
|
347
|
+
response.read_body do |chunk|
|
348
|
+
stream.write chunk
|
349
|
+
end
|
350
|
+
}
|
351
|
+
end
|
337
352
|
response = RestClient::Request.execute(@http_options.merge(
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
353
|
+
method: type,
|
354
|
+
url: @base_uri + url,
|
355
|
+
headers: headers(payload: payload, bearer_token: token),
|
356
|
+
block_response: block,
|
357
|
+
payload: payload
|
358
|
+
))
|
344
359
|
rescue SocketError
|
345
360
|
raise DockerRegistry2::RegistryUnknownException
|
346
361
|
rescue RestClient::Unauthorized
|
347
362
|
raise DockerRegistry2::RegistryAuthenticationException
|
348
363
|
rescue RestClient::MethodNotAllowed
|
349
364
|
raise DockerRegistry2::InvalidMethod
|
350
|
-
rescue RestClient::NotFound =>
|
351
|
-
raise DockerRegistry2::NotFound,
|
365
|
+
rescue RestClient::NotFound => e
|
366
|
+
raise DockerRegistry2::NotFound, e
|
352
367
|
end
|
353
368
|
|
354
|
-
|
369
|
+
response
|
355
370
|
end
|
356
371
|
|
357
372
|
def authenticate_bearer(header)
|
358
373
|
# get the parts we need
|
359
374
|
target = split_auth_header(header)
|
360
375
|
# did we have a username and password?
|
361
|
-
if defined? @user
|
362
|
-
target[:params][:account] = @user
|
363
|
-
end
|
376
|
+
target[:params][:account] = @user if defined? @user && !@user.to_s.strip.empty?
|
364
377
|
# authenticate against the realm
|
365
378
|
uri = URI.parse(target[:realm])
|
366
379
|
begin
|
367
380
|
response = RestClient::Request.execute(@http_options.merge(
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
381
|
+
method: :get,
|
382
|
+
url: uri.to_s, headers: { params: target[:params] },
|
383
|
+
user: @user,
|
384
|
+
password: @password
|
385
|
+
))
|
373
386
|
rescue RestClient::Unauthorized, RestClient::Forbidden
|
374
387
|
# bad authentication
|
375
388
|
raise DockerRegistry2::RegistryAuthenticationException
|
376
|
-
rescue RestClient::NotFound =>
|
377
|
-
raise DockerRegistry2::NotFound,
|
389
|
+
rescue RestClient::NotFound => e
|
390
|
+
raise DockerRegistry2::NotFound, e
|
378
391
|
end
|
379
392
|
# now save the web token
|
380
393
|
result = JSON.parse(response)
|
381
|
-
|
394
|
+
result['token'] || result['access_token']
|
382
395
|
end
|
383
396
|
|
384
397
|
def split_auth_header(header = '')
|
385
|
-
h =
|
386
|
-
h = {params: {}}
|
387
|
-
header.scan(/(
|
398
|
+
h = {}
|
399
|
+
h = { params: {} }
|
400
|
+
header.scan(/(\w+)="([^"]+)"/) do |entry|
|
388
401
|
case entry[0]
|
389
402
|
when 'realm'
|
390
403
|
h[:realm] = entry[1]
|
@@ -396,11 +409,12 @@ class DockerRegistry2::Registry
|
|
396
409
|
end
|
397
410
|
|
398
411
|
def headers(payload: nil, bearer_token: nil)
|
399
|
-
headers={}
|
400
|
-
headers['Authorization']="Bearer #{bearer_token}" unless bearer_token.nil?
|
401
|
-
headers['Accept']='application/vnd.docker.distribution.manifest.v2+json,
|
402
|
-
headers['Content-Type']='application/vnd.docker.distribution.manifest.v2+json' unless payload.nil?
|
412
|
+
headers = {}
|
413
|
+
headers['Authorization'] = "Bearer #{bearer_token}" unless bearer_token.nil?
|
414
|
+
headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json,application/vnd.docker.distribution.manifest.list.v2+json,application/json' if payload.nil?
|
415
|
+
headers['Content-Type'] = 'application/vnd.docker.distribution.manifest.v2+json' unless payload.nil?
|
403
416
|
|
404
417
|
headers
|
405
418
|
end
|
419
|
+
end
|
406
420
|
end
|
data/lib/registry/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: docker_registry2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Avi Deitcher https://github.com/deitch
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2022-
|
14
|
+
date: 2022-12-23 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: bundler
|
@@ -41,6 +41,20 @@ dependencies:
|
|
41
41
|
- - "~>"
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: '10.0'
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: rspec
|
46
|
+
requirement: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - "~>"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '3'
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3'
|
44
58
|
- !ruby/object:Gem::Dependency
|
45
59
|
name: rubocop
|
46
60
|
requirement: !ruby/object:Gem::Requirement
|
@@ -55,6 +69,34 @@ dependencies:
|
|
55
69
|
- - ">="
|
56
70
|
- !ruby/object:Gem::Version
|
57
71
|
version: 0.26.0
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: vcr
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - "~>"
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '6'
|
79
|
+
type: :development
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - "~>"
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '6'
|
86
|
+
- !ruby/object:Gem::Dependency
|
87
|
+
name: webmock
|
88
|
+
requirement: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
type: :development
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
58
100
|
- !ruby/object:Gem::Dependency
|
59
101
|
name: rest-client
|
60
102
|
requirement: !ruby/object:Gem::Requirement
|
@@ -102,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
144
|
- !ruby/object:Gem::Version
|
103
145
|
version: '0'
|
104
146
|
requirements: []
|
105
|
-
rubygems_version: 3.
|
147
|
+
rubygems_version: 3.3.26
|
106
148
|
signing_key:
|
107
149
|
specification_version: 4
|
108
150
|
summary: Docker v2 registry HTTP API client
|