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,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