docker-remote 0.1.0 → 0.5.1

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: f8dcfda8b4cbf58474274964a653f5049f609333bf88d1b9497205a4bb366e6e
4
- data.tar.gz: 9ec104d6e95619a579b0e8e4049d00d6e1eb68c425f7490a83a12477ca812e21
3
+ metadata.gz: dff0f9b1ca90b5aee31b32c2a21804958e56c820d0dea006dc72e01ec9d7bde0
4
+ data.tar.gz: 37430773f8c4cc38eab13ffe84c130ce1004248eb1eede2c9017125fda24fe52
5
5
  SHA512:
6
- metadata.gz: 96faa086fc0223dc3377c77e57042b806f446923156c46bf6627438d64104c06bcadcda9674901a830f209ab68abb09a79335893df8e9428472d292e62dc8c83
7
- data.tar.gz: '038eb80f159cc9c1c0a3740923c55a4c351c156b10a1e641aaaf4f3d7ceab790fe03d1ca0001f5b2b150978acf8f3e709654764b1e9ab9e950874d2837b54c59'
6
+ metadata.gz: ee3d605c43385c3ab6dd3da722813806abff376a762e1dc1a1d11192a1f9013dbbb63342ae70fe737770d63a065d33e3e1e4868617d78dd45dc401dd3026c57e
7
+ data.tar.gz: 437bb0ab332ebf9c62ef3ed9ff7609271770a711dc1d5c7f7d34d15bb9bf2d75076679667ff2e8c850d8e3b654ecca00c2c77dbb950b5a10d730bdff71ef0164
@@ -1,2 +1,18 @@
1
+ ## 0.5.1
2
+ * Just use given port if present, i.e. without checking it for connectivity.
3
+
4
+ ## 0.5.0
5
+ * Figure out registry port more accurately.
6
+
7
+ ## 0.4.0
8
+ * Support redirection when making HTTP requests.
9
+
10
+ ## 0.3.0
11
+ * Support registries with no auth.
12
+ * Raise errors upon receiving unexpected response codes during auth flow.
13
+
14
+ ## 0.2.0
15
+ * Support both basic and bearer auth.
16
+
1
17
  ## 0.1.0
2
18
  * Birthday!
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Cameron Dutro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -8,10 +8,10 @@ Gem::Specification.new do |s|
8
8
  s.email = ['camertron@gmail.com']
9
9
  s.homepage = 'http://github.com/getkuby/docker-remote'
10
10
 
11
- s.description = s.summary = 'A Ruby client for communicating with the Docker HTTP API v2.'
11
+ s.description = s.summary = 'A Ruby client for communicating with the Docker registry API v2.'
12
12
 
13
13
  s.platform = Gem::Platform::RUBY
14
14
 
15
15
  s.require_path = 'lib'
16
- s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'CHANGELOG.md', 'README.md', 'Rakefile', 'docker-remote.gemspec']
16
+ s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'CHANGELOG.md', 'LICENSE', 'README.md', 'Rakefile', 'docker-remote.gemspec']
17
17
  end
@@ -1,9 +1,16 @@
1
- require 'net/http'
2
- require 'uri'
3
-
4
1
  module Docker
5
2
  module Remote
3
+ class ClientError < StandardError; end
4
+ class ServerError < StandardError; end
5
+ class UnauthorizedError < ClientError; end
6
+ class NotFoundError < ClientError; end
7
+
8
+ class UnsupportedAuthTypeError < StandardError; end
9
+
10
+ autoload :BasicAuth, 'docker/remote/basic_auth'
11
+ autoload :BearerAuth, 'docker/remote/bearer_auth'
6
12
  autoload :Client, 'docker/remote/client'
7
- autoload :ServerAuth, 'docker/remote/server_auth'
13
+ autoload :NoAuth, 'docker/remote/no_auth'
14
+ autoload :Utils, 'docker/remote/utils'
8
15
  end
9
16
  end
@@ -0,0 +1,20 @@
1
+ require 'net/http'
2
+
3
+ module Docker
4
+ module Remote
5
+ class BasicAuth
6
+ attr_reader :username, :password
7
+
8
+ def initialize(username, password)
9
+ @username = username
10
+ @password = password
11
+ end
12
+
13
+ def make_get(path)
14
+ Net::HTTP::Get.new(path).tap do |request|
15
+ request.basic_auth(username, password)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Docker
6
+ module Remote
7
+ class BearerAuth
8
+ include Utils
9
+
10
+ attr_reader :params, :repo, :username, :password
11
+
12
+ def initialize(params, repo, username, password)
13
+ @params = params
14
+ @repo = repo
15
+ @username = username
16
+ @password = password
17
+ end
18
+
19
+ def make_get(path)
20
+ Net::HTTP::Get.new(path).tap do |request|
21
+ request['Authorization'] = "Bearer #{token}"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def realm
28
+ @realm ||= URI.parse(params['realm'])
29
+ end
30
+
31
+ def service
32
+ @serivce ||= params['service']
33
+ end
34
+
35
+ def token
36
+ @token ||= begin
37
+ http = Net::HTTP.new(realm.host, realm.port)
38
+ http.use_ssl = true if realm.scheme == 'https'
39
+
40
+ request = Net::HTTP::Get.new(
41
+ "#{realm.request_uri}?service=#{service}&scope=repository:#{repo}:pull"
42
+ )
43
+
44
+ if username && password
45
+ request.basic_auth(username, password)
46
+ end
47
+
48
+ response = http.request(request)
49
+ potentially_raise_error!(response)
50
+ JSON.parse(response.body)['token']
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,15 +1,23 @@
1
1
  require 'json'
2
+ require 'net/http'
3
+ require 'socket'
4
+ require 'uri'
2
5
 
3
6
  module Docker
4
7
  module Remote
5
- class ClientError < StandardError; end
6
- class ServerError < StandardError; end
7
- class UnauthorizedError < ClientError; end
8
- class NotFoundError < ClientError; end
8
+ class DockerRemoteError < StandardError; end
9
+ class UnsupportedVersionError < DockerRemoteError; end
10
+ class UnexpectedResponseCodeError < DockerRemoteError; end
9
11
 
10
12
  class Client
13
+ include Utils
14
+
11
15
  attr_reader :registry_url, :repo, :username, :password
12
16
 
17
+ PORTMAP = { 'ghcr.io' => 443 }.freeze
18
+ DEFAULT_PORT = 443
19
+ STANDARD_PORTS = [DEFAULT_PORT, 80].freeze
20
+
13
21
  def initialize(registry_url, repo, username = nil, password = nil)
14
22
  @registry_url = registry_url
15
23
  @repo = repo
@@ -18,96 +26,173 @@ module Docker
18
26
  end
19
27
 
20
28
  def tags
21
- request = make_get("/v2/#{repo}/tags/list")
22
- response = registry_http.request(request)
29
+ response = get("/v2/#{repo}/tags/list")
23
30
  potentially_raise_error!(response)
24
31
  JSON.parse(response.body)['tags']
25
32
  end
26
33
 
27
34
  def manifest_for(reference)
28
- request = make_get("/v2/#{repo}/manifests/#{reference}")
29
- response = registry_http.request(request)
35
+ response = get("/v2/#{repo}/manifests/#{reference}")
30
36
  potentially_raise_error!(response)
31
37
  JSON.parse(response.body)
32
38
  end
33
39
 
34
40
  def catalog
35
- request = make_get("/v2/_catalog")
36
- response = registry_http.request(request)
41
+ response = get("/v2/_catalog")
37
42
  potentially_raise_error!(response)
38
43
  JSON.parse(response.body)
39
44
  end
40
45
 
41
46
  private
42
47
 
43
- def token
44
- @token ||= begin
45
- uri = URI.parse(server_auth.realm)
46
- http = Net::HTTP.new(uri.host, uri.port)
47
- http.use_ssl = true if uri.scheme == 'https'
48
+ def auth
49
+ @auth ||= begin
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
48
68
 
49
- request = Net::HTTP::Get.new(
50
- "#{uri.request_uri}?service=#{server_auth.service}&scope=repository:#{repo}:pull"
51
- )
69
+ def www_auth(response)
70
+ auth = response['www-authenticate']
52
71
 
53
- if username && password
54
- request.basic_auth(username, password)
55
- end
72
+ idx = auth.index(' ')
73
+ auth_type = auth[0..idx].strip
56
74
 
57
- response = http.request(request)
58
- potentially_raise_error!(response)
59
- JSON.parse(response.body)['token']
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
60
78
  end
61
- end
62
79
 
63
- def server_auth
64
- @server_auth ||= begin
65
- request = Net::HTTP::Get.new('/v2/')
66
- response = registry_http.request(request)
67
- auth = response['www-authenticate']
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
68
90
 
69
- idx = auth.index(' ')
70
- auth_type = auth[0..idx].strip
91
+ def get(path, http: registry_http, use_auth: auth, limit: 5)
92
+ if limit == 0
93
+ raise DockerRemoteError, 'too many redirects'
94
+ end
71
95
 
72
- params = auth[idx..-1].split(',').each_with_object({}) do |param, ret|
73
- key, value = param.split('=')
74
- ret[key.strip] = value.strip[1..-2] # remove quotes
75
- end
96
+ request = if use_auth
97
+ use_auth.make_get(path)
98
+ else
99
+ Net::HTTP::Get.new(path)
100
+ end
76
101
 
77
- ServerAuth.new(auth_type, params)
102
+ response = http.request(request)
103
+
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
+ )
78
115
  end
116
+
117
+ response
79
118
  end
80
119
 
81
120
  def registry_uri
82
- @registry_uri ||= URI.parse(registry_url)
83
- end
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
133
+ end
84
134
 
85
- def registry_http
86
- @registry_http ||= Net::HTTP.new(registry_uri.host, registry_uri.port).tap do |http|
87
- http.use_ssl = true if registry_uri.scheme == 'https'
135
+ unless port
136
+ raise DockerRemoteError,
137
+ "couldn't determine what port to connect to for '#{registry_url}'"
138
+ end
139
+
140
+ scheme = port == DEFAULT_PORT ? 'https' : 'http'
141
+ URI.parse("#{scheme}://#{host}:#{port}/#{rest.join('/')}")
88
142
  end
89
143
  end
90
144
 
91
- def make_get(path)
92
- Net::HTTP::Get.new(path).tap do |request|
93
- request['Authorization'] = "Bearer #{token}"
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'
94
148
  end
95
149
  end
96
150
 
97
- def potentially_raise_error!(response)
98
- case response.code.to_i
99
- when 401
100
- raise UnauthorizedError, "401 Unauthorized: #{response.message}"
101
- when 404
102
- raise NotFoundError, "404 Not Found: #{response.message}"
103
- end
151
+ def registry_http
152
+ @registry_http ||= make_http(registry_uri)
153
+ end
104
154
 
105
- case response.code.to_i / 100
106
- when 4
107
- raise ClientError, "#{response.code}: #{response.message}"
108
- when 5
109
- raise ServerError, "#{response.code}: #{response.message}"
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
110
193
  end
194
+
195
+ false
111
196
  end
112
197
  end
113
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
@@ -0,0 +1,21 @@
1
+ module Docker
2
+ module Remote
3
+ module Utils
4
+ def potentially_raise_error!(response)
5
+ case response.code.to_i
6
+ when 401
7
+ raise UnauthorizedError, "401 Unauthorized: #{response.message}"
8
+ when 404
9
+ raise NotFoundError, "404 Not Found: #{response.message}"
10
+ end
11
+
12
+ case response.code.to_i / 100
13
+ when 4
14
+ raise ClientError, "#{response.code}: #{response.message}"
15
+ when 5
16
+ raise ServerError, "#{response.code}: #{response.message}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  module Docker
2
2
  module Remote
3
- VERSION = '0.1.0'
3
+ VERSION = '0.5.1'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docker-remote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Dutro
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-21 00:00:00.000000000 Z
11
+ date: 2020-11-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: A Ruby client for communicating with the Docker HTTP API v2.
13
+ description: A Ruby client for communicating with the Docker registry API v2.
14
14
  email:
15
15
  - camertron@gmail.com
16
16
  executables: []
@@ -19,16 +19,20 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - CHANGELOG.md
21
21
  - Gemfile
22
+ - LICENSE
22
23
  - Rakefile
23
24
  - docker-remote.gemspec
24
25
  - lib/docker/remote.rb
26
+ - lib/docker/remote/basic_auth.rb
27
+ - lib/docker/remote/bearer_auth.rb
25
28
  - lib/docker/remote/client.rb
26
- - lib/docker/remote/server_auth.rb
29
+ - lib/docker/remote/no_auth.rb
30
+ - lib/docker/remote/utils.rb
27
31
  - lib/docker/remote/version.rb
28
32
  homepage: http://github.com/getkuby/docker-remote
29
33
  licenses: []
30
34
  metadata: {}
31
- post_install_message:
35
+ post_install_message:
32
36
  rdoc_options: []
33
37
  require_paths:
34
38
  - lib
@@ -43,8 +47,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
47
  - !ruby/object:Gem::Version
44
48
  version: '0'
45
49
  requirements: []
46
- rubygems_version: 3.0.6
47
- signing_key:
50
+ rubygems_version: 3.1.4
51
+ signing_key:
48
52
  specification_version: 4
49
- summary: A Ruby client for communicating with the Docker HTTP API v2.
53
+ summary: A Ruby client for communicating with the Docker registry API v2.
50
54
  test_files: []
@@ -1,20 +0,0 @@
1
- module Docker
2
- module Remote
3
- class ServerAuth
4
- attr_reader :auth_type, :params
5
-
6
- def initialize(auth_type, params)
7
- @auth_type = auth_type
8
- @params = params
9
- end
10
-
11
- def realm
12
- @params['realm']
13
- end
14
-
15
- def service
16
- @params['service']
17
- end
18
- end
19
- end
20
- end