docker-remote 0.4.0 → 0.7.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: 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.