docker-remote 0.5.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dff0f9b1ca90b5aee31b32c2a21804958e56c820d0dea006dc72e01ec9d7bde0
4
- data.tar.gz: 37430773f8c4cc38eab13ffe84c130ce1004248eb1eede2c9017125fda24fe52
3
+ metadata.gz: 9219a19199767fbceaf6728af153b87d3ce3fbc1d134ac84652064f52497abd4
4
+ data.tar.gz: 8206bcd50b87f57bf2f361623bff376937563a129df1ab6d542c4e1d4272d296
5
5
  SHA512:
6
- metadata.gz: ee3d605c43385c3ab6dd3da722813806abff376a762e1dc1a1d11192a1f9013dbbb63342ae70fe737770d63a065d33e3e1e4868617d78dd45dc401dd3026c57e
7
- data.tar.gz: 437bb0ab332ebf9c62ef3ed9ff7609271770a711dc1d5c7f7d34d15bb9bf2d75076679667ff2e8c850d8e3b654ecca00c2c77dbb950b5a10d730bdff71ef0164
6
+ metadata.gz: ccc0ae007d8a34b79bcea3cecb10cf4daa7d6881f37eab895b5785a40392463ef410b98828e3de48b4d520046dfcc5859419854a4ca1dba13912c318775fb476
7
+ data.tar.gz: 7d49f75c74d551b3e6ddcd458115983bc09cee8b63844ef9536e9c0f29a809c32476c9e81349695e7ecb45ad1cbbddede89b8284eb28da4138c0192ddddf2148
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.8.0
2
+ * Raise a more specific `TooManyRetriesError` when the client tries too many times with various scopes, etc.
3
+ - This can happen if the repo doesn't exist yet and the API keeps responding with an error message of `insufficient_scope`.
4
+
5
+ ## 0.7.0
6
+ * Support Azure Container Registry (ACR)
7
+ - ACR does two things differently than other registries like DockerHub, GitHub, etc:
8
+ 1. OAuth tokens are returned under the `access_token` key instead of `token`.
9
+ 1. The repo:<name>:pull scope is not enough to read metadata like the list of tags. You need the repo:<name>:metadata_read scope instead. Fortunately the www-authenticate header contains the scope you need to perform the operation.
10
+
11
+ ## 0.6.0
12
+ * Raise `UnknownRepoError` if the registry returns the `NAME_UNKNOWN` error code, which indicates the repo has never been pushed to before.
13
+
1
14
  ## 0.5.1
2
15
  * Just use given port if present, i.e. without checking it for connectivity.
3
16
 
@@ -0,0 +1,40 @@
1
+ module Docker
2
+ module Remote
3
+ class AuthInfo
4
+ class << self
5
+ def from_header(header, creds)
6
+ idx = header.index(' ')
7
+ auth_type = header[0..idx].strip.downcase
8
+
9
+ params = header[idx..-1].split(',').each_with_object({}) do |param, ret|
10
+ key, value = param.split('=')
11
+ ret[key.strip] = value.strip[1..-2] # remove quotes
12
+ end
13
+
14
+ new(auth_type, params, creds)
15
+ end
16
+ end
17
+
18
+
19
+ attr_reader :auth_type, :params, :creds
20
+
21
+ def initialize(auth_type, params, creds)
22
+ @auth_type = auth_type
23
+ @params = params
24
+ @creds = creds
25
+ end
26
+
27
+ def strategy
28
+ @strategy ||= case auth_type
29
+ when 'bearer'
30
+ BearerAuth.new(self, creds)
31
+ when 'basic'
32
+ BasicAuth.new(creds)
33
+ else
34
+ raise UnsupportedAuthTypeError,
35
+ "unsupported Docker auth type '#{auth_type}'"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,16 +3,15 @@ require 'net/http'
3
3
  module Docker
4
4
  module Remote
5
5
  class BasicAuth
6
- attr_reader :username, :password
6
+ attr_reader :creds
7
7
 
8
- def initialize(username, password)
9
- @username = username
10
- @password = password
8
+ def initialize(creds)
9
+ @creds = creds
11
10
  end
12
11
 
13
12
  def make_get(path)
14
13
  Net::HTTP::Get.new(path).tap do |request|
15
- request.basic_auth(username, password)
14
+ request.basic_auth(creds.username, creds.password)
16
15
  end
17
16
  end
18
17
  end
@@ -7,13 +7,11 @@ module Docker
7
7
  class BearerAuth
8
8
  include Utils
9
9
 
10
- attr_reader :params, :repo, :username, :password
10
+ attr_reader :auth_info, :creds
11
11
 
12
- def initialize(params, repo, username, password)
13
- @params = params
14
- @repo = repo
15
- @username = username
16
- @password = password
12
+ def initialize(auth_info, creds)
13
+ @auth_info = auth_info
14
+ @creds = creds
17
15
  end
18
16
 
19
17
  def make_get(path)
@@ -25,11 +23,11 @@ module Docker
25
23
  private
26
24
 
27
25
  def realm
28
- @realm ||= URI.parse(params['realm'])
26
+ @realm ||= URI.parse(auth_info.params['realm'])
29
27
  end
30
28
 
31
29
  def service
32
- @serivce ||= params['service']
30
+ @serivce ||= auth_info.params['service']
33
31
  end
34
32
 
35
33
  def token
@@ -37,17 +35,24 @@ module Docker
37
35
  http = Net::HTTP.new(realm.host, realm.port)
38
36
  http.use_ssl = true if realm.scheme == 'https'
39
37
 
38
+ url_params = { service: service }
39
+
40
+ if scope = auth_info.params['scope']
41
+ url_params[:scope] = scope
42
+ end
43
+
40
44
  request = Net::HTTP::Get.new(
41
- "#{realm.request_uri}?service=#{service}&scope=repository:#{repo}:pull"
45
+ "#{realm.request_uri}?#{URI.encode_www_form(url_params)}"
42
46
  )
43
47
 
44
- if username && password
45
- request.basic_auth(username, password)
48
+ if creds.username && creds.password
49
+ request.basic_auth(creds.username, creds.password)
46
50
  end
47
51
 
48
52
  response = http.request(request)
49
53
  potentially_raise_error!(response)
50
- JSON.parse(response.body)['token']
54
+ body_json = JSON.parse(response.body)
55
+ body_json['token'] || body_json['access_token']
51
56
  end
52
57
  end
53
58
  end
@@ -8,11 +8,12 @@ module Docker
8
8
  class DockerRemoteError < StandardError; end
9
9
  class UnsupportedVersionError < DockerRemoteError; end
10
10
  class UnexpectedResponseCodeError < DockerRemoteError; end
11
+ class TooManyRetriesError < DockerRemoteError; end
11
12
 
12
13
  class Client
13
14
  include Utils
14
15
 
15
- attr_reader :registry_url, :repo, :username, :password
16
+ attr_reader :registry_url, :repo, :creds
16
17
 
17
18
  PORTMAP = { 'ghcr.io' => 443 }.freeze
18
19
  DEFAULT_PORT = 443
@@ -21,8 +22,7 @@ module Docker
21
22
  def initialize(registry_url, repo, username = nil, password = nil)
22
23
  @registry_url = registry_url
23
24
  @repo = repo
24
- @username = username
25
- @password = password
25
+ @creds = Credentials.new(username, password)
26
26
  end
27
27
 
28
28
  def tags
@@ -47,14 +47,14 @@ module Docker
47
47
 
48
48
  def auth
49
49
  @auth ||= begin
50
- response = get('/v2/', use_auth: nil)
50
+ response = get('/v2/', use_auth: NoAuth.instance)
51
51
 
52
- case response.code
53
- when '200'
52
+ case response
53
+ when Net::HTTPSuccess
54
54
  NoAuth.instance
55
- when '401'
56
- www_auth(response)
57
- when '404'
55
+ when Net::HTTPUnauthorized
56
+ www_auth(response).strategy
57
+ when Net::HTTPNotFound
58
58
  raise UnsupportedVersionError,
59
59
  "the registry at #{registry_url} doesn't support v2 "\
60
60
  'of the Docker registry API'
@@ -67,50 +67,39 @@ module Docker
67
67
  end
68
68
 
69
69
  def www_auth(response)
70
- auth = response['www-authenticate']
71
-
72
- idx = auth.index(' ')
73
- auth_type = auth[0..idx].strip
74
-
75
- params = auth[idx..-1].split(',').each_with_object({}) do |param, ret|
76
- key, value = param.split('=')
77
- ret[key.strip] = value.strip[1..-2] # remove quotes
78
- end
79
-
80
- case auth_type.downcase
81
- when 'bearer'
82
- BearerAuth.new(params, repo, username, password)
83
- when 'basic'
84
- BasicAuth.new(username, password)
85
- else
86
- raise UnsupportedAuthTypeError,
87
- "unsupported Docker auth type '#{auth_type}'"
88
- end
70
+ AuthInfo.from_header(response['www-authenticate'], creds)
89
71
  end
90
72
 
91
73
  def get(path, http: registry_http, use_auth: auth, limit: 5)
92
74
  if limit == 0
93
- raise DockerRemoteError, 'too many redirects'
94
- end
95
-
96
- request = if use_auth
97
- use_auth.make_get(path)
98
- else
99
- Net::HTTP::Get.new(path)
75
+ raise TooManyRetriesError, "too many retries contacting #{registry_uri.host}"
100
76
  end
101
77
 
78
+ request = use_auth.make_get(path)
102
79
  response = http.request(request)
103
80
 
104
81
  case response
82
+ when Net::HTTPUnauthorized
83
+ auth_info = www_auth(response)
84
+
85
+ if auth_info.params['error'] == 'insufficient_scope'
86
+ if auth_info.params.include?('scope')
87
+ return get(
88
+ path,
89
+ http: http,
90
+ use_auth: auth_info.strategy,
91
+ limit: limit - 1
92
+ )
93
+ end
94
+ end
105
95
  when Net::HTTPRedirection
106
96
  redirect_uri = URI.parse(response['location'])
107
97
  redirect_http = make_http(redirect_uri)
108
98
  return get(
109
- redirect_uri.path, {
110
- http: redirect_http,
111
- use_auth: use_auth,
112
- limit: limit - 1
113
- }
99
+ redirect_uri.path,
100
+ http: redirect_http,
101
+ use_auth: use_auth,
102
+ limit: limit - 1
114
103
  )
115
104
  end
116
105
 
@@ -0,0 +1,12 @@
1
+ module Docker
2
+ module Remote
3
+ class Credentials
4
+ attr_reader :username, :password
5
+
6
+ def initialize(username, password)
7
+ @username = username
8
+ @password = password
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module Docker
2
4
  module Remote
3
5
  module Utils
@@ -6,6 +8,13 @@ module Docker
6
8
  when 401
7
9
  raise UnauthorizedError, "401 Unauthorized: #{response.message}"
8
10
  when 404
11
+ json = JSON.parse(response.body) rescue {}
12
+ error = (json['errors'] || []).first || {}
13
+
14
+ if error['code'] == 'NAME_UNKNOWN'
15
+ raise UnknownRepoError, error['message']
16
+ end
17
+
9
18
  raise NotFoundError, "404 Not Found: #{response.message}"
10
19
  end
11
20
 
@@ -1,5 +1,5 @@
1
1
  module Docker
2
2
  module Remote
3
- VERSION = '0.5.1'
3
+ VERSION = '0.8.0'
4
4
  end
5
5
  end
data/lib/docker/remote.rb CHANGED
@@ -4,13 +4,16 @@ module Docker
4
4
  class ServerError < StandardError; end
5
5
  class UnauthorizedError < ClientError; end
6
6
  class NotFoundError < ClientError; end
7
+ class UnknownRepoError < ClientError; end
7
8
 
8
9
  class UnsupportedAuthTypeError < StandardError; end
9
10
 
10
- autoload :BasicAuth, 'docker/remote/basic_auth'
11
- autoload :BearerAuth, 'docker/remote/bearer_auth'
12
- autoload :Client, 'docker/remote/client'
13
- autoload :NoAuth, 'docker/remote/no_auth'
14
- autoload :Utils, 'docker/remote/utils'
11
+ autoload :AuthInfo, 'docker/remote/auth_info'
12
+ autoload :BasicAuth, 'docker/remote/basic_auth'
13
+ autoload :BearerAuth, 'docker/remote/bearer_auth'
14
+ autoload :Client, 'docker/remote/client'
15
+ autoload :Credentials, 'docker/remote/credentials'
16
+ autoload :NoAuth, 'docker/remote/no_auth'
17
+ autoload :Utils, 'docker/remote/utils'
15
18
  end
16
19
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docker-remote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Dutro
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-12 00:00:00.000000000 Z
11
+ date: 2022-02-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Ruby client for communicating with the Docker registry API v2.
14
14
  email:
@@ -23,9 +23,11 @@ files:
23
23
  - Rakefile
24
24
  - docker-remote.gemspec
25
25
  - lib/docker/remote.rb
26
+ - lib/docker/remote/auth_info.rb
26
27
  - lib/docker/remote/basic_auth.rb
27
28
  - lib/docker/remote/bearer_auth.rb
28
29
  - lib/docker/remote/client.rb
30
+ - lib/docker/remote/credentials.rb
29
31
  - lib/docker/remote/no_auth.rb
30
32
  - lib/docker/remote/utils.rb
31
33
  - lib/docker/remote/version.rb
@@ -47,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
49
  - !ruby/object:Gem::Version
48
50
  version: '0'
49
51
  requirements: []
50
- rubygems_version: 3.1.4
52
+ rubygems_version: 3.2.22
51
53
  signing_key:
52
54
  specification_version: 4
53
55
  summary: A Ruby client for communicating with the Docker registry API v2.