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,111 @@
1
+ # Connecting
2
+
3
+ ## Resolution order
4
+
5
+ `Docker::API::Client.new` with no arguments resolves its daemon the way the
6
+ `docker` CLI does:
7
+
8
+ 1. `DOCKER_HOST`, or `DOCKER_URL`
9
+ 2. the active Docker context — `DOCKER_CONTEXT`, then `currentContext` from
10
+ `~/.docker/config.json`
11
+ 3. `unix:///var/run/docker.sock` on Linux and macOS
12
+ 4. `npipe:////./pipe/docker_engine` on Windows
13
+
14
+ `docker context use` exports nothing; it writes `currentContext` into
15
+ `config.json`. Reading only `DOCKER_HOST` therefore disagrees with the CLI on
16
+ the same machine, and falls back to `/var/run/docker.sock` — which Docker
17
+ Desktop symlinks but Colima, Rancher Desktop, rootless Docker and Podman
18
+ generally do not. Set `DOCKER_CONFIG` to point the lookup somewhere else.
19
+
20
+ A context may carry its own TLS material, which is picked up with it. An
21
+ unreadable store, a malformed `meta.json` or a context that no longer exists
22
+ all mean "no context" rather than an error, because falling back to the
23
+ platform default is what the CLI does.
24
+
25
+ TLS material comes from `DOCKER_CERT_PATH`, which is expected to contain
26
+ `ca.pem`, `cert.pem` and `key.pem`. `DOCKER_TLS_VERIFY` is a presence flag
27
+ rather than a boolean: the CLI treats an exported-but-empty value as off, and
28
+ so does this gem.
29
+
30
+ Explicit arguments always win over the environment.
31
+
32
+ ## URL forms
33
+
34
+ | Form | Transport |
35
+ | --- | --- |
36
+ | `unix:///var/run/docker.sock` | `Transport::Unix` |
37
+ | `/var/run/docker.sock` | `Transport::Unix` — a bare path is a socket |
38
+ | `tcp://host:2375` | `Transport::Tcp` |
39
+ | `tcp://host:2376` with `tls:` | `Transport::Tls` |
40
+ | `https://host:2376` | `Transport::Tls` |
41
+ | `npipe:////./pipe/docker_engine` | `Transport::NamedPipe` |
42
+ | `tcp://` | `Transport::Tcp` at localhost:2375 |
43
+
44
+ ## Version negotiation
45
+
46
+ Docker versions its API by URL prefix: `/v1.55/containers/json`. On first use a
47
+ client pings `/_ping`, reads the `Api-Version` header, and settles on
48
+ `min(what this gem vendors, what the daemon speaks)`.
49
+
50
+ ```ruby
51
+ client = Docker::API::Client.new # negotiate (default)
52
+ client = Docker::API::Client.new(api_version: "1.44") # pin; no ping is sent
53
+ client = Docker::API::Client.new(api_version: :none) # send unprefixed paths
54
+ ```
55
+
56
+ Negotiation happens once per client, not once per request. A daemon older than
57
+ `Docker::API::MIN_API_VERSION` raises `VersionUnsupported` naming both
58
+ versions, at connection time rather than as a confusing 404 later.
59
+
60
+ Pinning is worth doing in CI, where an unexpected daemon upgrade changing
61
+ behaviour underneath a test suite is more disruptive than a version mismatch
62
+ you can see in the diff.
63
+
64
+ ## Timeouts
65
+
66
+ ```ruby
67
+ client = Docker::API::Client.new(read_timeout: 120, open_timeout: 5)
68
+ ```
69
+
70
+ `read_timeout` is the wait for response data, not a deadline for the whole
71
+ request. Long-running streams — `logs(follow: true)`, `system.events`, a slow
72
+ build — are not subject to a total limit, only to a gap between chunks.
73
+
74
+ ## Logging
75
+
76
+ ```ruby
77
+ require "logger"
78
+ client = Docker::API::Client.new(logger: Logger.new($stdout))
79
+ ```
80
+
81
+ One debug line per request, naming the operation, the verb and the resolved
82
+ path. Request bodies are not logged: they routinely carry registry credentials
83
+ and environment variables.
84
+
85
+ ## TLS
86
+
87
+ ```ruby
88
+ client = Docker::API::Client.new(
89
+ url: "tcp://build.internal:2376",
90
+ tls: {
91
+ ca_file: "/certs/ca.pem",
92
+ cert_file: "/certs/cert.pem",
93
+ key_file: "/certs/key.pem",
94
+ verify: true,
95
+ }
96
+ )
97
+ ```
98
+
99
+ The handshake happens in the transport rather than in `Net::HTTP`, so the layer
100
+ above receives an already-encrypted socket and there is one code path for every
101
+ transport. TLS 1.2 is the minimum. `verify: false` exists for self-signed
102
+ development daemons; it is not a default.
103
+
104
+ ## Windows named pipes
105
+
106
+ ```ruby
107
+ client = Docker::API::Client.new(url: "npipe:////./pipe/docker_engine")
108
+ ```
109
+
110
+ Both the forward-slash form that appears in `DOCKER_HOST` and the backslash
111
+ form Windows itself uses are accepted.
data/docs/errors.md ADDED
@@ -0,0 +1,89 @@
1
+ # Errors
2
+
3
+ Everything this gem raises descends from `Docker::API::Error`. Nothing from
4
+ beneath the abstraction escapes: no `Errno`, no `SocketError`, no `OpenSSL`
5
+ exception and no `Net::` class reaches your rescue clause. The original is
6
+ always retained as `#cause`, so nothing is lost.
7
+
8
+ That is a deliberate difference from the `docker-api` gem, where
9
+ `Excon::Error::Socket` reaches callers and couples them to an HTTP library they
10
+ never chose.
11
+
12
+ ## The hierarchy
13
+
14
+ ```
15
+ Docker::API::Error
16
+ ├── ConnectionError the daemon could not be reached at all
17
+ ├── TimeoutError it accepted the connection but did not answer in time
18
+ ├── ClientError 4xx
19
+ │ ├── BadRequest 400
20
+ │ ├── Unauthorized 401
21
+ │ ├── Forbidden 403
22
+ │ ├── NotFound 404
23
+ │ ├── NotModified 304
24
+ │ └── Conflict 409
25
+ ├── ServerError 5xx
26
+ ├── VersionUnsupported the daemon speaks an API version this gem cannot use
27
+ └── StreamError a response stream was malformed or truncated
28
+ ```
29
+
30
+ `NotModified` is grouped with the client errors rather than treated as a
31
+ redirect because that is the meaning Docker gives it: starting an
32
+ already-running container returns 304.
33
+
34
+ ## What an error carries
35
+
36
+ ```ruby
37
+ begin
38
+ client.containers.get("missing")
39
+ rescue Docker::API::Error => e
40
+ e.operation #=> "container_inspect"
41
+ e.status #=> 404
42
+ e.response #=> #<Docker::API::Response status=404 bytes=42>
43
+ e.message #=> "container_inspect failed (HTTP 404): No such container: missing"
44
+ e.cause #=> the underlying exception, when there was one
45
+ end
46
+ ```
47
+
48
+ The message is composed from what was attempted, the status, and the daemon's
49
+ own `message` field. When the body is not JSON the raw body is used instead,
50
+ because an unparsed body still helps a human more than nothing does.
51
+
52
+ ## Rescuing well
53
+
54
+ Rescue the narrowest thing that describes what you are handling:
55
+
56
+ ```ruby
57
+ # Absence is a normal answer to a question.
58
+ container = client.containers.find("web") # nil rather than an exception
59
+
60
+ # A name collision usually means somebody else won a race.
61
+ begin
62
+ client.networks.create("shared")
63
+ rescue Docker::API::Conflict
64
+ client.networks.get("shared")
65
+ end
66
+
67
+ # Connection and timeout failures are the ones worth retrying.
68
+ begin
69
+ client.system.info
70
+ rescue Docker::API::ConnectionError, Docker::API::TimeoutError => e
71
+ retry if (attempts += 1) < 3
72
+ raise
73
+ end
74
+ ```
75
+
76
+ Retrying a `ClientError` is almost always wrong: the request was malformed or
77
+ the object was not there, and it will not be there in a second.
78
+
79
+ ## Build failures are not HTTP failures
80
+
81
+ The daemon reports a failed build inside a `200` response, as an `error` key in
82
+ the JSON-lines stream. Anything that only checks the status code concludes the
83
+ build succeeded. `Images#build` reads the stream and raises, so a build failure
84
+ is an exception like any other.
85
+
86
+ ## Streaming errors
87
+
88
+ When a streaming request fails, the error response is buffered and raised
89
+ rather than handed to your block. Your block only ever sees output.
data/docs/exec.md ADDED
@@ -0,0 +1,100 @@
1
+ # Running commands in containers
2
+
3
+ ## exec
4
+
5
+ ```ruby
6
+ result = container.exec(%w{chef-client -z})
7
+
8
+ result.stdout #=> "Starting Chef Infra Client...\n"
9
+ result.stderr #=> ""
10
+ result.exit_code #=> 0
11
+ result.success? #=> true
12
+ result.output #=> stdout and stderr together
13
+ ```
14
+
15
+ `exec` creates an exec instance, starts it, reads the stream to completion, and
16
+ then asks the daemon for the exit code. All four steps are one call.
17
+
18
+ ### Streaming as it runs
19
+
20
+ ```ruby
21
+ container.exec(%w{make test}) do |stream, chunk|
22
+ logger << chunk if stream == :stdout
23
+ end
24
+ ```
25
+
26
+ The block receives chunks as they arrive, and the returned `ExecResult` still
27
+ carries the complete output. Both are available; you do not have to choose.
28
+
29
+ ### Options
30
+
31
+ ```ruby
32
+ container.exec(
33
+ %w{bundle exec rake},
34
+ env: { "RAILS_ENV" => "test" },
35
+ user: "app",
36
+ working_dir: "/srv/app",
37
+ tty: false,
38
+ privileged: false
39
+ )
40
+ ```
41
+
42
+ `tty: true` asks the daemon for a terminal, which means output arrives
43
+ unframed. Everything is then reported as `:stdout`, because with a TTY there is
44
+ genuinely only one stream.
45
+
46
+ ### Failure is returned, not raised
47
+
48
+ A non-zero exit is a result, not an exception. Whether a failing command is an
49
+ error depends entirely on why it was run — a test runner expects failures, a
50
+ provisioning step does not — so the decision stays with the caller:
51
+
52
+ ```ruby
53
+ result = container.exec(%w{rspec})
54
+ warn "tests failed" unless result.success?
55
+
56
+ container.exec(%w{apt-get update}).check! # raises when this one fails
57
+ ```
58
+
59
+ `check!` raises a `Docker::API::Error` whose message includes the exit code and
60
+ whatever the command said.
61
+
62
+ ## Logs
63
+
64
+ ```ruby
65
+ container.logs # everything, as a string
66
+ container.logs(tail: 100)
67
+ container.logs(since: Time.now.to_i - 3600)
68
+ container.logs(timestamps: true)
69
+
70
+ container.logs(follow: true) do |stream, chunk|
71
+ $stdout << chunk
72
+ end
73
+ ```
74
+
75
+ Without a block the whole log is returned. With one, chunks are yielded as they
76
+ arrive. `follow: true` does not end on its own — break out of the block.
77
+
78
+ ## Attach
79
+
80
+ `exec` runs a new command. `attach` connects to the container's own process,
81
+ and returns the socket so you can write to its stdin:
82
+
83
+ ```ruby
84
+ io = container.attach(stdin: true, stdout: true, stderr: true)
85
+ io.write("ls /\n")
86
+ puts io.readpartial(4096)
87
+ io.close
88
+ ```
89
+
90
+ ## Copying files
91
+
92
+ ```ruby
93
+ archive = Docker::API::Tar.pack_dockerfile("unused", files: { "config.yml" => yaml })
94
+ container.archive_in(archive.read, path: "/etc/app")
95
+
96
+ tar_bytes = container.archive_out("/var/log/app.log")
97
+ ```
98
+
99
+ Both take and produce tar archives, which is the only format the daemon's copy
100
+ endpoints speak.
data/docs/extending.md ADDED
@@ -0,0 +1,109 @@
1
+ # Extending and upgrading
2
+
3
+ ## How the generated layer is made
4
+
5
+ `data/swagger/v1.55.yaml` is a vendored copy of Docker's own Swagger 2.0
6
+ definition of the Engine API. Three emitters read it:
7
+
8
+ | Emitter | Output |
9
+ | --- | --- |
10
+ | `tools/generator/operations_emitter.rb` | `lib/docker/api/operations.rb` |
11
+ | `tools/generator/conformance_emitter.rb` | `spec/generated/operations_conformance_spec.rb` |
12
+ | `tools/generator/rbs_emitter.rb` | `sig/docker/api/operations.rbs` |
13
+
14
+ ```console
15
+ $ bundle exec rake api:generate
16
+ ```
17
+
18
+ The output is plain, committed Ruby — no `define_method`, no runtime
19
+ dispatch. Every method is spelled out so it can be read, grepped, documented by
20
+ YARD and stepped through in a debugger. A metaprogrammed layer would be shorter
21
+ to generate and considerably worse to live with.
22
+
23
+ Because the output is committed, installing the gem needs no toolchain.
24
+
25
+ ## Upgrading to a new API version
26
+
27
+ ```console
28
+ $ bundle exec rake api:sync[1.56]
29
+ $ git diff --stat
30
+ ```
31
+
32
+ `api:sync` fetches the specification, updates `MAX_API_VERSION`, regenerates
33
+ everything, and prints the diff. Then read it:
34
+
35
+ - A new endpoint appears as a new method.
36
+ - A new parameter appears in a signature and in the RBS.
37
+ - A removed parameter vanishes, and `steep check` names every caller.
38
+ - A changed path or verb shows up as a failing conformance test.
39
+
40
+ `rake api:verify` fails if the committed files do not match what the
41
+ specification currently produces, so CI catches a hand-edit or a forgotten
42
+ regeneration.
43
+
44
+ ## Adding an ergonomic wrapper
45
+
46
+ The generated layer is complete. The ergonomic layer is sugar, and it grows
47
+ when sugar is warranted — when there is normalisation to do, several calls to
48
+ sequence, or a stream to decode.
49
+
50
+ A wrapper is a thin call into `client.operations`:
51
+
52
+ ```ruby
53
+ module Docker
54
+ module API
55
+ class Containers < Collection
56
+ def restart_all(filters: nil)
57
+ all(filters: filters).each(&:restart)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ Two rules keep the layers honest:
65
+
66
+ 1. **The ergonomic layer never talks to the connection directly.** It goes
67
+ through `operations`, so there is one place where requests are made.
68
+ 2. **It never hides capability.** If a wrapper cannot express something the
69
+ endpoint supports, that is a reason to widen the wrapper, not a reason for
70
+ callers to have no way to say it.
71
+
72
+ ## Adding a transport
73
+
74
+ A transport makes a socket. That is the whole contract:
75
+
76
+ ```ruby
77
+ class MyTransport < Docker::API::Transport::Base
78
+ def connect
79
+ dial("my://endpoint") { SomeSocket.new(...) }
80
+ end
81
+
82
+ def host_header
83
+ "localhost"
84
+ end
85
+ end
86
+
87
+ Docker::API::Client.new(transport: MyTransport.new)
88
+ ```
89
+
90
+ `dial` converts every way the operating system reports failure into
91
+ `Docker::API::ConnectionError`, which is what keeps `Errno` out of callers'
92
+ rescue clauses.
93
+
94
+ ## Where the parameter names come from
95
+
96
+ Method and parameter names are derived from the specification's `operationId`
97
+ and parameter names:
98
+
99
+ - `ContainerCreate` becomes `container_create`
100
+ - `fromImage` becomes `from_image`
101
+ - `one-shot` becomes `one_shot`
102
+ - `X-Registry-Auth` becomes `x_registry_auth`
103
+ - `until` becomes `until_`, because Ruby will accept `def f(until: nil)` and
104
+ then refuse to parse any reference to it
105
+ - Every body parameter becomes `body`, whatever the specification calls it —
106
+ there are thirteen different names in v1.55
107
+
108
+ Renamed parameters say so in their generated documentation, naming the spelling
109
+ that goes on the wire.
@@ -0,0 +1,156 @@
1
+ # Migrating from the docker-api gem
2
+
3
+ This gem keeps no compatibility with
4
+ [docker-api](https://github.com/upserve/docker-api). Everything lives under
5
+ `Docker::API` and nothing is added to `::Docker`, so **both gems can be loaded
6
+ into one process** while a migration is in progress. There is no flag day.
7
+
8
+ ## The shape of the change
9
+
10
+ | docker-api | docker-api-ng |
11
+ | --- | --- |
12
+ | Class methods plus a connection argument | Everything hangs off a client |
13
+ | `Docker.url = host` (process-global) | `Client.new(url: host)` (per client) |
14
+ | `Docker.creds = ...` (process-global) | Resolved per call from `~/.docker/config.json` |
15
+ | `Docker::Error::DockerError` | `Docker::API::Error` |
16
+ | `Excon::Error::Socket` reaches you | `Docker::API::ConnectionError`, cause preserved |
17
+ | `.info` — shape depends on the call | Normalised accessors, plus `#raw` |
18
+
19
+ ## Call by call
20
+
21
+ ### The daemon
22
+
23
+ | docker-api | docker-api-ng |
24
+ | --- | --- |
25
+ | `Docker.info(conn)` | `client.system.info` |
26
+ | `Docker.version(conn)` | `client.system.version` |
27
+ | `Docker.ping(conn)` | `client.system.ping?` |
28
+ | `Docker.authenticate!(creds)` | `client.system.authenticate(username:, password:)` |
29
+ | `Docker.options` / `Docker.url=` | `Docker::API::Client.new(url:, tls:)` |
30
+ | `Docker::Connection.new(url, opts)` | `Docker::API::Client.new(url:, tls:)` |
31
+
32
+ ### Containers
33
+
34
+ | docker-api | docker-api-ng |
35
+ | --- | --- |
36
+ | `Docker::Container.get(name, {}, conn)` | `client.containers.get(name)` |
37
+ | `Docker::Container.all({ all: true }, conn)` | `client.containers.all(all: true)` |
38
+ | `Docker::Container.create(args, conn)` | `client.containers.create(**args)` |
39
+ | `container.start` | `container.start` |
40
+ | `container.stop` | `container.stop(timeout:)` |
41
+ | `container.delete(force: true)` | `container.remove(force: true)` |
42
+ | `container.json` | `container.raw`, or a named accessor |
43
+ | `container.exec(cmd, wait:, "e" => env) { }` | `container.exec(cmd, env: env) { }` |
44
+ | `container.info["Names"].include?("/x")` | `container.name == "x"` |
45
+
46
+ `exec` returns a `Docker::API::ExecResult` with `#stdout`, `#stderr` and
47
+ `#exit_code`, rather than a three-element array:
48
+
49
+ ```ruby
50
+ # docker-api
51
+ out = container.exec(cmd, wait: 60) { |_stream, chunk| logger << chunk }
52
+ raise if out[2] != 0
53
+
54
+ # docker-api-ng
55
+ result = container.exec(cmd) { |_stream, chunk| logger << chunk }
56
+ raise unless result.success?
57
+ ```
58
+
59
+ ### Images
60
+
61
+ | docker-api | docker-api-ng |
62
+ | --- | --- |
63
+ | `Docker::Image.exist?(name, {}, conn)` | `client.images.exist?(name)` |
64
+ | `Docker::Image.get(name, {}, conn)` | `client.images.get(name)` |
65
+ | `Docker::Image.create({ "fromImage" => x }, creds, conn)` | `client.images.pull(x)` |
66
+ | `Docker::Image.build(dockerfile, opts, conn)` | `client.images.build(dockerfile:, tag:)` |
67
+ | `Docker::Image.build_from_dir(dir, opts)` | `client.images.build(context: dir, tag:)` |
68
+ | `image.tag("repo" => r, "tag" => t)` | `image.tag("#{r}:#{t}")` |
69
+ | `image.remove` | `image.remove` |
70
+
71
+ ### Networks and volumes
72
+
73
+ | docker-api | docker-api-ng |
74
+ | --- | --- |
75
+ | `Docker::Network.get(name, {}, conn)` | `client.networks.get(name)` |
76
+ | `Docker::Network.create(name, settings)` | `client.networks.create(name, **settings)` |
77
+ | get-or-create with a rescue | `client.networks.ensure(name)` |
78
+ | `network.info["EnableIPv6"]` | `network.ipv6?` |
79
+ | `Docker::Volume.create(name)` | `client.volumes.create(name)` |
80
+
81
+ ### Errors
82
+
83
+ | docker-api | docker-api-ng |
84
+ | --- | --- |
85
+ | `Docker::Error::DockerError` | `Docker::API::Error` |
86
+ | `Docker::Error::NotFoundError` | `Docker::API::NotFound` |
87
+ | `Docker::Error::ConflictError` | `Docker::API::Conflict` |
88
+ | `Docker::Error::ServerError` | `Docker::API::ServerError` |
89
+ | `Docker::Error::UnexpectedResponseError` | `Docker::API::BadRequest` |
90
+ | `Docker::Error::TimeoutError` | `Docker::API::TimeoutError` |
91
+ | `Docker::Error::IOError` | `Docker::API::ConnectionError` |
92
+ | `Excon::Error::Socket` | `Docker::API::ConnectionError` |
93
+
94
+ ## Workarounds you can now delete
95
+
96
+ ### The dropped `platform` parameter
97
+
98
+ `Docker::Container.create` forwards only `name` to the query string, so
99
+ `platform` never reaches the daemon. The usual workaround is to create the
100
+ container and re-fetch it:
101
+
102
+ ```ruby
103
+ # docker-api
104
+ Docker::Container.create(args, conn)
105
+ Docker::Container.get(args["name"], {}, conn)
106
+
107
+ # docker-api-ng
108
+ client.containers.create(name: name, platform: "linux/arm64", **args)
109
+ ```
110
+
111
+ ### Listing everything to find one container
112
+
113
+ `Docker::Container.get` is unreliable in docker-api 2.0.0, and the workaround
114
+ is to list every container and match by name:
115
+
116
+ ```ruby
117
+ # docker-api
118
+ found = Docker::Container.all({ all: true }, conn)
119
+ .select { |c| c.info["Names"].include?("/#{name}") }
120
+ raise Docker::Error::NotFoundError if found.empty?
121
+
122
+ # docker-api-ng
123
+ client.containers.get(name)
124
+ ```
125
+
126
+ ### Per-host caching around global state
127
+
128
+ Because `Docker.url=` is process-global, code that talks to more than one
129
+ daemon has to cache results per host to avoid handing the second caller the
130
+ first daemon's answers. Two clients need no such care:
131
+
132
+ ```ruby
133
+ local = Docker::API::Client.new
134
+ build = Docker::API::Client.new(url: "tcp://build:2376")
135
+ ```
136
+
137
+ ### Rescuing the HTTP library
138
+
139
+ ```ruby
140
+ # docker-api
141
+ rescue Excon::Error::Socket => e
142
+
143
+ # docker-api-ng
144
+ rescue Docker::API::ConnectionError => e
145
+ e.cause # the Errno, if you actually want it
146
+ ```
147
+
148
+ ## Anything not listed here
149
+
150
+ The complete Engine API is available on `client.operations`, named after the
151
+ specification's own operation ids:
152
+
153
+ ```ruby
154
+ client.operations.container_prune(filters: { "until" => ["24h"] })
155
+ client.operations.image_history(name: "alpine:3.20")
156
+ ```
data/docs/streaming.md ADDED
@@ -0,0 +1,91 @@
1
+ # Streaming
2
+
3
+ The Engine API streams in three different wire formats, and which one you get
4
+ depends on the endpoint and on how the container was created.
5
+
6
+ ## The formats
7
+
8
+ | Format | Where it appears | Decoder |
9
+ | --- | --- | --- |
10
+ | Multiplexed | `logs`, `exec`, `attach` on a container with no TTY | `Stream::Demultiplexer` |
11
+ | Raw | the same endpoints when the container has a TTY | `Stream::Raw` |
12
+ | JSON lines | `pull`, `push`, `build`, `/events` | `Stream::JSONLines` |
13
+
14
+ The ergonomic layer picks the right decoder for you. `Container#logs` and
15
+ `Container#exec` check whether the container has a TTY, because that is what
16
+ decides between the first two.
17
+
18
+ ## Multiplexed framing
19
+
20
+ Without a TTY, stdout and stderr share one connection and are separated by an
21
+ eight-byte header on every frame: a stream id, three bytes of padding, then a
22
+ big-endian payload length.
23
+
24
+ ```
25
+ +--------+--------+--------+--------+--------+--------+--------+--------+
26
+ | stream | 0 | 0 | 0 | payload length |
27
+ +--------+--------+--------+--------+--------+--------+--------+--------+
28
+ ```
29
+
30
+ ```ruby
31
+ container.exec(%w{make test}) do |stream, chunk|
32
+ case stream
33
+ when :stdout then $stdout << chunk
34
+ when :stderr then $stderr << chunk
35
+ end
36
+ end
37
+ ```
38
+
39
+ ## Why the decoders buffer
40
+
41
+ HTTP delivers chunks at byte boundaries that have nothing to do with Docker's
42
+ framing. A frame header can be split across two chunks; a JSON object can be
43
+ split mid-string. Every decoder here accumulates and emits only complete units.
44
+
45
+ A decoder that assumes a chunk is a message passes its tests — where chunks
46
+ happen to align — and interleaves garbage in production. It is worth knowing
47
+ this is handled if you are ever tempted to read the raw stream yourself.
48
+
49
+ ## JSON-lines progress
50
+
51
+ ```ruby
52
+ client.images.pull("alpine:3.20") do |event|
53
+ puts "#{event["status"]} #{event["progress"]}"
54
+ end
55
+
56
+ client.images.build(context: ".", tag: "app:dev") do |event|
57
+ print event["stream"] if event["stream"]
58
+ end
59
+ ```
60
+
61
+ Build streams carry the resulting image id in an `aux` object near the end, and
62
+ report failures in an `error` key. `Images#build` reads both.
63
+
64
+ ## Endless streams
65
+
66
+ `system.events` and `logs(follow: true)` do not end on their own. Break out of
67
+ the block, or run them on a thread you can stop.
68
+
69
+ ```ruby
70
+ client.system.events(filters: { "type" => ["container"] }) do |event|
71
+ break if event["Action"] == "die"
72
+ end
73
+ ```
74
+
75
+ `read_timeout` applies to the gap between chunks, not to the total duration, so
76
+ a quiet stream will not be cut off mid-flow.
77
+
78
+ ## Taking the socket
79
+
80
+ For genuinely interactive work — sending stdin — `Container#attach` returns the
81
+ bidirectional socket itself:
82
+
83
+ ```ruby
84
+ io = container.attach(stdin: true, stdout: true, stderr: true)
85
+ io.write("echo hello\n")
86
+ puts io.readpartial(4096)
87
+ io.close
88
+ ```
89
+
90
+ This path bypasses `Net::HTTP` deliberately, so the socket comes back with
91
+ nothing consumed but the response head.