werkator build succeeded after 00:08
Werkprobe now drives werkdock through Testcontainers' JDBC driver as
well as programmatically, with a Spring Boot application whose
application.yml holds the URLs. It asked for no endpoint outside the
subset, so this is documentation only, no version bump.
Two corrections come with it. A JDBC init script is not a reason to
want PUT /containers/{id}/archive: Testcontainers runs it over the
connection. And the subset now has 1.21.4 behind it as well as 1.19.3,
which says it did not depend on that release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.3 KiB
Markdown
137 lines
5.3 KiB
Markdown
# Testcontainers against Werkdock
|
|
|
|
Werkdock serves the Docker Engine API subset Testcontainers uses, so a
|
|
test that asks for a real PostgreSQL gets one — without docker, without
|
|
podman, without root. Validated with Testcontainers 1.19.3 and 1.21.4
|
|
and `postgres:16-alpine`, on a developer machine and on a Hostsharing
|
|
managed webspace (RFC 0005 milestones 3 and 4), both programmatically
|
|
and through `jdbc:tc` URLs.
|
|
|
|
## Setting it up
|
|
|
|
```bash
|
|
werkdock pull postgres:16-alpine # once; needs the crane binary
|
|
werkdock system service & # listens on $XDG_RUNTIME_DIR/werkdock.sock
|
|
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/werkdock.sock
|
|
export TESTCONTAINERS_RYUK_DISABLED=true
|
|
```
|
|
|
|
That is the whole adaptation: one socket and one environment variable.
|
|
Ryuk, Testcontainers' cleanup container, wants the API socket
|
|
bind-mounted into a container of its own and is therefore not
|
|
supported; `--rm`, `werkdock rm`, and Testcontainers' own session
|
|
cleanup cover the same ground.
|
|
|
|
A test then reads as it always does:
|
|
|
|
```java
|
|
try (PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16-alpine")) {
|
|
pg.start();
|
|
try (Connection c = DriverManager.getConnection(pg.getJdbcUrl(), pg.getUsername(), pg.getPassword())) {
|
|
// pg.getMappedPort(5432) is where the server really listens
|
|
}
|
|
}
|
|
```
|
|
|
|
## Or no container code at all: `jdbc:tc` URLs
|
|
|
|
Testcontainers can be the JDBC driver itself, which is how most
|
|
projects use it — the database is then a line of configuration:
|
|
|
|
```yaml
|
|
spring:
|
|
datasource:
|
|
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
|
|
url: jdbc:tc:postgresql:16-alpine:///mydb?TC_DAEMON=true&TC_INITSCRIPT=init.sql
|
|
```
|
|
|
|
This works against werkdock with the same two environment variables and
|
|
asks for no endpoint the programmatic form does not. Two flags are
|
|
worth knowing:
|
|
|
|
- `TC_DAEMON=true` keeps the container while the JVM lives. Without it
|
|
Testcontainers stops the database when the last connection closes,
|
|
and a connection pool closes connections whenever it likes.
|
|
- `TC_INITSCRIPT` names a script on the classpath, which Testcontainers
|
|
runs **over JDBC**. No file is copied into the container, so it needs
|
|
nothing werkdock lacks.
|
|
|
|
[Werkprobe](https://git.javagil.de/mi/werkprobe) exercises both forms
|
|
against werkdock in its test suite.
|
|
|
|
To keep the socket available without starting it by hand, a systemd
|
|
user unit does it (the managed webspaces have a user manager and
|
|
lingering enabled):
|
|
|
|
```ini
|
|
[Service]
|
|
ExecStart=%h/opt/werkdock/bin/werkdock system service
|
|
Environment=WERKDOCK_HOME=%h/.werkdock
|
|
```
|
|
|
|
`werkdock system service --time 30m` exits when nothing has used it for
|
|
that long, podman's manner; without `--time` it runs until stopped.
|
|
|
|
## What is different from docker, and why
|
|
|
|
**Ports are moved, not mapped.** The sandbox shares the host's network
|
|
(RFC 0002), so there is nothing to map a port with. When a client asks
|
|
for a published port, werkdock picks a free one and tells the *service*
|
|
to bind it, through the environment variable the image honours, then
|
|
reports that port to the client. `getMappedPort(5432)` therefore
|
|
returns a real, free port and the JDBC URL works — but the server
|
|
inside is genuinely listening on that port, not on 5432.
|
|
|
|
The images werkdock knows how to move:
|
|
|
|
| Image | Variable |
|
|
|---|---|
|
|
| `postgres` | `PGPORT` |
|
|
| `mariadb` | `MARIADB_TCP_PORT` |
|
|
| `mysql` | `MYSQL_TCP_PORT` |
|
|
| `redis` | `REDIS_PORT` |
|
|
|
|
Any other image keeps its own port, which is correct as long as one
|
|
instance of it runs at a time. Asking to publish such a port somewhere
|
|
else fails with the reason rather than reporting a port nothing listens
|
|
on. Adding an image is one line in `quirks` (package `api`).
|
|
|
|
**One user.** An unprivileged user namespace maps the caller to exactly
|
|
one uid, so an entrypoint that starts as root to drop privileges later
|
|
(the official `postgres` image and its gosu dance) is started as its
|
|
target user right away; those entrypoints detect a non-root uid and
|
|
skip the dance. The same table holds that knowledge.
|
|
|
|
**Refused rather than ignored.** A parameter that would change the
|
|
answer is not quietly dropped: `logs?tail=10`, `timestamps=true`, and a
|
|
`since`/`until` other than zero all answer 501 with the reason, because
|
|
werkdock streams the recorded output whole and records no timestamps
|
|
beside it.
|
|
|
|
**Everything else answers 501 with the reason.** Werkdock implements
|
|
the traffic a real session sends
|
|
([docs/testcontainers-api.md](testcontainers-api.md)), not the whole
|
|
Engine API. A client that needs more gets a plain explanation, never a
|
|
404, and the service log names the endpoint. Known gaps, none of which
|
|
`PostgreSQLContainer` needs: `PUT /containers/{id}/archive`
|
|
(`withCopyFileToContainer` and anything else that puts a file inside a
|
|
container — a JDBC init script does not, it travels over the
|
|
connection), networks, and `docker build`.
|
|
|
|
## Inside a build sandbox
|
|
|
|
The intended use is an integration test *inside* a werkdock build,
|
|
which reaches the service by having the socket bound in:
|
|
|
|
```bash
|
|
werkdock run --rm -v /repo:/repo -w /repo \
|
|
-v $XDG_RUNTIME_DIR/werkdock.sock:/run/werkdock.sock \
|
|
-e DOCKER_HOST=unix:///run/werkdock.sock \
|
|
-e TESTCONTAINERS_RYUK_DISABLED=true \
|
|
buildenv sh -c './gradlew test'
|
|
```
|
|
|
|
The database then runs as a sibling of the build sandbox, not inside
|
|
it, which is exactly how docker-in-docker is done with a mounted
|
|
socket.
|