docker-remote 0.4.0 → 0.7.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: 6cfe49856e12add82e0b4fa76a42381640b46ab0810a08db03cbd4e6619fa8d5
4
- data.tar.gz: d5d7ab9eb55c7d2189ab5127e5b9c03a5e3c2c53a16241e540907cce3158fc72
3
+ metadata.gz: 7aafe47e42bed5d3cd16a135a0a3a6f771aaceb9c35b796bf0706ec09137814d
4
+ data.tar.gz: 1c5aa9f0e89a229cbe203f5d535154fcb85fe270df340862200f3a14df5e0789
5
5
  SHA512:
6
- metadata.gz: 046f4aa7357778f0ded105562fd8ec55e25dc4d012e1c9fcfd01ee4bda47670b26cb83a9fc1bb5f3b4ed625a248af8ea541effceb93e284984c1219baabe634e
7
- data.tar.gz: 38cd75b59a2a12e2339fff98a492f97851d28068184dc7b354ef080f70a7e65baf09a5ffe6fc0495d64158e74d58e4826ceec90da75a2ca28535cbbb6d2a8ffd
6
+ metadata.gz: d2a2c898b5150f0a69e6c42a8c097f5956044201749167a3648a89725f0ae62e2e5e03e9432133f68637325406842c3d529fa3aaed76ac07e19c51d67b569920
7
+ data.tar.gz: 1306ccd01efe9184963586f0b34bbe0d9b10ea262b9570538b8d234d4024c0e862dfc001cffa1c144b67699e9a231155940ea18fc36ebccf59ae444ad8777ab1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.7.0
2
+ * Support Azure Container Registry (ACR)
3
+ - ACR does two things differently than other registries like DockerHub, GitHub, etc:
4
+ 1. OAuth tokens are returned under the `access_token` key instead of `token`.
5
+ 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.
6
+
7
+ ## 0.6.0
8
+ * Raise `UnknownRepoError` if the registry returns the `NAME_UNKNOWN` error code, which indicates the repo has never been pushed to before.
9
+
10
+ ## 0.5.1
11
+ * Just use given port if present, i.e. without checking it for connectivity.
12
+
13
+ ## 0.5.0
14
+ * Figure out registry port more accurately.
15
+
1
16
  ## 0.4.0
2
17
  * Support redirection when making HTTP requests.
3
18
 
@@ -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
@@ -1,5 +1,6 @@
1
1
  require 'json'
2
2
  require 'net/http'
3
+ require 'socket'
3
4
  require 'uri'
4
5
 
5
6
  module Docker
@@ -11,13 +12,16 @@ module Docker
11
12
  class Client
12
13
  include Utils
13
14
 
14
- attr_reader :registry_url, :repo, :username, :password
15
+ attr_reader :registry_url, :repo, :creds
16
+
17
+ PORTMAP = { 'ghcr.io' => 443 }.freeze
18
+ DEFAULT_PORT = 443
19
+ STANDARD_PORTS = [DEFAULT_PORT, 80].freeze
15
20
 
16
21
  def initialize(registry_url, repo, username = nil, password = nil)
17
22
  @registry_url = registry_url
18
23
  @repo = repo
19
- @username = username
20
- @password = password
24
+ @creds = Credentials.new(username, password)
21
25
  end
22
26
 
23
27
  def tags
@@ -42,14 +46,14 @@ module Docker
42
46
 
43
47
  def auth
44
48
  @auth ||= begin
45
- response = get('/v2/', use_auth: nil)
49
+ response = get('/v2/', use_auth: NoAuth.instance)
46
50
 
47
- case response.code
48
- when '200'
51
+ case response
52
+ when Net::HTTPSuccess
49
53
  NoAuth.instance
50
- when '401'
51
- www_auth(response)
52
- when '404'
54
+ when Net::HTTPUnauthorized
55
+ www_auth(response).strategy
56
+ when Net::HTTPNotFound
53
57
  raise UnsupportedVersionError,
54
58
  "the registry at #{registry_url} doesn't support v2 "\
55
59
  'of the Docker registry API'
@@ -62,25 +66,7 @@ module Docker
62
66
  end
63
67
 
64
68
  def www_auth(response)
65
- auth = response['www-authenticate']
66
-
67
- idx = auth.index(' ')
68
- auth_type = auth[0..idx].strip
69
-
70
- params = auth[idx..-1].split(',').each_with_object({}) do |param, ret|
71
- key, value = param.split('=')
72
- ret[key.strip] = value.strip[1..-2] # remove quotes
73
- end
74
-
75
- case auth_type.downcase
76
- when 'bearer'
77
- BearerAuth.new(params, repo, username, password)
78
- when 'basic'
79
- BasicAuth.new(username, password)
80
- else
81
- raise UnsupportedAuthTypeError,
82
- "unsupported Docker auth type '#{auth_type}'"
83
- end
69
+ AuthInfo.from_header(response['www-authenticate'], creds)
84
70
  end
85
71
 
86
72
  def get(path, http: registry_http, use_auth: auth, limit: 5)
@@ -88,24 +74,31 @@ module Docker
88
74
  raise DockerRemoteError, 'too many redirects'
89
75
  end
90
76
 
91
- request = if use_auth
92
- use_auth.make_get(path)
93
- else
94
- Net::HTTP::Get.new(path)
95
- end
96
-
77
+ request = use_auth.make_get(path)
97
78
  response = http.request(request)
98
79
 
99
80
  case response
81
+ when Net::HTTPUnauthorized
82
+ auth_info = www_auth(response)
83
+
84
+ if auth_info.params['error'] == 'insufficient_scope'
85
+ if auth_info.params.include?('scope')
86
+ return get(
87
+ path,
88
+ http: http,
89
+ use_auth: auth_info.strategy,
90
+ limit: limit - 1
91
+ )
92
+ end
93
+ end
100
94
  when Net::HTTPRedirection
101
95
  redirect_uri = URI.parse(response['location'])
102
96
  redirect_http = make_http(redirect_uri)
103
97
  return get(
104
- redirect_uri.path, {
105
- http: redirect_http,
106
- use_auth: use_auth,
107
- limit: limit - 1
108
- }
98
+ redirect_uri.path,
99
+ http: redirect_http,
100
+ use_auth: use_auth,
101
+ limit: limit - 1
109
102
  )
110
103
  end
111
104
 
@@ -113,7 +106,28 @@ module Docker
113
106
  end
114
107
 
115
108
  def registry_uri
116
- @registry_uri ||= URI.parse(registry_url)
109
+ @registry_uri ||= begin
110
+ host_port, *rest = registry_url.split('/')
111
+ host, orig_port = host_port.split(':')
112
+
113
+ port = if orig_port
114
+ orig_port.to_i
115
+ elsif prt = PORTMAP[host]
116
+ prt
117
+ else
118
+ STANDARD_PORTS.find do |prt|
119
+ can_connect?(host, prt)
120
+ end
121
+ end
122
+
123
+ unless port
124
+ raise DockerRemoteError,
125
+ "couldn't determine what port to connect to for '#{registry_url}'"
126
+ end
127
+
128
+ scheme = port == DEFAULT_PORT ? 'https' : 'http'
129
+ URI.parse("#{scheme}://#{host}:#{port}/#{rest.join('/')}")
130
+ end
117
131
  end
118
132
 
119
133
  def make_http(uri)
@@ -125,6 +139,49 @@ module Docker
125
139
  def registry_http
126
140
  @registry_http ||= make_http(registry_uri)
127
141
  end
142
+
143
+ # Adapted from: https://spin.atomicobject.com/2013/09/30/socket-connection-timeout-ruby/
144
+ def can_connect?(host, port)
145
+ # Convert the passed host into structures the non-blocking calls
146
+ # can deal with
147
+ addr = Socket.getaddrinfo(host, nil)
148
+ sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
149
+ timeout = 3
150
+
151
+ Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0).tap do |socket|
152
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
153
+
154
+ begin
155
+ # Initiate the socket connection in the background. If it doesn't fail
156
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
157
+ # indicating the connection is in progress.
158
+ socket.connect_nonblock(sockaddr)
159
+
160
+ rescue IO::WaitWritable
161
+ # IO.select will block until the socket is writable or the timeout
162
+ # is exceeded - whichever comes first.
163
+ if IO.select(nil, [socket], nil, timeout)
164
+ begin
165
+ # Verify there is now a good connection
166
+ socket.connect_nonblock(sockaddr)
167
+ rescue Errno::EISCONN
168
+ # Good news everybody, the socket is connected!
169
+ socket.close
170
+ return true
171
+ rescue
172
+ # An unexpected exception was raised - the connection is no good.
173
+ socket.close
174
+ end
175
+ else
176
+ # IO.select returns nil when the socket is not ready before timeout
177
+ # seconds have elapsed
178
+ socket.close
179
+ end
180
+ end
181
+ end
182
+
183
+ false
184
+ end
128
185
  end
129
186
  end
130
187
  end
@@ -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.4.0'
3
+ VERSION = '0.7.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.4.0
4
+ version: 0.7.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-02 00:00:00.000000000 Z
11
+ date: 2022-02-11 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.