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
data/docs/connecting.md
ADDED
|
@@ -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.
|