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.
95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
//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
|
|
}
|