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
7.1 KiB
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>-appsecret (host/port/dbname/user/passwordkeys) 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_operatorwhen present, or asWAITFORDB_USERwith 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:
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:
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.
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
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).