docker-api-ng 0.3.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 +7 -0
- data/LICENSE +202 -0
- data/NOTICE +12 -0
- data/README.md +278 -0
- data/docker-api-ng.gemspec +42 -0
- data/docs/building-images.md +98 -0
- data/docs/connecting.md +111 -0
- data/docs/errors.md +89 -0
- data/docs/exec.md +100 -0
- data/docs/extending.md +109 -0
- data/docs/migrating-from-docker-api.md +156 -0
- data/docs/streaming.md +91 -0
- data/lib/docker/api/auth.rb +154 -0
- data/lib/docker/api/body.rb +47 -0
- data/lib/docker/api/client.rb +117 -0
- data/lib/docker/api/collection.rb +60 -0
- data/lib/docker/api/collections/containers.rb +78 -0
- data/lib/docker/api/collections/images.rb +221 -0
- data/lib/docker/api/collections/networks.rb +85 -0
- data/lib/docker/api/collections/system.rb +98 -0
- data/lib/docker/api/collections/volumes.rb +54 -0
- data/lib/docker/api/config.rb +163 -0
- data/lib/docker/api/connection.rb +333 -0
- data/lib/docker/api/context.rb +124 -0
- data/lib/docker/api/errors.rb +159 -0
- data/lib/docker/api/operations.rb +3064 -0
- data/lib/docker/api/path.rb +33 -0
- data/lib/docker/api/platform.rb +68 -0
- data/lib/docker/api/query.rb +76 -0
- data/lib/docker/api/resource.rb +174 -0
- data/lib/docker/api/resources/container.rb +378 -0
- data/lib/docker/api/resources/exec.rb +48 -0
- data/lib/docker/api/resources/image.rb +209 -0
- data/lib/docker/api/resources/network.rb +90 -0
- data/lib/docker/api/resources/volume.rb +51 -0
- data/lib/docker/api/response.rb +110 -0
- data/lib/docker/api/session.rb +70 -0
- data/lib/docker/api/stream.rb +142 -0
- data/lib/docker/api/tar.rb +157 -0
- data/lib/docker/api/transport/base.rb +62 -0
- data/lib/docker/api/transport/fake.rb +154 -0
- data/lib/docker/api/transport/named_pipe.rb +58 -0
- data/lib/docker/api/transport/tcp.rb +49 -0
- data/lib/docker/api/transport/tls.rb +84 -0
- data/lib/docker/api/transport/unix.rb +35 -0
- data/lib/docker/api/transport.rb +113 -0
- data/lib/docker/api/version.rb +21 -0
- data/lib/docker/api.rb +68 -0
- data/sig/docker/api/core.rbs +109 -0
- data/sig/docker/api/operations.rbs +123 -0
- metadata +97 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Tim Smith
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
module Docker
|
|
7
|
+
module API
|
|
8
|
+
# The networks on a daemon.
|
|
9
|
+
class Networks < Collection
|
|
10
|
+
# @param filters [Hash, nil] daemon-side filters
|
|
11
|
+
# @return [Array<Docker::API::Network>] partial resources
|
|
12
|
+
def all(filters: nil)
|
|
13
|
+
operations.network_list(filters: filters)
|
|
14
|
+
.json!.map { |payload| Network.new(client: client, raw: payload, partial: true) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param id [String] a name or id
|
|
18
|
+
# @param verbose [Boolean] include service details
|
|
19
|
+
# @param scope [String, nil] "swarm", "global" or "local"
|
|
20
|
+
# @return [Docker::API::Network]
|
|
21
|
+
# @raise [Docker::API::NotFound] if there is no such network
|
|
22
|
+
def get(id, verbose: false, scope: nil)
|
|
23
|
+
Network.new(
|
|
24
|
+
client: client,
|
|
25
|
+
raw: operations.network_inspect(id: id, verbose: verbose, scope: scope).json,
|
|
26
|
+
partial: false
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Create a network.
|
|
31
|
+
#
|
|
32
|
+
# @param name [String] the network's name
|
|
33
|
+
# @param driver [String, nil] "bridge", "overlay", and so on
|
|
34
|
+
# @param ipv6 [Boolean] enable IPv6 addressing
|
|
35
|
+
# @param internal [Boolean] withhold external access
|
|
36
|
+
# @param attachable [Boolean] allow manual container attachment
|
|
37
|
+
# @param labels [Hash, nil] labels for the network
|
|
38
|
+
# @param ipam [Hash, nil] an IPAM configuration
|
|
39
|
+
# @param options [Hash, nil] driver-specific options
|
|
40
|
+
# @param attributes [Hash] any further body attributes
|
|
41
|
+
# @return [Docker::API::Network]
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# client.networks.create("dokken", ipv6: true,
|
|
45
|
+
# ipam: { "Config" => [{ "Subnet" => "fd00::/64" }] })
|
|
46
|
+
def create(name, driver: nil, ipv6: false, internal: false, attachable: false,
|
|
47
|
+
labels: nil, ipam: nil, options: nil, **attributes)
|
|
48
|
+
body = Body.build(attributes).merge(
|
|
49
|
+
"Name" => name, "Driver" => driver, "EnableIPv6" => ipv6,
|
|
50
|
+
"Internal" => internal, "Attachable" => attachable,
|
|
51
|
+
"Labels" => labels, "IPAM" => ipam, "Options" => options
|
|
52
|
+
).compact
|
|
53
|
+
|
|
54
|
+
get(operations.network_create(body: body).json!["Id"])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Fetch a network by name, creating it if it does not exist.
|
|
58
|
+
#
|
|
59
|
+
# Two processes racing to create the same shared network is ordinary
|
|
60
|
+
# rather than exceptional -- parallel test suites do it constantly -- so
|
|
61
|
+
# losing that race is treated as success and the winner's network is
|
|
62
|
+
# returned.
|
|
63
|
+
#
|
|
64
|
+
# @param name [String] the network's name
|
|
65
|
+
# @param attributes [Hash] passed to {#create} when creating
|
|
66
|
+
# @return [Docker::API::Network]
|
|
67
|
+
def ensure(name, **attributes)
|
|
68
|
+
found = find(name)
|
|
69
|
+
return found if found
|
|
70
|
+
|
|
71
|
+
begin
|
|
72
|
+
create(name, **attributes)
|
|
73
|
+
rescue Conflict
|
|
74
|
+
get(name)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param filters [Hash, nil] which networks to consider
|
|
79
|
+
# @return [Hash] what was deleted
|
|
80
|
+
def prune(filters: nil)
|
|
81
|
+
operations.network_prune(filters: filters).json
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Tim Smith
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
module Docker
|
|
7
|
+
module API
|
|
8
|
+
# The daemon itself, rather than the things it holds.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# client.system.info["ServerVersion"] #=> "29.7.2"
|
|
12
|
+
# client.system.ping? #=> true
|
|
13
|
+
class System < Collection
|
|
14
|
+
# @return [Hash] the daemon's full description of itself
|
|
15
|
+
def info
|
|
16
|
+
operations.system_info.json
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Hash] versions of the daemon and its components
|
|
20
|
+
def version
|
|
21
|
+
operations.system_version.json
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The Engine API version in use for this client, after negotiation.
|
|
25
|
+
#
|
|
26
|
+
# @return [String, nil]
|
|
27
|
+
def api_version
|
|
28
|
+
client.connection.api_version
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] whether the daemon answered
|
|
32
|
+
def ping?
|
|
33
|
+
client.connection.ping.success?
|
|
34
|
+
rescue Error
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Hash] disk usage by images, containers, volumes and cache
|
|
39
|
+
def data_usage(type: nil, verbose: false)
|
|
40
|
+
operations.system_data_usage(type: type, verbose: verbose).json
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Whether the daemon is Podman wearing Docker's API.
|
|
44
|
+
#
|
|
45
|
+
# Worth knowing, because Podman implements most of this API and diverges
|
|
46
|
+
# in enough places -- notably around networking and rootless behaviour --
|
|
47
|
+
# that callers sometimes need to branch on it.
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def podman?
|
|
51
|
+
Array(version["Components"]).any? { |c| c["Name"].to_s.include?("Podman") }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Boolean] whether the daemon runs without root privileges
|
|
55
|
+
def rootless?
|
|
56
|
+
info["Rootless"] == true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Stream events from the daemon as they happen.
|
|
60
|
+
#
|
|
61
|
+
# @param since [String, Integer, nil] where to start in the past
|
|
62
|
+
# @param until_time [String, Integer, nil] where to stop
|
|
63
|
+
# @param filters [Hash, nil] which events to report
|
|
64
|
+
# @yieldparam event [Hash] one event
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# client.system.events(filters: { "type" => ["container"] }) do |event|
|
|
69
|
+
# puts "#{event["Action"]} #{event.dig("Actor", "Attributes", "name")}"
|
|
70
|
+
# end
|
|
71
|
+
def events(since: nil, until_time: nil, filters: nil, &block)
|
|
72
|
+
raise ArgumentError, "events needs a block to yield to" unless block
|
|
73
|
+
|
|
74
|
+
stream = Stream::JSONLines.new(&block)
|
|
75
|
+
operations.system_events(
|
|
76
|
+
since: since, until_: until_time, filters: filters
|
|
77
|
+
) { |chunk| stream << chunk }
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check credentials against a registry.
|
|
82
|
+
#
|
|
83
|
+
# @param username [String] the account
|
|
84
|
+
# @param password [String] its password or token
|
|
85
|
+
# @param serveraddress [String, nil] the registry, or nil for Docker Hub
|
|
86
|
+
# @return [Hash] the daemon's answer
|
|
87
|
+
# @raise [Docker::API::Unauthorized] if the registry rejected them
|
|
88
|
+
def authenticate(username:, password:, serveraddress: nil)
|
|
89
|
+
operations.system_auth(
|
|
90
|
+
body: {
|
|
91
|
+
"username" => username, "password" => password,
|
|
92
|
+
"serveraddress" => serveraddress || Auth::DOCKER_HUB_KEY
|
|
93
|
+
}.compact
|
|
94
|
+
).json
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Tim Smith
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
module Docker
|
|
7
|
+
module API
|
|
8
|
+
# The volumes on a daemon.
|
|
9
|
+
class Volumes < Collection
|
|
10
|
+
# @param filters [Hash, nil] daemon-side filters
|
|
11
|
+
# @return [Array<Docker::API::Volume>]
|
|
12
|
+
def all(filters: nil)
|
|
13
|
+
payload = operations.volume_list(filters: filters).json!
|
|
14
|
+
Array(payload["Volumes"]).map do |entry|
|
|
15
|
+
Volume.new(client: client, raw: entry, partial: false)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<String>] names of volumes the daemon could not inspect
|
|
20
|
+
def warnings(filters: nil)
|
|
21
|
+
Array(operations.volume_list(filters: filters).json!["Warnings"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param name [String] the volume's name
|
|
25
|
+
# @return [Docker::API::Volume]
|
|
26
|
+
# @raise [Docker::API::NotFound] if there is no such volume
|
|
27
|
+
def get(name)
|
|
28
|
+
Volume.new(client: client, raw: operations.volume_inspect(name: name).json, partial: false)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create a volume.
|
|
32
|
+
#
|
|
33
|
+
# @param name [String, nil] a name, or nil to let the daemon choose one
|
|
34
|
+
# @param driver [String, nil] the driver to back it with
|
|
35
|
+
# @param labels [Hash, nil] labels for the volume
|
|
36
|
+
# @param driver_opts [Hash, nil] driver-specific options
|
|
37
|
+
# @return [Docker::API::Volume]
|
|
38
|
+
def create(name = nil, driver: nil, labels: nil, driver_opts: nil)
|
|
39
|
+
body = {
|
|
40
|
+
"Name" => name, "Driver" => driver,
|
|
41
|
+
"Labels" => labels, "DriverOpts" => driver_opts
|
|
42
|
+
}.compact
|
|
43
|
+
|
|
44
|
+
Volume.new(client: client, raw: operations.volume_create(body: body).json, partial: false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param filters [Hash, nil] which volumes to consider
|
|
48
|
+
# @return [Hash] what was deleted and how much space it freed
|
|
49
|
+
def prune(filters: nil)
|
|
50
|
+
operations.volume_prune(filters: filters).json
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Tim Smith
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require "rbconfig" unless defined?(RbConfig)
|
|
7
|
+
|
|
8
|
+
module Docker
|
|
9
|
+
module API
|
|
10
|
+
# Where the daemon is and how to reach it, resolved once and then frozen.
|
|
11
|
+
#
|
|
12
|
+
# Frozen is the point. The docker-api gem exposes `Docker.url=` and
|
|
13
|
+
# `Docker.creds=` as process-global setters, so a second caller silently
|
|
14
|
+
# inherits -- or clobbers -- the first caller's daemon. Configuration here
|
|
15
|
+
# is a value: two clients hold two of them and cannot interfere.
|
|
16
|
+
class Config
|
|
17
|
+
# Where Docker listens by default on Linux and macOS.
|
|
18
|
+
DEFAULT_UNIX_SOCKET = "unix:///var/run/docker.sock"
|
|
19
|
+
|
|
20
|
+
# Where Docker Desktop for Windows listens by default.
|
|
21
|
+
DEFAULT_WINDOWS_PIPE = "npipe:////./pipe/docker_engine"
|
|
22
|
+
|
|
23
|
+
# @return [String] the daemon URL
|
|
24
|
+
attr_reader :url
|
|
25
|
+
|
|
26
|
+
# @return [Hash] TLS material, empty when TLS is not in use
|
|
27
|
+
attr_reader :tls
|
|
28
|
+
|
|
29
|
+
# @return [String, Symbol] `:negotiate`, `:none`, or a pinned version
|
|
30
|
+
attr_reader :api_version
|
|
31
|
+
|
|
32
|
+
# @return [Numeric] seconds to wait for response data
|
|
33
|
+
attr_reader :read_timeout
|
|
34
|
+
|
|
35
|
+
# @return [Numeric] seconds to wait for a connection
|
|
36
|
+
attr_reader :open_timeout
|
|
37
|
+
|
|
38
|
+
# @param url [String, nil] the daemon URL
|
|
39
|
+
# @param tls [Hash, nil] TLS material
|
|
40
|
+
# @param api_version [String, Symbol] version handling
|
|
41
|
+
# @param read_timeout [Numeric] seconds
|
|
42
|
+
# @param open_timeout [Numeric] seconds
|
|
43
|
+
def initialize(url: nil, tls: nil, api_version: :negotiate,
|
|
44
|
+
read_timeout: 60, open_timeout: 10)
|
|
45
|
+
@url = url || self.class.default_url
|
|
46
|
+
@tls = (tls || {}).freeze
|
|
47
|
+
@api_version = api_version
|
|
48
|
+
@read_timeout = read_timeout
|
|
49
|
+
@open_timeout = open_timeout
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Resolve configuration the way the Docker CLI does.
|
|
54
|
+
#
|
|
55
|
+
# @param env [Hash] the environment to read, injectable for tests
|
|
56
|
+
# @return [Docker::API::Config]
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# Config.from_env("DOCKER_HOST" => "tcp://build:2376",
|
|
60
|
+
# "DOCKER_CERT_PATH" => "/certs",
|
|
61
|
+
# "DOCKER_TLS_VERIFY" => "1")
|
|
62
|
+
def self.from_env(env = ENV, **overrides)
|
|
63
|
+
# Ruby permits non-Symbol keys in a **kwargs splat, so
|
|
64
|
+
# `from_env("DOCKER_HOST" => "tcp://x")` puts the variable in overrides
|
|
65
|
+
# and quietly reads the real environment instead. Saying so is much
|
|
66
|
+
# kinder than resolving the wrong daemon and never mentioning it.
|
|
67
|
+
stray = overrides.keys.grep(String)
|
|
68
|
+
unless stray.empty?
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"from_env takes the environment as its first argument: " \
|
|
71
|
+
"from_env({ #{stray.first.inspect} => ... }). Got #{stray.inspect} as options."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Resolution order, matching the CLI: an explicit argument, then
|
|
75
|
+
# DOCKER_HOST, then the active context, then the platform default.
|
|
76
|
+
#
|
|
77
|
+
# `docker context use` exports nothing -- it writes currentContext into
|
|
78
|
+
# config.json -- so a client that skips the store disagrees with the
|
|
79
|
+
# CLI on the same machine and falls back to /var/run/docker.sock, which
|
|
80
|
+
# Colima, Rancher Desktop, rootless Docker and Podman generally do not
|
|
81
|
+
# create. The store is only consulted when nothing more explicit has
|
|
82
|
+
# already answered, so this cannot change what an existing caller resolves.
|
|
83
|
+
context = if overrides[:url].nil? && blank?(env["DOCKER_HOST"]) && blank?(env["DOCKER_URL"])
|
|
84
|
+
Context.resolve(env)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# presence, not a bare ||: an exported-but-empty DOCKER_HOST is truthy
|
|
88
|
+
# in Ruby, so it would beat the context and then resolve to the default
|
|
89
|
+
# socket anyway. The CLI treats an empty value as unset.
|
|
90
|
+
url = overrides[:url] || presence(env["DOCKER_HOST"]) || presence(env["DOCKER_URL"]) ||
|
|
91
|
+
context&.[](:url) || default_url
|
|
92
|
+
|
|
93
|
+
tls = if overrides.key?(:tls)
|
|
94
|
+
overrides[:tls]
|
|
95
|
+
else
|
|
96
|
+
from_env = tls_from_env(env)
|
|
97
|
+
from_env.empty? ? (context&.[](:tls) || {}) : from_env
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
new(
|
|
101
|
+
url: url,
|
|
102
|
+
tls: tls,
|
|
103
|
+
api_version: overrides.fetch(:api_version, env["DOCKER_API_VERSION"] || :negotiate),
|
|
104
|
+
read_timeout: overrides.fetch(:read_timeout, 60),
|
|
105
|
+
open_timeout: overrides.fetch(:open_timeout, 10)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [String] the platform's default daemon URL
|
|
110
|
+
def self.default_url
|
|
111
|
+
windows? ? DEFAULT_WINDOWS_PIPE : DEFAULT_UNIX_SOCKET
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
# @api private
|
|
116
|
+
def self.blank?(value)
|
|
117
|
+
value.nil? || value.empty?
|
|
118
|
+
end
|
|
119
|
+
private_class_method :blank?
|
|
120
|
+
|
|
121
|
+
# @return [String, nil] the value, or nil when it is absent or empty
|
|
122
|
+
# @api private
|
|
123
|
+
def self.presence(value)
|
|
124
|
+
blank?(value) ? nil : value
|
|
125
|
+
end
|
|
126
|
+
private_class_method :presence
|
|
127
|
+
|
|
128
|
+
# @return [Boolean]
|
|
129
|
+
def self.windows?
|
|
130
|
+
RbConfig::CONFIG["host_os"].match?(/mswin|mingw|cygwin/)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Build TLS material from `DOCKER_CERT_PATH`, which is the only way the
|
|
134
|
+
# CLI expresses it.
|
|
135
|
+
#
|
|
136
|
+
# `DOCKER_TLS_VERIFY` is a presence flag rather than a boolean: the CLI
|
|
137
|
+
# treats an empty value as "off", so an exported-but-empty variable does
|
|
138
|
+
# not silently start verifying.
|
|
139
|
+
#
|
|
140
|
+
# @param env [Hash]
|
|
141
|
+
# @return [Hash]
|
|
142
|
+
def self.tls_from_env(env)
|
|
143
|
+
cert_path = env["DOCKER_CERT_PATH"]
|
|
144
|
+
return {} if cert_path.nil? || cert_path.empty?
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
ca_file: File.join(cert_path, "ca.pem"),
|
|
148
|
+
cert_file: File.join(cert_path, "cert.pem"),
|
|
149
|
+
key_file: File.join(cert_path, "key.pem"),
|
|
150
|
+
verify: !env["DOCKER_TLS_VERIFY"].to_s.empty?,
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
private_class_method :tls_from_env
|
|
154
|
+
|
|
155
|
+
# @return [String]
|
|
156
|
+
def to_s
|
|
157
|
+
"#<Docker::API::Config url=#{url} tls=#{tls.empty? ? "none" : "configured"} " \
|
|
158
|
+
"api_version=#{api_version}>"
|
|
159
|
+
end
|
|
160
|
+
alias_method :inspect, :to_s
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|