Skip to content

Commit bd64e7c

Browse files
authored
Add unix:/// base_url support for Docker socket transportFeat/unix socket base url (#15)
* Add unix socket support for unix:/// base_url * Document unix socket base_url usage * Fix unix socket Net::HTTP initializer compatibility
1 parent cd08154 commit bd64e7c

File tree

4 files changed

+107
-8
lines changed

4 files changed

+107
-8
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ create_response = docker.containers.create(name: "sample-container")
3737
puts(create_response.Id)
3838
```
3939

40+
### Using a Unix socket
41+
42+
You can connect to a local Docker daemon over a Unix socket by setting `base_url` to a `unix:///...` path:
43+
44+
```ruby
45+
docker = DockerEngineRuby::Client.new(
46+
base_url: "unix:///var/run/docker.sock"
47+
)
48+
```
49+
50+
When `base_url` uses `unix:///`, TLS-related options are not used. TLS/mTLS configuration applies to HTTPS daemon endpoints (for example, `https://localhost:2376`).
51+
4052
### Handling errors
4153

4254
When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `DockerEngineRuby::Errors::APIError` will be thrown:

lib/docker_engine_ruby/internal/transport/base_client.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,13 @@ def initialize(
212212
tls_client_cert_path: nil,
213213
tls_client_key_path: nil
214214
)
215+
parsed_base_url = DockerEngineRuby::Internal::Util.parse_uri(base_url)
216+
@unix_socket_path = parsed_base_url[:scheme] == "unix" ? parsed_base_url[:path] : nil
217+
if parsed_base_url[:scheme] == "unix" && @unix_socket_path.to_s.empty?
218+
raise ArgumentError.new("base_url unix:// must include an absolute socket path")
219+
end
215220
@requester = DockerEngineRuby::Internal::Transport::PooledNetRequester.new(
221+
unix_socket_path: @unix_socket_path,
216222
tls_ca_cert_path: tls_ca_cert_path,
217223
tls_client_cert_path: tls_client_cert_path,
218224
tls_client_key_path: tls_client_key_path
@@ -226,8 +232,13 @@ def initialize(
226232
},
227233
headers
228234
)
229-
@base_url_components = DockerEngineRuby::Internal::Util.parse_uri(base_url)
230-
@base_url = DockerEngineRuby::Internal::Util.unparse_uri(@base_url_components)
235+
@base_url_components =
236+
if @unix_socket_path
237+
DockerEngineRuby::Internal::Util.parse_uri("http://localhost")
238+
else
239+
parsed_base_url
240+
end
241+
@base_url = base_url
231242
@idempotency_header = idempotency_header&.to_s&.downcase
232243
@timeout = timeout
233244
@max_retries = max_retries
@@ -328,6 +339,7 @@ def initialize(
328339
{
329340
method: method,
330341
url: url,
342+
unix_socket_path: @unix_socket_path,
331343
headers: headers,
332344
body: encoded,
333345
max_retries: opts.fetch(:max_retries, @max_retries),
@@ -590,6 +602,7 @@ def inspect
590602
{
591603
method: Symbol,
592604
url: URI::Generic,
605+
unix_socket_path: T.nilable(String),
593606
headers: T::Hash[String, String],
594607
body: T.anything,
595608
max_retries: Integer,

lib/docker_engine_ruby/internal/transport/pooled_net_requester.rb

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ module Transport
77
class PooledNetRequester
88
extend DockerEngineRuby::Internal::Util::SorbetRuntimeSupport
99

10+
class UnixSocketHTTP < Net::HTTP
11+
# Net::HTTP.new forwards multiple args to #initialize.
12+
# We only care about socket_path and ignore the rest.
13+
def initialize(socket_path, *_rest)
14+
super("localhost", Net::HTTP.http_default_port)
15+
@socket_path = socket_path
16+
end
17+
18+
private def connect
19+
raise ArgumentError.new("TLS over unix sockets is not supported") if use_ssl?
20+
21+
@socket = Net::BufferedIO.new(UNIXSocket.new(@socket_path))
22+
@last_communicated = nil
23+
on_connect
24+
end
25+
end
26+
1027
# from the golang stdlib
1128
# https://github.com/golang/go/blob/c8eced8580028328fde7c03cbfcb720ce15b2358/src/net/http/transport.go#L49
1229
KEEP_ALIVE_TIMEOUT = 30
@@ -19,10 +36,17 @@ class << self
1936
# @param cert_store [OpenSSL::X509::Store]
2037
# @param tls_cert [OpenSSL::X509::Certificate, nil]
2138
# @param tls_key [OpenSSL::PKey::PKey, nil]
39+
# @param unix_socket_path [String, nil]
2240
# @param url [URI::Generic]
2341
#
2442
# @return [Net::HTTP]
25-
def connect(cert_store:, tls_cert:, tls_key:, url:)
43+
def connect(cert_store:, tls_cert:, tls_key:, unix_socket_path:, url:)
44+
if unix_socket_path
45+
return UnixSocketHTTP.new(unix_socket_path).tap do
46+
_1.use_ssl = false
47+
_1.max_retries = 0
48+
end
49+
end
2650
port =
2751
case [url.port, url.scheme]
2852
in [Integer, _]
@@ -100,18 +124,25 @@ def build_request(request, &blk)
100124
# @api private
101125
#
102126
# @param url [URI::Generic]
127+
# @param unix_socket_path [String, nil]
103128
# @param deadline [Float]
104129
# @param blk [Proc]
105130
#
106131
# @raise [Timeout::Error]
107132
# @yieldparam [Net::HTTP]
108-
private def with_pool(url, deadline:, &blk)
109-
origin = DockerEngineRuby::Internal::Util.uri_origin(url)
133+
private def with_pool(url, unix_socket_path:, deadline:, &blk)
134+
origin = unix_socket_path || DockerEngineRuby::Internal::Util.uri_origin(url)
110135
timeout = deadline - DockerEngineRuby::Internal::Util.monotonic_secs
111136
pool =
112137
@mutex.synchronize do
113138
@pools[origin] ||= ConnectionPool.new(size: @size) do
114-
self.class.connect(cert_store: @cert_store, tls_cert: @tls_cert, tls_key: @tls_key, url: url)
139+
self.class.connect(
140+
cert_store: @cert_store,
141+
tls_cert: @tls_cert,
142+
tls_key: @tls_key,
143+
unix_socket_path: unix_socket_path,
144+
url: url
145+
)
115146
end
116147
end
117148

@@ -135,6 +166,7 @@ def build_request(request, &blk)
135166
# @return [Array(Integer, Net::HTTPResponse, Enumerable<String>)]
136167
def execute(request)
137168
url, deadline = request.fetch_values(:url, :deadline)
169+
unix_socket_path = request.fetch(:unix_socket_path, @default_unix_socket_path)
138170

139171
req = nil
140172
finished = false
@@ -143,7 +175,7 @@ def execute(request)
143175
enum = Enumerator.new do |y|
144176
next if finished
145177

146-
with_pool(url, deadline: deadline) do |conn|
178+
with_pool(url, unix_socket_path: unix_socket_path, deadline: deadline) do |conn|
147179
eof = false
148180
closing = nil
149181
::Thread.handle_interrupt(Object => :never) do
@@ -203,14 +235,17 @@ def execute(request)
203235
# @param tls_ca_cert_path [String, nil]
204236
# @param tls_client_cert_path [String, nil]
205237
# @param tls_client_key_path [String, nil]
238+
# @param unix_socket_path [String, nil]
206239
def initialize(
207240
size: self.class::DEFAULT_MAX_CONNECTIONS,
241+
unix_socket_path: nil,
208242
tls_ca_cert_path: nil,
209243
tls_client_cert_path: nil,
210244
tls_client_key_path: nil
211245
)
212246
@mutex = Mutex.new
213247
@size = size
248+
@default_unix_socket_path = unix_socket_path
214249
@cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
215250
@cert_store.add_file(tls_ca_cert_path) if tls_ca_cert_path
216251

@@ -230,7 +265,16 @@ def initialize(
230265
end
231266

232267
define_sorbet_constant!(:Request) do
233-
T.type_alias { {method: Symbol, url: URI::Generic, headers: T::Hash[String, String], body: T.anything, deadline: Float} }
268+
T.type_alias do
269+
{
270+
method: Symbol,
271+
url: URI::Generic,
272+
unix_socket_path: T.nilable(String),
273+
headers: T::Hash[String, String],
274+
body: T.anything,
275+
deadline: Float
276+
}
277+
end
234278
end
235279
end
236280
end

test/docker_engine_ruby/client_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,36 @@ def test_non_redirect_304_is_treated_as_status_error_for_other_operations
372372
assert_equal(304, error.status)
373373
end
374374

375+
def test_client_unix_base_url_routes_requests_through_unix_socket_path
376+
response_class = Struct.new(:headers) do
377+
def each_header = headers.each
378+
end
379+
requester_class = Class.new do
380+
attr_reader :last_request
381+
382+
define_method(:initialize) { |response| @response = response }
383+
define_method(:execute) do |request|
384+
@last_request = request
385+
[200, @response, ["[]"]]
386+
end
387+
end
388+
requester = requester_class.new(response_class.new({"content-type" => "application/json"}))
389+
390+
docker = DockerEngineRuby::Client.new(base_url: "unix:///var/run/docker.sock")
391+
docker.instance_variable_set(:@requester, requester)
392+
393+
docker.containers.list
394+
395+
assert_equal("/var/run/docker.sock", requester.last_request[:unix_socket_path])
396+
assert_equal("http://localhost/containers/json", requester.last_request[:url].to_s)
397+
end
398+
399+
def test_client_unix_base_url_requires_socket_path
400+
assert_raises(ArgumentError) do
401+
DockerEngineRuby::Client.new(base_url: "unix://")
402+
end
403+
end
404+
375405
def test_default_headers
376406
stub_request(:get, "http://localhost/containers/json").to_return_json(status: 200, body: {})
377407

0 commit comments

Comments
 (0)