119 lines
5.5 KiB
Markdown
119 lines
5.5 KiB
Markdown
# What Testcontainers Actually Sends
|
|
|
|
Measured, not read off the Engine API documentation: a real
|
|
`PostgreSQLContainer` run recorded between Testcontainers and podman.
|
|
This is the specification for werkdock's API server
|
|
([RFC 0005](rfcs/0005-services-and-testcontainers.md) milestone 3) —
|
|
what it must answer, and what it may leave at 501.
|
|
|
|
## How it was recorded
|
|
|
|
`tools/apirecord` proxies a unix socket to another one and writes one
|
|
JSON line per exchange. Podman serves the same Docker Engine API that
|
|
werkdock is to serve, so its answers are a working reference:
|
|
|
|
```bash
|
|
go run ./tools/apirecord -listen /tmp/rec.sock -upstream $XDG_RUNTIME_DIR/podman/podman.sock -trace trace.jsonl
|
|
```
|
|
|
|
Against it, a plain Java program (Testcontainers 1.19.3, docker-java's
|
|
zerodep transport): start `postgres:16-alpine`, connect over JDBC,
|
|
`select 42`, `execInContainer`, read the logs, close.
|
|
|
|
```bash
|
|
DOCKER_HOST=unix:///tmp/rec.sock TESTCONTAINERS_RYUK_DISABLED=true java ...
|
|
```
|
|
|
|
Recorded on 2026-09-10 against podman 4.9.3, twice: with the image
|
|
absent (24 requests) and present (22). The difference is the pull.
|
|
|
|
## The traffic
|
|
|
|
All paths carry the prefix `/v1.32/`, the API version docker-java asks
|
|
for. In call order, with the counts of the steady-state run:
|
|
|
|
| # | Request | What werkdock answers with |
|
|
|---|---|---|
|
|
| 1 | `GET /info` | one JSON object; identity and host facts |
|
|
| 2 | `GET /version` | `Components`, `ApiVersion`, `Os`, `Arch` (called twice per run) |
|
|
| 3 | `GET /images/json` | `images` |
|
|
| 4 | `GET /images/{ref}/json` | `inspect`; **404 is the signal to pull** |
|
|
| 5 | `POST /images/create?fromImage=…&tag=…` | `pull`, answered as a stream of progress JSON lines |
|
|
| 6 | `POST /containers/create` | `create` |
|
|
| 7 | `POST /containers/{id}/start` | `start`, 204 |
|
|
| 8 | `GET /containers/{id}/json` | instance `inspect` (three times per run) |
|
|
| 9 | `GET /containers/{id}/logs?stdout&stderr&follow&since` | `logs`, in docker's frame format |
|
|
| 10 | `POST /containers/{id}/exec` | creates an exec record, 201 with its id |
|
|
| 11 | `POST /exec/{id}/start` | runs it, streams the output framed |
|
|
| 12 | `GET /exec/{id}/json` | its `ExitCode` |
|
|
| 13 | `POST /containers/{id}/kill` | `kill` |
|
|
| 14 | `DELETE /containers/{id}?v=true&force=true` | `rm -f` |
|
|
| 15 | `GET /containers/json?all=true&filters={"label":…}` | `ps -a` with a label filter |
|
|
| 16 | `POST /networks/prune`, `/volumes/prune`, `/images/prune` | cleanup; an empty result is a valid answer |
|
|
|
|
Notable absences: no `/_ping` (the session opens with `/info`), no
|
|
`/containers/{id}/stop` (Testcontainers kills), no `attach`, no
|
|
`/archive` (no init script in this test), and no Ryuk container
|
|
because `TESTCONTAINERS_RYUK_DISABLED=true` was set. Ryuk would want
|
|
the API socket bind-mounted into a container of its own, which werkdock
|
|
will not offer; disabling it stays a documented requirement.
|
|
|
|
## The four details that decide the implementation
|
|
|
|
**Ports.** `containers/create` asks for
|
|
`"PortBindings":{"5432/tcp":[{"HostIp":"","HostPort":""}]}` — an empty
|
|
`HostPort` means "choose a free one". The answer comes back in
|
|
`containers/{id}/json` as
|
|
`"NetworkSettings":{"Ports":{"5432/tcp":[{"HostIp":"","HostPort":"34259"}]}}`,
|
|
and that number is what `getMappedPort(5432)` returns and what the JDBC
|
|
URL uses. Werkdock has no network namespace, so it cannot map anything:
|
|
the port the service binds *is* the host port. It must therefore pick
|
|
the free port itself and pass it *into* the service, through the
|
|
environment variable the image honours (`PGPORT` for postgres), then
|
|
report that same port. Without this the demo target fails outright, its
|
|
port 5432 being taken by the webspace's own PostgreSQL.
|
|
|
|
**Log framing.** `logs` and `exec/{id}/start` answer
|
|
`application/octet-stream` in docker's multiplexed format: per frame an
|
|
8-byte header, byte 0 the stream (1 stdout, 2 stderr), bytes 4 to 7 the
|
|
payload length big-endian, then the payload. Podman emits one frame per
|
|
line. Werkdock keeps stdout and stderr in separate files, so framing
|
|
them is easy; the exact interleaving of the two streams is lost, which
|
|
no wait strategy depends on.
|
|
|
|
**Exec is not hijacked.** `POST /exec/{id}/start` is a plain request
|
|
with a framed response body, not an HTTP upgrade. No connection
|
|
hijacking is needed. The exit code is fetched afterwards from
|
|
`GET /exec/{id}/json`, so an exec must outlive its run as a record with
|
|
an id.
|
|
|
|
**Labels and filters.** Every container is created with
|
|
`org.testcontainers`, `.lang`, `.version` and `.sessionId` labels, and
|
|
the cleanup at the end lists containers filtered by exactly those
|
|
labels. Instances therefore need labels on disk and `ps` needs a label
|
|
filter.
|
|
|
|
## What werkdock does with it (0.6.0)
|
|
|
|
All of the above is implemented in package `api` and served by
|
|
`werkdock system service`. Two things the trace against podman could
|
|
not show, found when running against werkdock itself:
|
|
|
|
- **The reported version is checked.** Testcontainers refuses a server
|
|
below Docker 1.6.0, so `/version` reports the Engine version whose
|
|
API is served (`api.EngineVersion`), not werkdock's own 0.x.
|
|
Werkdock's version is reported beside it, in `Components` and in
|
|
`/info`'s `OperatingSystem`, where people read it.
|
|
- **`/_ping` is never called** by this client, though it is served
|
|
anyway; other clients open with it.
|
|
|
|
To record a session again, against werkdock this time:
|
|
|
|
```bash
|
|
werkdock system service /tmp/wd.sock &
|
|
go run ./tools/apirecord -listen /tmp/rec.sock -upstream /tmp/wd.sock -trace trace.jsonl
|
|
DOCKER_HOST=unix:///tmp/rec.sock TESTCONTAINERS_RYUK_DISABLED=true java ...
|
|
```
|
|
|
|
A 501 in that trace is the next endpoint to implement.
|