waitfordb: initial tool — env-configured wait-for-DB init container
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.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//go:build integration
|
||||
|
||||
// Integration test: runs a real postgres container that accepts connections a
|
||||
// few seconds after start and asserts waitfordb retries then succeeds. Run with:
|
||||
//
|
||||
// go test -tags integration -run TestIntegration -v .
|
||||
//
|
||||
// Requires podman (or docker via WAITFORDB_TEST_RUNTIME). Skipped otherwise.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.unkin.net/unkin/waitfordb/internal/config"
|
||||
"git.unkin.net/unkin/waitfordb/internal/driver"
|
||||
"git.unkin.net/unkin/waitfordb/internal/wait"
|
||||
)
|
||||
|
||||
func TestIntegrationPostgresComesUpLate(t *testing.T) {
|
||||
runtime := envOr("WAITFORDB_TEST_RUNTIME", "podman")
|
||||
if _, err := exec.LookPath(runtime); err != nil {
|
||||
t.Skipf("%s not available: %v", runtime, err)
|
||||
}
|
||||
image := envOr("WAITFORDB_TEST_PG_IMAGE", "docker.io/library/postgres:16")
|
||||
|
||||
name := "waitfordb-it-" + time.Now().Format("150405")
|
||||
// Publish on a fixed host port; map to a random-ish high port.
|
||||
hostPort := "55432"
|
||||
args := []string{
|
||||
"run", "-d", "--rm", "--name", name,
|
||||
"-e", "POSTGRES_PASSWORD=secret",
|
||||
"-e", "POSTGRES_USER=app",
|
||||
"-e", "POSTGRES_DB=appdb",
|
||||
"-p", hostPort + ":5432",
|
||||
image,
|
||||
}
|
||||
if out, err := exec.Command(runtime, args...).CombinedOutput(); err != nil {
|
||||
t.Skipf("cannot start container (%s): %v\n%s", runtime, err, out)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = exec.Command(runtime, "rm", "-f", name).Run()
|
||||
})
|
||||
|
||||
t.Setenv("WAITFORDB_HOST", "127.0.0.1")
|
||||
t.Setenv("WAITFORDB_PORT", hostPort)
|
||||
t.Setenv("WAITFORDB_USER", "app")
|
||||
t.Setenv("WAITFORDB_PASSWORD", "secret")
|
||||
t.Setenv("WAITFORDB_DATABASE", "appdb")
|
||||
t.Setenv("WAITFORDB_SSLMODE", "disable")
|
||||
t.Setenv("WAITFORDB_TIMEOUT", "60s")
|
||||
t.Setenv("WAITFORDB_INTERVAL", "1s")
|
||||
|
||||
cfg, err := config.Load(os.Getenv)
|
||||
if err != nil {
|
||||
t.Fatalf("config: %v", err)
|
||||
}
|
||||
drv, err := driver.Get(cfg.Driver)
|
||||
if err != nil {
|
||||
t.Fatalf("driver: %v", err)
|
||||
}
|
||||
pinger, err := drv.Open(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer pinger.Close()
|
||||
|
||||
res := wait.Run(context.Background(),
|
||||
wait.Params{Timeout: cfg.Timeout, Interval: cfg.Interval, ConnectTimeout: cfg.ConnectTimeout},
|
||||
pinger.Ping,
|
||||
func(n int, err error, _, _, _ time.Duration) {
|
||||
t.Logf("attempt %d failed as expected: %v", n, err)
|
||||
},
|
||||
wait.RealClock{})
|
||||
|
||||
if !res.OK {
|
||||
t.Fatalf("expected readiness, got %+v", res)
|
||||
}
|
||||
t.Logf("ready after %s (%d attempts)", res.Elapsed, res.Attempts)
|
||||
if res.Attempts < 1 {
|
||||
t.Errorf("expected at least one attempt")
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(k, def string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(k)); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
Reference in New Issue
Block a user