Files
waitfordb/README.md
T
unkin-agent a466b3e07f
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add valkey/redis driver with operator-secret auto-configuration
Apps fronted by valkey-operator instances need the same wait-for-ready
initContainer postgres workloads already get, and wiring per-field
secretKeyRefs for operator-generated secrets is boilerplate. waitfordb
now speaks enough RESP to AUTH and PING, and natively understands the
secret shapes CNPG and valkey-operator generate so an initContainer is
just envFrom plus a mode variable.

- valkey driver (alias: redis): fresh TCP connection per attempt,
  optional AUTH (ACL user or default), PING, reusing the existing
  retry/backoff wait loop; redis:///valkey:// DSNs; default port 6379
- WAITFORDB_SECRET_FORMAT=cnpg|valkey for envFrom-injected operator
  secrets: CNPG <cluster>-app host/port/dbname/user/password keys, and
  valkey-operator key-per-username secrets (_operator preferred, or
  WAITFORDB_USER's same-named key)
- autodetection from injected keys (CNPG keys -> postgres, valkey keys
  -> valkey); explicit WAITFORDB_DRIVER/WAITFORDB_* always win, PG*
  fallback and all existing flags unchanged
- valkey needs no user/database to be valid (unauthenticated PING)
- tests: secret shape parsing/autodetect/mismatch, fake RESP server
  covering NOAUTH/WRONGPASS/ACL auth/DSN, retry-until-up wait
- README: envFrom initContainer snippets for CNPG and valkey-operator
2026-08-23 16:36:52 +10:00

174 lines
7.1 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, without hand-writing a `psql`-in-a-shell
probe in every workload.
Readiness means a trivial liveness query (`SELECT 1`, or `PING` for
valkey/redis) 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` and `valkey` (alias: `redis`) are
also supported. Configuration is entirely via environment variables.
## Environment variables
| Variable | Default | Description |
| --------------------------- | ------------ | ------------------------------------------------------------------ |
| `WAITFORDB_DRIVER` | `postgres` | Database driver: `postgres`, `mysql`, or `valkey` (alias: `redis`). |
| `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. Valkey accepts `redis://`/`valkey://` URLs. |
| `WAITFORDB_SECRET_FORMAT` | autodetected | Operator secret shape injected via `envFrom`: `cnpg` or `valkey`. See below. |
| `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*` > operator-secret keys. The `PG*` (libpq) fallback applies to the
**postgres** driver only, so the tool is a drop-in replacement anywhere those
variables are already set.
## Operator secrets via `envFrom`
waitfordb natively understands the secrets that CloudNativePG and
valkey-operator generate, so an initContainer is just an `envFrom` plus (at
most) a mode variable — no `secretKeyRef` plumbing per field.
- **`cnpg`** — a CNPG `<cluster>-app` secret (`host`/`port`/`dbname`/`user`/
`password` keys) fills the postgres connection parameters.
- **`valkey`** — a valkey-operator generated secret where each key is a
username and its value the password (e.g. the system-passwords secret with
`_operator`/`_replication`). waitfordb authenticates as `_operator` when
present, or as `WAITFORDB_USER` with the password taken from the key of the
same name.
The format is autodetected from the injected keys (CNPG keys → postgres,
valkey/redis keys → valkey); set `WAITFORDB_SECRET_FORMAT` to pin it. Explicit
`WAITFORDB_*` variables always win over secret keys.
Wait for a CNPG cluster:
```yaml
initContainers:
- name: wait-for-db
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest
envFrom:
- secretRef:
name: mydb-app # CNPG <cluster>-app secret
env:
- name: WAITFORDB_TIMEOUT
value: 5m
```
Wait for a valkey-operator instance:
```yaml
initContainers:
- name: wait-for-valkey
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest
envFrom:
- secretRef:
name: internal-myapp-valkey-system-passwords
env:
- name: WAITFORDB_SECRET_FORMAT
value: valkey # optional; autodetected from the _operator key
- name: WAITFORDB_HOST
value: myapp-valkey.myns.svc
- name: WAITFORDB_TIMEOUT
value: 5m
```
### 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=appdb user=appuser password=*** timeout=5m interval=2s connect_timeout=5s)
12:49:52 attempt 1: failed to connect to `user=appuser database=appdb`: dial tcp ... connection refused; retrying in 2s (elapsed 2s/5m)
12:49:56 database appdb 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: postgres-rw.default.svc.cluster.local
- name: WAITFORDB_PORT
value: "5432"
- name: WAITFORDB_DATABASE
value: appdb
- name: WAITFORDB_SSLMODE
value: disable
- name: WAITFORDB_TIMEOUT
value: 5m
- name: WAITFORDB_USER
valueFrom:
secretKeyRef:
name: app-db
key: username
- name: WAITFORDB_PASSWORD
valueFrom:
secretKeyRef:
name: app-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`).