docker-remote 0.5.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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.