docker_registry2 1.12.0 → 1.14.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/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 +298 -281
- 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: 6c331456d39ce2a4a57002e7117101f261b2b78bea3b056843430b6c8ccc2110
|
4
|
+
data.tar.gz: 2370009afd0319cfc35a49443e51ad6566508362e5cf3fce941af793c16ac885
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32a04a5987fa31cd7feb34d476f82b771e02fd4e5f93caeb4888ad27da0ab7b20cd5cf279f3f06954c599cf68ac930ed9183e5b0410ef90642110bd9ca859b62
|
7
|
+
data.tar.gz: 6ae15951f3f7ca11af1700a701fbc5e270f4adf9a9fb1998bb3657ef85fc79bd1c7cb18e1f9e598e8d592eec9d3c2dfb412d34c7c39fab26b7a3df69585b0dc0
|
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,291 @@
|
|
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 # rubocop:disable Metrics/ClassLength
|
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
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
+
loop do
|
49
|
+
response = doget(url)
|
50
|
+
yield response
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
52
|
+
link_header = response.headers[:link]
|
53
|
+
break unless link_header
|
54
|
+
|
55
|
+
url = parse_link_header(link_header)[:next]
|
53
56
|
end
|
54
57
|
end
|
55
|
-
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
re = Regexp.new query
|
64
|
-
repos = repos.find_all {|e| re =~ e }
|
59
|
+
def search(query = '')
|
60
|
+
all_repos = []
|
61
|
+
paginate_doget('/v2/_catalog') do |response|
|
62
|
+
repos = JSON.parse(response)['repositories']
|
63
|
+
repos.select! { |repo| repo.match?(/#{query}/) } unless query.empty?
|
64
|
+
all_repos += repos
|
65
65
|
end
|
66
|
-
all_repos
|
66
|
+
all_repos
|
67
67
|
end
|
68
|
-
all_repos
|
69
|
-
end
|
70
68
|
|
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
|
69
|
+
def tags(repo, count = nil, last = '', withHashes = false, auto_paginate: false)
|
70
|
+
# create query params
|
71
|
+
params = []
|
72
|
+
params.push(['last', last]) if last && last != ''
|
73
|
+
params.push(['n', count]) unless count.nil?
|
74
|
+
|
75
|
+
query_vars = ''
|
76
|
+
query_vars = "?#{URI.encode_www_form(params)}" if params.length.positive?
|
77
|
+
|
78
|
+
response = doget "/v2/#{repo}/tags/list#{query_vars}"
|
79
|
+
# parse the response
|
80
|
+
resp = JSON.parse response
|
81
|
+
# parse out next page link if necessary
|
82
|
+
resp['last'] = last(response.headers[:link]) if response.headers[:link]
|
83
|
+
|
84
|
+
# do we include the hashes?
|
85
|
+
if withHashes
|
86
|
+
useGet = false
|
87
|
+
resp['hashes'] = {}
|
88
|
+
resp['tags'].each do |tag|
|
89
|
+
if useGet
|
99
90
|
head = doget "/v2/#{repo}/manifests/#{tag}"
|
91
|
+
else
|
92
|
+
begin
|
93
|
+
head = dohead "/v2/#{repo}/manifests/#{tag}"
|
94
|
+
rescue DockerRegistry2::InvalidMethod
|
95
|
+
# in case we are in a registry pre-2.3.0, which did not support manifest HEAD
|
96
|
+
useGet = true
|
97
|
+
head = doget "/v2/#{repo}/manifests/#{tag}"
|
98
|
+
end
|
100
99
|
end
|
100
|
+
resp['hashes'][tag] = head.headers[:docker_content_digest]
|
101
101
|
end
|
102
|
-
resp["hashes"][tag] = head.headers[:docker_content_digest]
|
103
102
|
end
|
104
|
-
end
|
105
103
|
|
106
|
-
|
104
|
+
return resp unless auto_paginate
|
105
|
+
|
106
|
+
while (last_tag = resp.delete('last'))
|
107
|
+
additional_tags = tags(repo, count, last_tag, withHashes)
|
108
|
+
resp['last'] = additional_tags['last']
|
109
|
+
resp['tags'] += additional_tags['tags']
|
110
|
+
resp['tags'] = resp['tags'].uniq
|
111
|
+
resp['hashes'].merge!(additional_tags['hashes']) if withHashes
|
112
|
+
end
|
107
113
|
|
108
|
-
|
109
|
-
additional_tags = tags(repo, count, last_tag, withHashes)
|
110
|
-
resp["last"] = additional_tags["last"]
|
111
|
-
resp["tags"] += additional_tags["tags"]
|
112
|
-
resp["tags"] = resp["tags"].uniq
|
113
|
-
resp["hashes"].merge!(additional_tags["hashes"]) if withHashes
|
114
|
+
resp
|
114
115
|
end
|
115
116
|
|
116
|
-
|
117
|
-
|
117
|
+
def manifest(repo, tag)
|
118
|
+
# first get the manifest
|
119
|
+
response = doget "/v2/#{repo}/manifests/#{tag}"
|
120
|
+
parsed = JSON.parse response.body
|
121
|
+
manifest = DockerRegistry2::Manifest[parsed]
|
122
|
+
manifest.body = response.body
|
123
|
+
manifest.headers = response.headers
|
124
|
+
manifest
|
125
|
+
end
|
118
126
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
127
|
+
def blob(repo, digest, outpath = nil)
|
128
|
+
blob_url = "/v2/#{repo}/blobs/#{digest}"
|
129
|
+
if outpath.nil?
|
130
|
+
response = doget(blob_url)
|
131
|
+
DockerRegistry2::Blob.new(response.headers, response.body)
|
132
|
+
else
|
133
|
+
File.open(outpath, 'w') do |fd|
|
134
|
+
doreq('get', blob_url, fd)
|
135
|
+
end
|
128
136
|
|
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)
|
137
|
+
outpath
|
137
138
|
end
|
138
|
-
|
139
|
-
outpath
|
140
139
|
end
|
141
|
-
end
|
142
140
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
141
|
+
def digest(repo, tag)
|
142
|
+
tag_path = "/v2/#{repo}/manifests/#{tag}"
|
143
|
+
dohead(tag_path).headers[:docker_content_digest]
|
144
|
+
rescue DockerRegistry2::InvalidMethod
|
145
|
+
# Pre-2.3.0 registries didn't support manifest HEAD requests
|
146
|
+
doget(tag_path).headers[:docker_content_digest]
|
147
|
+
end
|
150
148
|
|
151
|
-
|
152
|
-
|
153
|
-
|
149
|
+
def rmtag(image, tag)
|
150
|
+
# TODO: Need full response back. Rewrite other manifests() calls without JSON?
|
151
|
+
reference = doget("/v2/#{image}/manifests/#{tag}").headers[:docker_content_digest]
|
154
152
|
|
155
|
-
|
156
|
-
|
153
|
+
dodelete("/v2/#{image}/manifests/#{reference}").code
|
154
|
+
end
|
157
155
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
156
|
+
def pull(repo, tag, dir)
|
157
|
+
# make sure the directory exists
|
158
|
+
FileUtils.mkdir_p dir
|
159
|
+
# get the manifest
|
160
|
+
m = manifest repo, tag
|
161
|
+
# puts "pulling #{repo}:#{tag} into #{dir}"
|
162
|
+
# manifest can contain multiple manifests one for each API version
|
163
|
+
downloaded_layers = []
|
164
|
+
downloaded_layers += _pull_v2(repo, m, dir) if m['schemaVersion'] == 2
|
165
|
+
downloaded_layers += _pull_v1(repo, m, dir) if m['schemaVersion'] == 1
|
166
|
+
# return downloaded_layers
|
167
|
+
downloaded_layers
|
168
|
+
end
|
171
169
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
170
|
+
def _pull_v2(repo, manifest, dir)
|
171
|
+
# make sure the directory exists
|
172
|
+
FileUtils.mkdir_p dir
|
173
|
+
return false unless manifest['schemaVersion'] == 2
|
174
|
+
|
175
|
+
# pull each of the layers
|
176
|
+
manifest['layers'].each do |layer|
|
177
|
+
# define path of file to save layer in
|
178
|
+
layer_file = "#{dir}/#{layer['digest']}"
|
179
|
+
# skip layer if we already got it
|
180
|
+
next if File.file? layer_file
|
181
|
+
|
182
|
+
# download layer
|
183
|
+
# puts "getting layer (v2) #{layer['digest']}"
|
184
|
+
blob(repo, layer['digest'], layer_file)
|
185
|
+
layer_file
|
186
|
+
end
|
186
187
|
end
|
187
|
-
end
|
188
188
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
189
|
+
def _pull_v1(repo, manifest, dir)
|
190
|
+
# make sure the directory exists
|
191
|
+
FileUtils.mkdir_p dir
|
192
|
+
return false unless manifest['schemaVersion'] == 1
|
193
|
+
|
194
|
+
# pull each of the layers
|
195
|
+
manifest['fsLayers'].each do |layer|
|
196
|
+
# define path of file to save layer in
|
197
|
+
layer_file = "#{dir}/#{layer['blobSum']}"
|
198
|
+
# skip layer if we already got it
|
199
|
+
next if File.file? layer_file
|
200
|
+
|
201
|
+
# download layer
|
202
|
+
# puts "getting layer (v1) #{layer['blobSum']}"
|
203
|
+
blob(repo, layer['blobSum'], layer_file)
|
204
|
+
# return layer file
|
205
|
+
layer_file
|
206
|
+
end
|
204
207
|
end
|
205
|
-
end
|
206
208
|
|
207
|
-
|
208
|
-
|
209
|
+
def push(manifest, dir); end
|
210
|
+
|
211
|
+
def tag(repo, tag, newrepo, newtag)
|
212
|
+
manifest = manifest(repo, tag)
|
209
213
|
|
210
|
-
|
211
|
-
manifest = manifest(repo, tag)
|
214
|
+
raise DockerRegistry2::RegistryVersionException unless manifest['schemaVersion'] == 2
|
212
215
|
|
213
|
-
if manifest['schemaVersion'] == 2
|
214
216
|
doput "/v2/#{newrepo}/manifests/#{newtag}", manifest.to_json
|
215
|
-
else
|
216
|
-
raise DockerRegistry2::RegistryVersionException
|
217
217
|
end
|
218
|
-
end
|
219
218
|
|
220
|
-
|
221
|
-
end
|
219
|
+
def copy(repo, tag, newregistry, newrepo, newtag); end
|
222
220
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
221
|
+
# gets the size of a particular blob, given the repo and the content-addressable hash
|
222
|
+
# usually unneeded, since manifest includes it
|
223
|
+
def blob_size(repo, blobSum)
|
224
|
+
response = dohead "/v2/#{repo}/blobs/#{blobSum}"
|
225
|
+
Integer(response.headers[:content_length], 10)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Parse the value of the Link HTTP header and return a Hash whose keys are the rel values turned into symbols, and
|
229
|
+
# the values are URLs. For example, `{ next: '/v2/_catalog?n=100&last=x' }`.
|
230
|
+
def parse_link_header(header)
|
231
|
+
last = ''
|
232
|
+
parts = header.split(',')
|
233
|
+
links = {}
|
234
|
+
|
235
|
+
# Parse each part into a named link
|
236
|
+
parts.each do |part, _index|
|
237
|
+
section = part.split(';')
|
238
|
+
url = section[0][/<(.*)>/, 1]
|
239
|
+
name = section[1][/rel="?([^"]*)"?/, 1].to_sym
|
240
|
+
links[name] = url
|
241
|
+
end
|
229
242
|
|
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
|
243
|
+
links
|
243
244
|
end
|
244
245
|
|
245
|
-
|
246
|
-
|
246
|
+
def last(header)
|
247
|
+
links = parse_link_header(header)
|
248
|
+
if links[:next]
|
249
|
+
query = URI(links[:next]).query
|
250
|
+
link_key = @uri.host.eql?('quay.io') ? 'next_page' : 'last'
|
251
|
+
last = URI.decode_www_form(query).to_h[link_key]
|
247
252
|
|
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]
|
253
|
+
end
|
254
|
+
last
|
255
|
+
end
|
254
256
|
|
257
|
+
def manifest_sum(manifest)
|
258
|
+
size = 0
|
259
|
+
manifest['layers'].each do |layer|
|
260
|
+
size += layer['size']
|
261
|
+
end
|
262
|
+
size
|
255
263
|
end
|
256
|
-
last
|
257
|
-
end
|
258
264
|
|
259
|
-
|
260
|
-
size = 0
|
261
|
-
manifest["layers"].each { |layer|
|
262
|
-
size += layer["size"]
|
263
|
-
}
|
264
|
-
size
|
265
|
-
end
|
265
|
+
private
|
266
266
|
|
267
|
-
|
268
|
-
def doreq(type,url,stream=nil,payload=nil)
|
267
|
+
def doreq(type, url, stream = nil, payload = nil)
|
269
268
|
begin
|
270
|
-
block = stream.nil?
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
269
|
+
block = if stream.nil?
|
270
|
+
nil
|
271
|
+
else
|
272
|
+
proc { |response|
|
273
|
+
response.read_body do |chunk|
|
274
|
+
stream.write chunk
|
275
|
+
end
|
276
|
+
}
|
277
|
+
end
|
275
278
|
response = RestClient::Request.execute(@http_options.merge(
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
279
|
+
method: type,
|
280
|
+
url: @base_uri + url,
|
281
|
+
headers: headers(payload: payload),
|
282
|
+
block_response: block,
|
283
|
+
payload: payload
|
284
|
+
))
|
282
285
|
rescue SocketError
|
283
286
|
raise DockerRegistry2::RegistryUnknownException
|
284
|
-
rescue RestClient::NotFound
|
285
|
-
raise DockerRegistry2::NotFound,
|
287
|
+
rescue RestClient::NotFound
|
288
|
+
raise DockerRegistry2::NotFound, "Image not found at #{@uri.host}"
|
286
289
|
rescue RestClient::Unauthorized => e
|
287
290
|
header = e.response.headers[:www_authenticate]
|
288
291
|
method = header.to_s.downcase.split(' ')[0]
|
@@ -295,96 +298,102 @@ class DockerRegistry2::Registry
|
|
295
298
|
raise DockerRegistry2::RegistryUnknownException
|
296
299
|
end
|
297
300
|
end
|
298
|
-
|
301
|
+
response
|
299
302
|
end
|
300
303
|
|
301
|
-
def do_basic_req(type, url, stream=nil, payload=nil)
|
304
|
+
def do_basic_req(type, url, stream = nil, payload = nil)
|
302
305
|
begin
|
303
|
-
block = stream.nil?
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
306
|
+
block = if stream.nil?
|
307
|
+
nil
|
308
|
+
else
|
309
|
+
proc { |response|
|
310
|
+
response.read_body do |chunk|
|
311
|
+
stream.write chunk
|
312
|
+
end
|
313
|
+
}
|
314
|
+
end
|
308
315
|
response = RestClient::Request.execute(@http_options.merge(
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
316
|
+
method: type,
|
317
|
+
url: @base_uri + url,
|
318
|
+
user: @user,
|
319
|
+
password: @password,
|
320
|
+
headers: headers(payload: payload),
|
321
|
+
block_response: block,
|
322
|
+
payload: payload
|
323
|
+
))
|
317
324
|
rescue SocketError
|
318
325
|
raise DockerRegistry2::RegistryUnknownException
|
319
326
|
rescue RestClient::Unauthorized
|
320
327
|
raise DockerRegistry2::RegistryAuthenticationException
|
321
328
|
rescue RestClient::MethodNotAllowed
|
322
329
|
raise DockerRegistry2::InvalidMethod
|
323
|
-
rescue RestClient::NotFound =>
|
324
|
-
raise DockerRegistry2::NotFound,
|
330
|
+
rescue RestClient::NotFound => e
|
331
|
+
raise DockerRegistry2::NotFound, e
|
325
332
|
end
|
326
|
-
|
333
|
+
response
|
327
334
|
end
|
328
335
|
|
329
|
-
def do_bearer_req(type, url, header, stream=false, payload=nil)
|
336
|
+
def do_bearer_req(type, url, header, stream = false, payload = nil)
|
330
337
|
token = authenticate_bearer(header)
|
331
338
|
begin
|
332
|
-
block = stream.nil?
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
339
|
+
block = if stream.nil?
|
340
|
+
nil
|
341
|
+
else
|
342
|
+
proc { |response|
|
343
|
+
response.read_body do |chunk|
|
344
|
+
stream.write chunk
|
345
|
+
end
|
346
|
+
}
|
347
|
+
end
|
337
348
|
response = RestClient::Request.execute(@http_options.merge(
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
349
|
+
method: type,
|
350
|
+
url: @base_uri + url,
|
351
|
+
headers: headers(payload: payload, bearer_token: token),
|
352
|
+
block_response: block,
|
353
|
+
payload: payload
|
354
|
+
))
|
344
355
|
rescue SocketError
|
345
356
|
raise DockerRegistry2::RegistryUnknownException
|
346
357
|
rescue RestClient::Unauthorized
|
347
358
|
raise DockerRegistry2::RegistryAuthenticationException
|
348
359
|
rescue RestClient::MethodNotAllowed
|
349
360
|
raise DockerRegistry2::InvalidMethod
|
350
|
-
rescue RestClient::NotFound =>
|
351
|
-
raise DockerRegistry2::NotFound,
|
361
|
+
rescue RestClient::NotFound => e
|
362
|
+
raise DockerRegistry2::NotFound, e
|
352
363
|
end
|
353
364
|
|
354
|
-
|
365
|
+
response
|
355
366
|
end
|
356
367
|
|
357
368
|
def authenticate_bearer(header)
|
358
369
|
# get the parts we need
|
359
370
|
target = split_auth_header(header)
|
360
371
|
# did we have a username and password?
|
361
|
-
if defined? @user
|
362
|
-
target[:params][:account] = @user
|
363
|
-
end
|
372
|
+
target[:params][:account] = @user if defined? @user && !@user.to_s.strip.empty?
|
364
373
|
# authenticate against the realm
|
365
374
|
uri = URI.parse(target[:realm])
|
366
375
|
begin
|
367
376
|
response = RestClient::Request.execute(@http_options.merge(
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
377
|
+
method: :get,
|
378
|
+
url: uri.to_s, headers: { params: target[:params] },
|
379
|
+
user: @user,
|
380
|
+
password: @password
|
381
|
+
))
|
373
382
|
rescue RestClient::Unauthorized, RestClient::Forbidden
|
374
383
|
# bad authentication
|
375
384
|
raise DockerRegistry2::RegistryAuthenticationException
|
376
|
-
rescue RestClient::NotFound =>
|
377
|
-
raise DockerRegistry2::NotFound,
|
385
|
+
rescue RestClient::NotFound => e
|
386
|
+
raise DockerRegistry2::NotFound, e
|
378
387
|
end
|
379
388
|
# now save the web token
|
380
389
|
result = JSON.parse(response)
|
381
|
-
|
390
|
+
result['token'] || result['access_token']
|
382
391
|
end
|
383
392
|
|
384
393
|
def split_auth_header(header = '')
|
385
|
-
h =
|
386
|
-
h = {params: {}}
|
387
|
-
header.scan(/(
|
394
|
+
h = {}
|
395
|
+
h = { params: {} }
|
396
|
+
header.scan(/(\w+)="([^"]+)"/) do |entry|
|
388
397
|
case entry[0]
|
389
398
|
when 'realm'
|
390
399
|
h[:realm] = entry[1]
|
@@ -396,11 +405,19 @@ class DockerRegistry2::Registry
|
|
396
405
|
end
|
397
406
|
|
398
407
|
def headers(payload: nil, bearer_token: nil)
|
399
|
-
headers={}
|
400
|
-
headers['Authorization']="Bearer #{bearer_token}" unless bearer_token.nil?
|
401
|
-
|
402
|
-
|
408
|
+
headers = {}
|
409
|
+
headers['Authorization'] = "Bearer #{bearer_token}" unless bearer_token.nil?
|
410
|
+
if payload.nil?
|
411
|
+
headers['Accept'] =
|
412
|
+
%w[application/vnd.docker.distribution.manifest.v2+json
|
413
|
+
application/vnd.docker.distribution.manifest.list.v2+json
|
414
|
+
application/vnd.oci.image.manifest.v1+json
|
415
|
+
application/vnd.oci.image.index.v1+json
|
416
|
+
application/json].join(',')
|
417
|
+
end
|
418
|
+
headers['Content-Type'] = 'application/vnd.docker.distribution.manifest.v2+json' unless payload.nil?
|
403
419
|
|
404
420
|
headers
|
405
421
|
end
|
422
|
+
end
|
406
423
|
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.14.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:
|
14
|
+
date: 2023-03-31 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
|