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,33 @@
|
|
|
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
|
+
# Escaping for values interpolated into a request path.
|
|
9
|
+
module Path
|
|
10
|
+
# Characters that may appear unescaped in a path. This is RFC 3986's
|
|
11
|
+
# `pchar` set plus the separator itself.
|
|
12
|
+
#
|
|
13
|
+
# Keeping "/" and ":" unescaped is the point. Image references are
|
|
14
|
+
# `registry.example.com/team/image:tag`, and the daemon's router matches
|
|
15
|
+
# those slashes as path structure -- percent-encoding them turns a valid
|
|
16
|
+
# reference into a 404. Everything genuinely unsafe (spaces, "?", "#",
|
|
17
|
+
# "%") is still escaped.
|
|
18
|
+
SAFE = %r{[^A-Za-z0-9\-._~!$&'()*+,;=:@/]}
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# @param value [Object] a value being interpolated into a path
|
|
23
|
+
# @return [String] the value, safe to place in a path
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# Path.escape("registry.io/team/img:1.0") #=> "registry.io/team/img:1.0"
|
|
27
|
+
# Path.escape("weird name") #=> "weird%20name"
|
|
28
|
+
def escape(value)
|
|
29
|
+
URI::DEFAULT_PARSER.escape(value.to_s, SAFE)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
# Platform selectors, in the two encodings the Engine API uses for them.
|
|
9
|
+
#
|
|
10
|
+
# The API is not consistent here, and the inconsistency is silent. Some
|
|
11
|
+
# endpoints want the familiar string:
|
|
12
|
+
#
|
|
13
|
+
# POST /containers/create?platform=linux/arm64
|
|
14
|
+
# POST /build?platform=linux/arm64
|
|
15
|
+
# POST /images/create?platform=linux/arm64
|
|
16
|
+
#
|
|
17
|
+
# while others want a JSON-encoded OCI platform object:
|
|
18
|
+
#
|
|
19
|
+
# GET /images/{name}/json?platform={"os":"linux","architecture":"arm64"}
|
|
20
|
+
# POST /images/{name}/push?platform={"os":"linux","architecture":"arm64"}
|
|
21
|
+
# GET /images/{name}/history?platform={...}
|
|
22
|
+
#
|
|
23
|
+
# Sending the string where the object is expected fails with
|
|
24
|
+
# `400 failed to parse platform: invalid character 'l'`, which does not
|
|
25
|
+
# obviously mean "wrong encoding" to anyone reading it for the first time.
|
|
26
|
+
#
|
|
27
|
+
# Callers of this gem write `"linux/arm64"` everywhere and the ergonomic
|
|
28
|
+
# layer encodes whichever form the endpoint in question wants.
|
|
29
|
+
module Platform
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Parse a platform into its OCI object form.
|
|
33
|
+
#
|
|
34
|
+
# @param value [String, Hash, nil] "os/arch", "os/arch/variant", an
|
|
35
|
+
# already-built OCI hash, or nil
|
|
36
|
+
# @return [Hash, nil] with "os", "architecture" and optionally "variant"
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# Platform.oci("linux/arm64") #=> {"os"=>"linux", "architecture"=>"arm64"}
|
|
40
|
+
# Platform.oci("linux/arm/v7") #=> {"os"=>"linux", "architecture"=>"arm", "variant"=>"v7"}
|
|
41
|
+
def oci(value)
|
|
42
|
+
return nil if value.nil?
|
|
43
|
+
return value if value.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
os, architecture, variant = value.to_s.split("/", 3)
|
|
46
|
+
return nil if os.nil? || os.empty?
|
|
47
|
+
|
|
48
|
+
{ "os" => os, "architecture" => architecture, "variant" => variant }.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Render a platform in the `os[/arch[/variant]]` string form.
|
|
52
|
+
#
|
|
53
|
+
# @param value [String, Hash, nil]
|
|
54
|
+
# @return [String, nil]
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# Platform.string("os" => "linux", "architecture" => "arm64") #=> "linux/arm64"
|
|
58
|
+
def string(value)
|
|
59
|
+
return nil if value.nil?
|
|
60
|
+
return value if value.is_a?(String)
|
|
61
|
+
|
|
62
|
+
[value["os"] || value[:os],
|
|
63
|
+
value["architecture"] || value[:architecture],
|
|
64
|
+
value["variant"] || value[:variant]].compact.join("/")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
# Turns Ruby values into the query strings the Engine API expects.
|
|
9
|
+
#
|
|
10
|
+
# Docker's conventions are specific enough to be worth centralising, and
|
|
11
|
+
# they are not uniform:
|
|
12
|
+
#
|
|
13
|
+
# * Booleans travel as the literal strings "true" and "false".
|
|
14
|
+
# * Structured parameters such as `filters` are declared in the API
|
|
15
|
+
# specification as strings holding a JSON document, so a Hash becomes one
|
|
16
|
+
# JSON-encoded value.
|
|
17
|
+
# * A handful of parameters -- the `platform` and `type` parameters of the
|
|
18
|
+
# image and system endpoints -- are declared as arrays with
|
|
19
|
+
# `collectionFormat: multi`, so an Array becomes a repeated key rather
|
|
20
|
+
# than one JSON value.
|
|
21
|
+
#
|
|
22
|
+
# Getting this wrong does not raise: the daemon ignores a parameter it
|
|
23
|
+
# cannot parse and quietly does something else, which is the failure mode
|
|
24
|
+
# that makes it worth encoding in one place.
|
|
25
|
+
module Query
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Encode a parameter hash as a URL query fragment.
|
|
29
|
+
#
|
|
30
|
+
# Nil values are dropped rather than sent empty, because an empty value
|
|
31
|
+
# is not the same as an absent one to the daemon: `?all=` is a parse
|
|
32
|
+
# error where omitting `all` is a default.
|
|
33
|
+
#
|
|
34
|
+
# @param params [Hash] parameters in Ruby types
|
|
35
|
+
# @return [String] a fragment beginning with "?", or "" when nothing
|
|
36
|
+
# survived, so callers can concatenate unconditionally
|
|
37
|
+
#
|
|
38
|
+
# @example A boolean and a JSON-encoded filter
|
|
39
|
+
# Query.encode(all: true, filters: { "status" => ["running"] })
|
|
40
|
+
# #=> "?all=true&filters=%7B%22status%22%3A%5B%22running%22%5D%7D"
|
|
41
|
+
#
|
|
42
|
+
# @example An array parameter, repeated rather than JSON-encoded
|
|
43
|
+
# Query.encode(platform: ["linux/amd64", "linux/arm64"])
|
|
44
|
+
# #=> "?platform=linux%2Famd64&platform=linux%2Farm64"
|
|
45
|
+
def encode(params)
|
|
46
|
+
pairs = (params || {}).reject { |_, value| value.nil? }
|
|
47
|
+
.flat_map { |key, value| pairs_for(key.to_s, value) }
|
|
48
|
+
|
|
49
|
+
return "" if pairs.empty?
|
|
50
|
+
|
|
51
|
+
"?#{URI.encode_www_form(pairs)}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param key [String] the wire parameter name
|
|
55
|
+
# @param value [Object] the Ruby value
|
|
56
|
+
# @return [Array<Array(String, String)>] one or more key/value pairs
|
|
57
|
+
# @api private
|
|
58
|
+
def pairs_for(key, value)
|
|
59
|
+
return value.map { |element| [key, serialize(element)] } if value.is_a?(Array)
|
|
60
|
+
|
|
61
|
+
[[key, serialize(value)]]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param value [Object]
|
|
65
|
+
# @return [String] the daemon's expected wire form for a single value
|
|
66
|
+
# @api private
|
|
67
|
+
def serialize(value)
|
|
68
|
+
case value
|
|
69
|
+
when true, false then value.to_s
|
|
70
|
+
when Hash then JSON.generate(value)
|
|
71
|
+
else value.to_s
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
# Shared behaviour for the objects the daemon describes: containers,
|
|
9
|
+
# images, networks and volumes.
|
|
10
|
+
#
|
|
11
|
+
# The Engine API returns different shapes for the same object depending on
|
|
12
|
+
# how you asked. `GET /containers/json` answers with `Names: ["/web"]`;
|
|
13
|
+
# `GET /containers/{id}/json` answers with `Name: "/web"`. A client that
|
|
14
|
+
# simply exposes whichever payload it happened to receive pushes that
|
|
15
|
+
# difference onto every caller, which is why code written against such a
|
|
16
|
+
# client ends up reading `info["Names"]` in one place and `[:Name]` in
|
|
17
|
+
# another and breaking when the two are swapped.
|
|
18
|
+
#
|
|
19
|
+
# Resources here normalise. {#name} answers the same thing regardless of
|
|
20
|
+
# origin, and an object built from a list marks itself {#partial?}: the
|
|
21
|
+
# first accessor that needs detail the list did not carry fetches it once,
|
|
22
|
+
# rather than returning nil and letting the caller guess why.
|
|
23
|
+
#
|
|
24
|
+
# @abstract Subclass and implement {#reload}.
|
|
25
|
+
class Resource
|
|
26
|
+
# @return [Docker::API::Client] the client this resource came from
|
|
27
|
+
attr_reader :client
|
|
28
|
+
|
|
29
|
+
# @param client [Docker::API::Client] the client to issue further calls on
|
|
30
|
+
# @param raw [Hash] the daemon's payload
|
|
31
|
+
# @param partial [Boolean] whether the payload came from a list endpoint
|
|
32
|
+
def initialize(client:, raw:, partial: false)
|
|
33
|
+
@client = client
|
|
34
|
+
@raw = raw || {}
|
|
35
|
+
@partial = partial
|
|
36
|
+
@reloaded = false
|
|
37
|
+
@stale = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The daemon's payload, exactly as it arrived.
|
|
41
|
+
#
|
|
42
|
+
# Always available, so nothing this gem does not model is out of reach.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash]
|
|
45
|
+
attr_reader :raw
|
|
46
|
+
|
|
47
|
+
# @return [Boolean] whether this came from a list and may lack detail
|
|
48
|
+
def partial?
|
|
49
|
+
@partial
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [String, nil] the object's id
|
|
53
|
+
def id
|
|
54
|
+
raw["Id"] || raw["ID"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Read a key straight from the payload, with no normalisation.
|
|
58
|
+
#
|
|
59
|
+
# @param key [String] a key as the daemon spells it
|
|
60
|
+
# @return [Object, nil]
|
|
61
|
+
def [](key)
|
|
62
|
+
raw[key]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Boolean] whether an action has changed this object since the
|
|
66
|
+
# payload in hand was fetched
|
|
67
|
+
def stale?
|
|
68
|
+
@stale
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Re-fetch this object from the daemon.
|
|
72
|
+
#
|
|
73
|
+
# @return [self]
|
|
74
|
+
def reload
|
|
75
|
+
raise NotImplementedError, "#{self.class} must implement #reload"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [String]
|
|
79
|
+
def to_s
|
|
80
|
+
"#<#{self.class.name} id=#{id.to_s[0, 12]}#{partial? ? " partial" : ""}>"
|
|
81
|
+
end
|
|
82
|
+
alias_method :inspect, :to_s
|
|
83
|
+
|
|
84
|
+
# @param other [Object]
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def ==(other)
|
|
87
|
+
other.is_a?(self.class) && !id.nil? && id == other.id
|
|
88
|
+
end
|
|
89
|
+
alias_method :eql?, :==
|
|
90
|
+
|
|
91
|
+
# @return [Integer]
|
|
92
|
+
def hash
|
|
93
|
+
[self.class, id].hash
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Read the first of several possible keys, fetching detail if needed.
|
|
99
|
+
#
|
|
100
|
+
# Every key is tried against the payload already in hand before any
|
|
101
|
+
# request is considered. That ordering is the whole point: a container
|
|
102
|
+
# from a list carries its name under "Names", so asking for
|
|
103
|
+
# `detail("Name", "Names")` must answer from memory rather than spending
|
|
104
|
+
# a round trip rediscovering what it was already told.
|
|
105
|
+
#
|
|
106
|
+
# A partial resource reloads at most once, no matter how many accessors
|
|
107
|
+
# ask for something it does not have. A complete resource never reloads:
|
|
108
|
+
# if the daemon did not report the field, it is genuinely absent, and a
|
|
109
|
+
# second identical request would only confirm that more slowly.
|
|
110
|
+
#
|
|
111
|
+
# @param keys [Array<String>] keys to try, in order of preference
|
|
112
|
+
# @return [Object, nil]
|
|
113
|
+
def detail(*keys)
|
|
114
|
+
refresh_if_stale
|
|
115
|
+
found = first_present(keys)
|
|
116
|
+
return found unless found.nil?
|
|
117
|
+
return nil unless partial? && !@reloaded
|
|
118
|
+
|
|
119
|
+
@reloaded = true
|
|
120
|
+
reload
|
|
121
|
+
first_present(keys)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Note that an action just changed this object on the daemon.
|
|
125
|
+
#
|
|
126
|
+
# Starting a container does not update the payload we are holding, so
|
|
127
|
+
# without this a caller who starts a container and then asks its state is
|
|
128
|
+
# told "created" -- true when we fetched it, and useless now. The refresh
|
|
129
|
+
# is deferred rather than immediate, so an action followed by no question
|
|
130
|
+
# still costs one request rather than two.
|
|
131
|
+
#
|
|
132
|
+
# @return [self]
|
|
133
|
+
def mark_stale
|
|
134
|
+
@stale = true
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [void]
|
|
139
|
+
def refresh_if_stale
|
|
140
|
+
return unless @stale
|
|
141
|
+
|
|
142
|
+
@stale = false
|
|
143
|
+
@reloaded = false
|
|
144
|
+
reload
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @param keys [Array<String>]
|
|
148
|
+
# @return [Object, nil]
|
|
149
|
+
def first_present(keys)
|
|
150
|
+
keys.each do |key|
|
|
151
|
+
value = key.include?(".") ? raw.dig(*key.split(".")) : raw[key]
|
|
152
|
+
return value unless value.nil?
|
|
153
|
+
end
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Replace the payload after a re-fetch.
|
|
158
|
+
#
|
|
159
|
+
# @param payload [Hash]
|
|
160
|
+
# @return [self]
|
|
161
|
+
def replace_raw(payload)
|
|
162
|
+
@raw = payload || {}
|
|
163
|
+
@partial = false
|
|
164
|
+
@stale = false
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @return [Docker::API::Operations]
|
|
169
|
+
def operations
|
|
170
|
+
client.operations
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|