d59dcbe74e
Stand up the shared library with its first module: the Postgres plumbing every service currently copy-pastes — the DSN builder, the pool constructor, and the migration runner arrproxy proved out. - Add pg.DSNFromEnv, generalising the identical Sprintf builders in encapi, artifactapi and forgebot into one prefixed lookup with DATABASE_URL passthrough and libpq fallbacks. - Add pg.New and pg.NewMigrated, which ping before returning so an unreachable server fails at startup rather than on the first query. - Add pg.Migrate, lifting arrproxy's runner verbatim in semantics and generalising the hardcoded advisory-lock key to FNV-1a/64 of a caller-supplied name and the embedded set to an fs.FS. - Add pg/pgtest, unifying the encapi and artifactapi testcontainers helpers, with SkipIfShort so container-backed tests self-skip on the Docker-less Kubernetes runners. - Add the Makefile, README and pre-commit config, plus test, pre-commit and build pipelines on golib-ci.
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
// Package pgtest starts a throwaway Postgres container for integration-style
|
|
// tests. Import it only from _test.go files so testcontainers never reaches a
|
|
// production binary.
|
|
//
|
|
// The estate's CI runs on Kubernetes with no Docker socket, so tests that need
|
|
// a container must skip themselves under -short. SkipIfShort does exactly that,
|
|
// and StartPostgres reports a plain error when no container runtime is
|
|
// reachable, leaving it to the caller whether that is a skip or a failure.
|
|
package pgtest
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/testcontainers/testcontainers-go"
|
|
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
// Image is the Postgres the estate targets; every service runs the same major.
|
|
const Image = "postgres:17-alpine"
|
|
|
|
const (
|
|
database = "pgtest"
|
|
username = "pgtest"
|
|
password = "pgtest123"
|
|
)
|
|
|
|
// startTimeout bounds the container's readiness wait.
|
|
const startTimeout = 60 * time.Second
|
|
|
|
func init() {
|
|
// The Ryuk reaper container cannot start in every environment (rootless
|
|
// podman, restricted CI). Callers get an explicit terminate func instead,
|
|
// so disable Ryuk unless the environment has deliberately enabled it.
|
|
if _, ok := os.LookupEnv("TESTCONTAINERS_RYUK_DISABLED"); !ok {
|
|
_ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
|
|
}
|
|
}
|
|
|
|
// SkipIfShort skips the test under -short. Container-backed tests call it
|
|
// first, which is what keeps `go test -short ./...` green with no Docker.
|
|
func SkipIfShort(t *testing.T) {
|
|
t.Helper()
|
|
if testing.Short() {
|
|
t.Skip("skipping container-backed test in short mode")
|
|
}
|
|
}
|
|
|
|
// StartPostgres launches a Postgres container and returns its DSN plus a
|
|
// terminate func the caller must run, even on failure paths.
|
|
func StartPostgres(ctx context.Context) (dsn string, terminate func(), err error) {
|
|
c, err := tcpostgres.Run(ctx,
|
|
Image,
|
|
tcpostgres.WithDatabase(database),
|
|
tcpostgres.WithUsername(username),
|
|
tcpostgres.WithPassword(password),
|
|
testcontainers.WithWaitStrategy(
|
|
// Postgres opens the port, runs its init scripts, then restarts, so
|
|
// wait for the readiness log twice to avoid connection resets.
|
|
wait.ForLog("database system is ready to accept connections").
|
|
WithOccurrence(2).
|
|
WithStartupTimeout(startTimeout),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("start postgres container: %w", err)
|
|
}
|
|
terminate = func() { _ = c.Terminate(ctx) }
|
|
|
|
host, err := c.Host(ctx)
|
|
if err != nil {
|
|
terminate()
|
|
return "", nil, fmt.Errorf("container host: %w", err)
|
|
}
|
|
port, err := c.MappedPort(ctx, "5432/tcp")
|
|
if err != nil {
|
|
terminate()
|
|
return "", nil, fmt.Errorf("container port: %w", err)
|
|
}
|
|
dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
|
username, password, host, port.Port(), database)
|
|
return dsn, terminate, nil
|
|
}
|
|
|
|
// MustStartPostgres is StartPostgres for a TestMain-less test: it skips under
|
|
// -short, fails the test if the container cannot start, and registers cleanup.
|
|
func MustStartPostgres(ctx context.Context, t *testing.T) string {
|
|
t.Helper()
|
|
SkipIfShort(t)
|
|
dsn, terminate, err := StartPostgres(ctx)
|
|
if err != nil {
|
|
t.Fatalf("start postgres: %v", err)
|
|
}
|
|
t.Cleanup(terminate)
|
|
return dsn
|
|
}
|