2b37986218
Small Go tool + distroless container used as a K8s initContainer to block an app until its database (Postgres/MySQL) is reachable. Env-var configured (WAITFORDB_* + libpq PG* fallback), configurable timeout/interval, redacted logs, exit codes. Woodpecker CI publishes docker-internal/waitfordb on tag.
122 lines
5.3 KiB
Markdown
122 lines
5.3 KiB
Markdown
# waitfordb
|
|
|
|
A tiny, stateless Go tool that blocks until a database is ready, then exits `0`.
|
|
It is designed to run as a Kubernetes **initContainer** so an app never starts
|
|
before its database can serve queries. It replaces the hand-written
|
|
`psql`-in-a-shell initContainers we used on the arrstack (sonarr/radarr/prowlarr).
|
|
|
|
Readiness means a trivial liveness query (`SELECT 1`) succeeds under the given
|
|
credentials and database — proving the **server is up, auth works, and the
|
|
target database/role exist**. On timeout it exits non-zero so the pod fails
|
|
fast and Kubernetes restarts it.
|
|
|
|
`postgres` is the default driver; `mysql` is also supported. Configuration is
|
|
entirely via environment variables.
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Default | Description |
|
|
| --------------------------- | ------------ | ------------------------------------------------------------------ |
|
|
| `WAITFORDB_DRIVER` | `postgres` | Database driver: `postgres` or `mysql`. |
|
|
| `WAITFORDB_HOST` | `localhost` | Database host. Postgres falls back to `PGHOST`. |
|
|
| `WAITFORDB_PORT` | `5432`/`3306`| Database port (driver default). Postgres falls back to `PGPORT`. |
|
|
| `WAITFORDB_USER` | — | Username. Postgres falls back to `PGUSER`. Required (unless DSN). |
|
|
| `WAITFORDB_PASSWORD` | — | Password. Postgres falls back to `PGPASSWORD`. |
|
|
| `WAITFORDB_DATABASE` | — | Database name. Postgres falls back to `PGDATABASE`. Required (unless DSN). |
|
|
| `WAITFORDB_SSLMODE` | driver default | Postgres sslmode (e.g. `disable`). Falls back to `PGSSLMODE`. |
|
|
| `WAITFORDB_DSN` | — | Full driver-native connection string. Overrides all fields above. |
|
|
| `WAITFORDB_TIMEOUT` | `0` (forever)| Total wait budget as a Go duration (e.g. `5m`). `0` waits forever. |
|
|
| `WAITFORDB_INTERVAL` | `2s` | Gap between retries. |
|
|
| `WAITFORDB_CONNECT_TIMEOUT` | `5s` | Per-attempt connect timeout, so a black-holed host cannot hang. |
|
|
|
|
**Precedence for connection parameters:** `WAITFORDB_DSN` > `WAITFORDB_*` > `PG*`.
|
|
The `PG*` (libpq) fallback applies to the **postgres** driver only, so the tool
|
|
is a drop-in replacement anywhere those variables are already set.
|
|
|
|
### Exit codes
|
|
|
|
| Code | Meaning |
|
|
| ---- | ------------------------------------------- |
|
|
| `0` | Database ready. |
|
|
| `1` | Timed out (or interrupted by SIGTERM/SIGINT). |
|
|
| `2` | Configuration error. |
|
|
|
|
`waitfordb version` prints the version; `waitfordb help` prints usage.
|
|
|
|
### Example logs
|
|
|
|
```
|
|
12:49:50 waitfordb v0.1.0: waiting for database (driver=postgres addr=db:5432 database=sonarr-main user=sonarr password=*** timeout=5m interval=2s connect_timeout=5s)
|
|
12:49:52 attempt 1: failed to connect to `user=sonarr database=sonarr-main`: dial tcp ... connection refused; retrying in 2s (elapsed 2s/5m)
|
|
12:49:56 database sonarr-main ready after 4.2s (3 attempts)
|
|
```
|
|
|
|
The password is never printed — not in the startup line, not in any error line.
|
|
|
|
## Kubernetes initContainer example
|
|
|
|
Drop this initContainer into a Deployment/StatefulSet. It reuses the same
|
|
`Secret` the app consumes, so there is no second place to keep credentials.
|
|
|
|
```yaml
|
|
initContainers:
|
|
- name: wait-for-db
|
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest
|
|
env:
|
|
- name: WAITFORDB_HOST
|
|
value: arrstack-postgres-rw.arrstack.svc.cluster.local
|
|
- name: WAITFORDB_PORT
|
|
value: "5432"
|
|
- name: WAITFORDB_DATABASE
|
|
value: sonarr-main
|
|
- name: WAITFORDB_SSLMODE
|
|
value: disable
|
|
- name: WAITFORDB_TIMEOUT
|
|
value: 5m
|
|
- name: WAITFORDB_USER
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: sonarr-db
|
|
key: username
|
|
- name: WAITFORDB_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: sonarr-db
|
|
key: password
|
|
resources:
|
|
requests:
|
|
cpu: 10m
|
|
memory: 16Mi
|
|
limits:
|
|
cpu: 100m
|
|
memory: 64Mi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
```
|
|
|
|
The libpq fallback means you can instead pass a single set of `PG*` variables
|
|
(e.g. via `envFrom` a shared ConfigMap/Secret) with no `WAITFORDB_*` at all.
|
|
|
|
## Development
|
|
|
|
```sh
|
|
make build # build ./dist/waitfordb
|
|
make test # unit tests (fake driver + fake clock, deterministic)
|
|
make test-integration # spins up postgres:16 via podman, asserts wait-then-succeed
|
|
make docker # build the container image locally
|
|
```
|
|
|
|
Releases are cut by tagging: `make patch|minor|major` bumps the semver tag and
|
|
pushes it, which triggers the Woodpecker pipeline to build and push
|
|
`artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:<tag>` (and `latest`).
|
|
|
|
## Adding a driver
|
|
|
|
Implement `driver.Driver` (`Name()` + `Open(config.Config) (Pinger, error)`) and
|
|
register it from an `init()`. Engines that plug into `database/sql` can reuse the
|
|
shared `sqlPinger` (see `internal/driver/postgres.go` and `mysql.go`).
|