docker-remote 0.2.0 → 0.6.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: 19acf54d628fdf18296c7a86ffdd6760840f7222fa28e5438e5b0edc7bac7136
4
- data.tar.gz: 1ce08796037731c708c9f5ce79e8038d31d288654f0ea7070166a2d6ac4e711c
3
+ metadata.gz: 67104ac59d9b809cfaafaac9c1711b4d68b4d514c4c0360a28cb42cea105a72b
4
+ data.tar.gz: 59d0560b904d4fbf9bd4e7484caf179cf0355f3b716eaaf104c87a3700be00f6
5
5
  SHA512:
6
- metadata.gz: 273f430350900735e9efb3a63155d01d32d14afcc7ab5b34b94250e2de0d12c2cd5301f88d08c0729513848f06a559b918d7a762a0b1803854c6f8069e9263f3
7
- data.tar.gz: 60684d86a0675c3465455b3df6373144db0894a908fd5c0491c5b822a7c5544d00dd50121a3d57d666ea4b5a4b0a4a34b674c29bd6e25e690c142217a63c60d0
6
+ metadata.gz: ee73da94c49c3c70779ad258d9b0e0147c2768998762bcaa5829756ba4203163245fcb00031bdd7d9abeaee5d680bc5077788fa5826deb03e7e99f9a7eb01591
7
+ data.tar.gz: 1cd3bac68947f1bf7af75ca903de81b932ae2258842ae22a71eee444c621ea63975b243cbc63a1bd273a848d756a3427409548375454580372f6f7c6d00b0626
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 0.6.0
2
+ * Raise `UnknownRepoError` if the registry returns the `NAME_UNKNOWN` error code, which indicates the repo has never been pushed to before.
3
+
4
+ ## 0.5.1
5
+ * Just use given port if present, i.e. without checking it for connectivity.
6
+
7
+ ## 0.5.0
8
+ * Figure out registry port more accurately.
9
+
10
+ ## 0.4.0
11
+ * Support redirection when making HTTP requests.
12
+
13
+ ## 0.3.0
14
+ * Support registries with no auth.
15
+ * Raise errors upon receiving unexpected response codes during auth flow.
16
+
1
17
  ## 0.2.0
2
18
  * Support both basic and bearer auth.
3
19
 
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ gemspec
4
4
 
5
5
  group :development do
6
6
  gem 'rake'
7
+ gem 'pry-byebug'
7
8
  end
8
9
 
9
10
  group :test do
data/lib/docker/remote.rb CHANGED
@@ -4,12 +4,14 @@ 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
11
  autoload :BasicAuth, 'docker/remote/basic_auth'
11
12
  autoload :BearerAuth, 'docker/remote/bearer_auth'
12
13
  autoload :Client, 'docker/remote/client'
14
+ autoload :NoAuth, 'docker/remote/no_auth'
13
15
  autoload :Utils, 'docker/remote/utils'
14
16
  end
15
17
  end
@@ -1,14 +1,23 @@
1
1
  require 'json'
2
2
  require 'net/http'
3
+ require 'socket'
3
4
  require 'uri'
4
5
 
5
6
  module Docker
6
7
  module Remote
8
+ class DockerRemoteError < StandardError; end
9
+ class UnsupportedVersionError < DockerRemoteError; end
10
+ class UnexpectedResponseCodeError < DockerRemoteError; end
11
+
7
12
  class Client
8
13
  include Utils
9
14
 
10
15
  attr_reader :registry_url, :repo, :username, :password
11
16
 
17
+ PORTMAP = { 'ghcr.io' => 443 }.freeze
18
+ DEFAULT_PORT = 443
19
+ STANDARD_PORTS = [DEFAULT_PORT, 80].freeze
20
+
12
21
  def initialize(registry_url, repo, username = nil, password = nil)
13
22
  @registry_url = registry_url
14
23
  @repo = repo
@@ -17,22 +26,19 @@ module Docker
17
26
  end
18
27
 
19
28
  def tags
20
- request = auth.make_get("/v2/#{repo}/tags/list")
21
- response = registry_http.request(request)
29
+ response = get("/v2/#{repo}/tags/list")
22
30
  potentially_raise_error!(response)
23
31
  JSON.parse(response.body)['tags']
24
32
  end
25
33
 
26
34
  def manifest_for(reference)
27
- request = auth.make_get("/v2/#{repo}/manifests/#{reference}")
28
- response = registry_http.request(request)
35
+ response = get("/v2/#{repo}/manifests/#{reference}")
29
36
  potentially_raise_error!(response)
30
37
  JSON.parse(response.body)
31
38
  end
32
39
 
33
40
  def catalog
34
- request = auth.make_get("/v2/_catalog")
35
- response = registry_http.request(request)
41
+ response = get("/v2/_catalog")
36
42
  potentially_raise_error!(response)
37
43
  JSON.parse(response.body)
38
44
  end
@@ -41,37 +47,152 @@ module Docker
41
47
 
42
48
  def auth
43
49
  @auth ||= begin
44
- request = Net::HTTP::Get.new('/v2/')
45
- response = registry_http.request(request)
46
- auth = response['www-authenticate']
50
+ response = get('/v2/', use_auth: nil)
51
+
52
+ case response.code
53
+ when '200'
54
+ NoAuth.instance
55
+ when '401'
56
+ www_auth(response)
57
+ when '404'
58
+ raise UnsupportedVersionError,
59
+ "the registry at #{registry_url} doesn't support v2 "\
60
+ 'of the Docker registry API'
61
+ else
62
+ raise UnexpectedResponseCodeError,
63
+ "the registry at #{registry_url} responded with an "\
64
+ "unexpected HTTP status code of #{response.code}"
65
+ end
66
+ end
67
+ end
68
+
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
89
+ end
90
+
91
+ def get(path, http: registry_http, use_auth: auth, limit: 5)
92
+ 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)
100
+ end
47
101
 
48
- idx = auth.index(' ')
49
- auth_type = auth[0..idx].strip
102
+ response = http.request(request)
50
103
 
51
- params = auth[idx..-1].split(',').each_with_object({}) do |param, ret|
52
- key, value = param.split('=')
53
- ret[key.strip] = value.strip[1..-2] # remove quotes
104
+ case response
105
+ when Net::HTTPRedirection
106
+ redirect_uri = URI.parse(response['location'])
107
+ redirect_http = make_http(redirect_uri)
108
+ return get(
109
+ redirect_uri.path, {
110
+ http: redirect_http,
111
+ use_auth: use_auth,
112
+ limit: limit - 1
113
+ }
114
+ )
115
+ end
116
+
117
+ response
118
+ end
119
+
120
+ def registry_uri
121
+ @registry_uri ||= begin
122
+ host_port, *rest = registry_url.split('/')
123
+ host, orig_port = host_port.split(':')
124
+
125
+ port = if orig_port
126
+ orig_port.to_i
127
+ elsif prt = PORTMAP[host]
128
+ prt
129
+ else
130
+ STANDARD_PORTS.find do |prt|
131
+ can_connect?(host, prt)
132
+ end
54
133
  end
55
134
 
56
- case auth_type.downcase
57
- when 'bearer'
58
- BearerAuth.new(params, repo, username, password)
59
- when 'basic'
60
- BasicAuth.new(username, password)
61
- else
62
- raise UnsupportedAuthTypeError, "unsupported Docker auth type '#{auth_type}'"
135
+ unless port
136
+ raise DockerRemoteError,
137
+ "couldn't determine what port to connect to for '#{registry_url}'"
63
138
  end
139
+
140
+ scheme = port == DEFAULT_PORT ? 'https' : 'http'
141
+ URI.parse("#{scheme}://#{host}:#{port}/#{rest.join('/')}")
64
142
  end
65
143
  end
66
144
 
67
- def registry_uri
68
- @registry_uri ||= URI.parse(registry_url)
145
+ def make_http(uri)
146
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
147
+ http.use_ssl = true if uri.scheme == 'https'
148
+ end
69
149
  end
70
150
 
71
151
  def registry_http
72
- @registry_http ||= Net::HTTP.new(registry_uri.host, registry_uri.port).tap do |http|
73
- http.use_ssl = true if registry_uri.scheme == 'https'
152
+ @registry_http ||= make_http(registry_uri)
153
+ end
154
+
155
+ # Adapted from: https://spin.atomicobject.com/2013/09/30/socket-connection-timeout-ruby/
156
+ def can_connect?(host, port)
157
+ # Convert the passed host into structures the non-blocking calls
158
+ # can deal with
159
+ addr = Socket.getaddrinfo(host, nil)
160
+ sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
161
+ timeout = 3
162
+
163
+ Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0).tap do |socket|
164
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
165
+
166
+ begin
167
+ # Initiate the socket connection in the background. If it doesn't fail
168
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
169
+ # indicating the connection is in progress.
170
+ socket.connect_nonblock(sockaddr)
171
+
172
+ rescue IO::WaitWritable
173
+ # IO.select will block until the socket is writable or the timeout
174
+ # is exceeded - whichever comes first.
175
+ if IO.select(nil, [socket], nil, timeout)
176
+ begin
177
+ # Verify there is now a good connection
178
+ socket.connect_nonblock(sockaddr)
179
+ rescue Errno::EISCONN
180
+ # Good news everybody, the socket is connected!
181
+ socket.close
182
+ return true
183
+ rescue
184
+ # An unexpected exception was raised - the connection is no good.
185
+ socket.close
186
+ end
187
+ else
188
+ # IO.select returns nil when the socket is not ready before timeout
189
+ # seconds have elapsed
190
+ socket.close
191
+ end
192
+ end
74
193
  end
194
+
195
+ false
75
196
  end
76
197
  end
77
198
  end
@@ -0,0 +1,11 @@
1
+ module Docker
2
+ module Remote
3
+ class NoAuth
4
+ include Singleton
5
+
6
+ def make_get(path)
7
+ Net::HTTP::Get.new(path)
8
+ end
9
+ end
10
+ end
11
+ 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.2.0'
3
+ VERSION = '0.6.0'
4
4
  end
5
5
  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.2.0
4
+ version: 0.6.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-10-26 00:00:00.000000000 Z
11
+ date: 2021-06-06 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:
@@ -26,6 +26,7 @@ files:
26
26
  - lib/docker/remote/basic_auth.rb
27
27
  - lib/docker/remote/bearer_auth.rb
28
28
  - lib/docker/remote/client.rb
29
+ - lib/docker/remote/no_auth.rb
29
30
  - lib/docker/remote/utils.rb
30
31
  - lib/docker/remote/version.rb
31
32
  homepage: http://github.com/getkuby/docker-remote