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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/NOTICE +12 -0
  4. data/README.md +278 -0
  5. data/docker-api-ng.gemspec +42 -0
  6. data/docs/building-images.md +98 -0
  7. data/docs/connecting.md +111 -0
  8. data/docs/errors.md +89 -0
  9. data/docs/exec.md +100 -0
  10. data/docs/extending.md +109 -0
  11. data/docs/migrating-from-docker-api.md +156 -0
  12. data/docs/streaming.md +91 -0
  13. data/lib/docker/api/auth.rb +154 -0
  14. data/lib/docker/api/body.rb +47 -0
  15. data/lib/docker/api/client.rb +117 -0
  16. data/lib/docker/api/collection.rb +60 -0
  17. data/lib/docker/api/collections/containers.rb +78 -0
  18. data/lib/docker/api/collections/images.rb +221 -0
  19. data/lib/docker/api/collections/networks.rb +85 -0
  20. data/lib/docker/api/collections/system.rb +98 -0
  21. data/lib/docker/api/collections/volumes.rb +54 -0
  22. data/lib/docker/api/config.rb +163 -0
  23. data/lib/docker/api/connection.rb +333 -0
  24. data/lib/docker/api/context.rb +124 -0
  25. data/lib/docker/api/errors.rb +159 -0
  26. data/lib/docker/api/operations.rb +3064 -0
  27. data/lib/docker/api/path.rb +33 -0
  28. data/lib/docker/api/platform.rb +68 -0
  29. data/lib/docker/api/query.rb +76 -0
  30. data/lib/docker/api/resource.rb +174 -0
  31. data/lib/docker/api/resources/container.rb +378 -0
  32. data/lib/docker/api/resources/exec.rb +48 -0
  33. data/lib/docker/api/resources/image.rb +209 -0
  34. data/lib/docker/api/resources/network.rb +90 -0
  35. data/lib/docker/api/resources/volume.rb +51 -0
  36. data/lib/docker/api/response.rb +110 -0
  37. data/lib/docker/api/session.rb +70 -0
  38. data/lib/docker/api/stream.rb +142 -0
  39. data/lib/docker/api/tar.rb +157 -0
  40. data/lib/docker/api/transport/base.rb +62 -0
  41. data/lib/docker/api/transport/fake.rb +154 -0
  42. data/lib/docker/api/transport/named_pipe.rb +58 -0
  43. data/lib/docker/api/transport/tcp.rb +49 -0
  44. data/lib/docker/api/transport/tls.rb +84 -0
  45. data/lib/docker/api/transport/unix.rb +35 -0
  46. data/lib/docker/api/transport.rb +113 -0
  47. data/lib/docker/api/version.rb +21 -0
  48. data/lib/docker/api.rb +68 -0
  49. data/sig/docker/api/core.rbs +109 -0
  50. data/sig/docker/api/operations.rbs +123 -0
  51. 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