Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ On timeout, `DockerEngineRuby::Errors::APITimeoutError` is raised.

Note that requests that time out are retried by default.

### TLS / mTLS

For Docker Engine over TLS (`https://...:2376`), you can provide CA, client certificate, and client key paths:

```ruby
docker = DockerEngineRuby::Client.new(
base_url: "https://localhost:2376",
tls_ca_cert_path: "/path/to/ca.pem",
tls_client_cert_path: "/path/to/cert.pem",
tls_client_key_path: "/path/to/key.pem"
)
```

You can also configure these through environment variables:

- `DOCKER_TLS_CA_CERT_PATH`
- `DOCKER_TLS_CLIENT_CERT_PATH`
- `DOCKER_TLS_CLIENT_KEY_PATH`

## Advanced concepts

### BaseModel
Expand Down
34 changes: 33 additions & 1 deletion lib/docker_engine_ruby/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ class Client < DockerEngineRuby::Internal::Transport::BaseClient
ENVIRONMENTS = {production: "http://localhost:2375", production_tls: "https://localhost:2376"}
# rubocop:enable Style/MutableConstant

# Path to the trusted CA certificate file (PEM) used to verify the Docker daemon
# certificate.
# @return [String, nil]
attr_reader :tls_ca_cert_path

# Path to the client TLS certificate file (PEM).
# @return [String, nil]
attr_reader :tls_client_cert_path

# Path to the client TLS private key file (PEM).
# @return [String, nil]
attr_reader :tls_client_key_path

# @return [DockerEngineRuby::Resources::Auth]
attr_reader :auth

Expand Down Expand Up @@ -67,6 +80,15 @@ class Client < DockerEngineRuby::Internal::Transport::BaseClient

# Creates and returns a new client for interacting with the API.
#
# @param tls_ca_cert_path [String, nil] Path to the trusted CA certificate file (PEM) used to verify the Docker daemon
# certificate. Defaults to `ENV["DOCKER_TLS_CA_CERT_PATH"]`
#
# @param tls_client_cert_path [String, nil] Path to the client TLS certificate file (PEM). Defaults to
# `ENV["DOCKER_TLS_CLIENT_CERT_PATH"]`
#
# @param tls_client_key_path [String, nil] Path to the client TLS private key file (PEM). Defaults to
# `ENV["DOCKER_TLS_CLIENT_KEY_PATH"]`
#
# @param environment [:production, :production_tls, nil] Specifies the environment to use for the API.
#
# Each environment maps to a different base URL:
Expand All @@ -85,6 +107,9 @@ class Client < DockerEngineRuby::Internal::Transport::BaseClient
#
# @param max_retry_delay [Float]
def initialize(
tls_ca_cert_path: ENV["DOCKER_TLS_CA_CERT_PATH"],
tls_client_cert_path: ENV["DOCKER_TLS_CLIENT_CERT_PATH"],
tls_client_key_path: ENV["DOCKER_TLS_CLIENT_KEY_PATH"],
environment: nil,
base_url: ENV["DOCKER_BASE_URL"],
max_retries: self.class::DEFAULT_MAX_RETRIES,
Expand All @@ -97,12 +122,19 @@ def initialize(
raise ArgumentError.new(message)
end

@tls_ca_cert_path = tls_ca_cert_path&.to_s
@tls_client_cert_path = tls_client_cert_path&.to_s
@tls_client_key_path = tls_client_key_path&.to_s

super(
base_url: base_url,
timeout: timeout,
max_retries: max_retries,
initial_retry_delay: initial_retry_delay,
max_retry_delay: max_retry_delay
max_retry_delay: max_retry_delay,
tls_ca_cert_path: @tls_ca_cert_path,
tls_client_cert_path: @tls_client_cert_path,
tls_client_key_path: @tls_client_key_path
)

@auth = DockerEngineRuby::Resources::Auth.new(client: self)
Expand Down
14 changes: 12 additions & 2 deletions lib/docker_engine_ruby/internal/transport/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,26 @@ def reap_connection!(status, stream:)
# @param max_retry_delay [Float]
# @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
# @param idempotency_header [String, nil]
# @param tls_ca_cert_path [String, nil]
# @param tls_client_cert_path [String, nil]
# @param tls_client_key_path [String, nil]
def initialize(
base_url:,
timeout: 0.0,
max_retries: 0,
initial_retry_delay: 0.0,
max_retry_delay: 0.0,
headers: {},
idempotency_header: nil
idempotency_header: nil,
tls_ca_cert_path: nil,
tls_client_cert_path: nil,
tls_client_key_path: nil
)
@requester = DockerEngineRuby::Internal::Transport::PooledNetRequester.new
@requester = DockerEngineRuby::Internal::Transport::PooledNetRequester.new(
tls_ca_cert_path: tls_ca_cert_path,
tls_client_cert_path: tls_client_cert_path,
tls_client_key_path: tls_client_key_path
)
@headers = DockerEngineRuby::Internal::Util.normalized_headers(
self.class::PLATFORM_HEADERS,
{
Expand Down
36 changes: 32 additions & 4 deletions lib/docker_engine_ruby/internal/transport/pooled_net_requester.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ class << self
# @api private
#
# @param cert_store [OpenSSL::X509::Store]
# @param tls_cert [OpenSSL::X509::Certificate, nil]
# @param tls_key [OpenSSL::PKey::PKey, nil]
# @param url [URI::Generic]
#
# @return [Net::HTTP]
def connect(cert_store:, url:)
def connect(cert_store:, tls_cert:, tls_key:, url:)
port =
case [url.port, url.scheme]
in [Integer, _]
Expand All @@ -35,7 +37,11 @@ def connect(cert_store:, url:)
_1.use_ssl = %w[https wss].include?(url.scheme)
_1.max_retries = 0

(_1.cert_store = cert_store) if _1.use_ssl?
if _1.use_ssl?
_1.cert_store = cert_store
_1.cert = tls_cert if tls_cert
_1.key = tls_key if tls_key
end
end
end

Expand Down Expand Up @@ -105,7 +111,7 @@ def build_request(request, &blk)
pool =
@mutex.synchronize do
@pools[origin] ||= ConnectionPool.new(size: @size) do
self.class.connect(cert_store: @cert_store, url: url)
self.class.connect(cert_store: @cert_store, tls_cert: @tls_cert, tls_key: @tls_key, url: url)
end
end

Expand Down Expand Up @@ -194,10 +200,32 @@ def execute(request)
# @api private
#
# @param size [Integer]
def initialize(size: self.class::DEFAULT_MAX_CONNECTIONS)
# @param tls_ca_cert_path [String, nil]
# @param tls_client_cert_path [String, nil]
# @param tls_client_key_path [String, nil]
def initialize(
size: self.class::DEFAULT_MAX_CONNECTIONS,
tls_ca_cert_path: nil,
tls_client_cert_path: nil,
tls_client_key_path: nil
)
@mutex = Mutex.new
@size = size
@cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
@cert_store.add_file(tls_ca_cert_path) if tls_ca_cert_path

if tls_client_cert_path || tls_client_key_path
if tls_client_cert_path.nil? || tls_client_key_path.nil?
raise ArgumentError.new("Both tls_client_cert_path and tls_client_key_path must be provided together.")
end

@tls_cert = OpenSSL::X509::Certificate.new(File.read(tls_client_cert_path))
@tls_key = OpenSSL::PKey.read(File.read(tls_client_key_path))
else
@tls_cert = nil
@tls_key = nil
end

@pools = {}
end

Expand Down
25 changes: 25 additions & 0 deletions rbi/docker_engine_ruby/client.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ module DockerEngineRuby
T::Hash[Symbol, String]
)

# Path to the trusted CA certificate file (PEM) used to verify the Docker daemon
# certificate.
sig { returns(T.nilable(String)) }
attr_reader :tls_ca_cert_path

# Path to the client TLS certificate file (PEM).
sig { returns(T.nilable(String)) }
attr_reader :tls_client_cert_path

# Path to the client TLS private key file (PEM).
sig { returns(T.nilable(String)) }
attr_reader :tls_client_key_path

sig { returns(DockerEngineRuby::Resources::Auth) }
attr_reader :auth

Expand Down Expand Up @@ -67,6 +80,9 @@ module DockerEngineRuby
# Creates and returns a new client for interacting with the API.
sig do
params(
tls_ca_cert_path: T.nilable(String),
tls_client_cert_path: T.nilable(String),
tls_client_key_path: T.nilable(String),
environment: T.nilable(T.any(Symbol, String)),
base_url: T.nilable(String),
max_retries: Integer,
Expand All @@ -76,6 +92,15 @@ module DockerEngineRuby
).returns(T.attached_class)
end
def self.new(
# Path to the trusted CA certificate file (PEM) used to verify the Docker daemon
# certificate. Defaults to `ENV["DOCKER_TLS_CA_CERT_PATH"]`
tls_ca_cert_path: ENV["DOCKER_TLS_CA_CERT_PATH"],
# Path to the client TLS certificate file (PEM). Defaults to
# `ENV["DOCKER_TLS_CLIENT_CERT_PATH"]`
tls_client_cert_path: ENV["DOCKER_TLS_CLIENT_CERT_PATH"],
# Path to the client TLS private key file (PEM). Defaults to
# `ENV["DOCKER_TLS_CLIENT_KEY_PATH"]`
tls_client_key_path: ENV["DOCKER_TLS_CLIENT_KEY_PATH"],
# Specifies the environment to use for the API.
#
# Each environment maps to a different base URL:
Expand Down
10 changes: 8 additions & 2 deletions rbi/docker_engine_ruby/internal/transport/base_client.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ module DockerEngineRuby
)
)
],
idempotency_header: T.nilable(String)
idempotency_header: T.nilable(String),
tls_ca_cert_path: T.nilable(String),
tls_client_cert_path: T.nilable(String),
tls_client_key_path: T.nilable(String)
).returns(T.attached_class)
end
def self.new(
Expand All @@ -175,7 +178,10 @@ module DockerEngineRuby
initial_retry_delay: 0.0,
max_retry_delay: 0.0,
headers: {},
idempotency_header: nil
idempotency_header: nil,
tls_ca_cert_path: nil,
tls_client_cert_path: nil,
tls_client_key_path: nil
)
end

Expand Down
23 changes: 19 additions & 4 deletions rbi/docker_engine_ruby/internal/transport/pooled_net_requester.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ module DockerEngineRuby
class << self
# @api private
sig do
params(cert_store: OpenSSL::X509::Store, url: URI::Generic).returns(
params(
cert_store: OpenSSL::X509::Store,
tls_cert: T.nilable(OpenSSL::X509::Certificate),
tls_key: T.nilable(OpenSSL::PKey::PKey),
url: URI::Generic
).returns(
Net::HTTP
)
end
def connect(cert_store:, url:)
def connect(cert_store:, tls_cert:, tls_key:, url:)
end

# @api private
Expand Down Expand Up @@ -73,9 +78,19 @@ module DockerEngineRuby
end

# @api private
sig { params(size: Integer).returns(T.attached_class) }
sig do
params(
size: Integer,
tls_ca_cert_path: T.nilable(String),
tls_client_cert_path: T.nilable(String),
tls_client_key_path: T.nilable(String)
).returns(T.attached_class)
end
def self.new(
size: DockerEngineRuby::Internal::Transport::PooledNetRequester::DEFAULT_MAX_CONNECTIONS
size: DockerEngineRuby::Internal::Transport::PooledNetRequester::DEFAULT_MAX_CONNECTIONS,
tls_ca_cert_path: nil,
tls_client_cert_path: nil,
tls_client_key_path: nil
)
end
end
Expand Down
9 changes: 9 additions & 0 deletions sig/docker_engine_ruby/client.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ module DockerEngineRuby
production_tls: "https://localhost:2376"
}

attr_reader tls_ca_cert_path: String?

attr_reader tls_client_cert_path: String?

attr_reader tls_client_key_path: String?

attr_reader auth: DockerEngineRuby::Resources::Auth

attr_reader system_: DockerEngineRuby::Resources::System
Expand Down Expand Up @@ -44,6 +50,9 @@ module DockerEngineRuby
attr_reader distribution: DockerEngineRuby::Resources::Distribution

def initialize: (
?tls_ca_cert_path: String?,
?tls_client_cert_path: String?,
?tls_client_key_path: String?,
?environment: :production | :production_tls | nil,
?base_url: String?,
?max_retries: Integer,
Expand Down
5 changes: 4 additions & 1 deletion sig/docker_engine_ruby/internal/transport/base_client.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ module DockerEngineRuby
?headers: ::Hash[String, (String
| Integer
| ::Array[(String | Integer)?])?],
?idempotency_header: String?
?idempotency_header: String?,
?tls_ca_cert_path: String?,
?tls_client_cert_path: String?,
?tls_client_key_path: String?
) -> void

private def user_agent: -> String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module DockerEngineRuby

def self.connect: (
cert_store: OpenSSL::X509::Store,
tls_cert: OpenSSL::X509::Certificate?,
tls_key: OpenSSL::PKey::PKey?,
url: URI::Generic
) -> top

Expand All @@ -41,7 +43,12 @@ module DockerEngineRuby
DockerEngineRuby::Internal::Transport::PooledNetRequester::request request
) -> [Integer, top, Enumerable[String]]

def initialize: (?size: Integer) -> void
def initialize: (
?size: Integer,
?tls_ca_cert_path: String?,
?tls_client_cert_path: String?,
?tls_client_key_path: String?
) -> void
end
end
end
Expand Down
Loading