Files
coder/coderd/database/gen/dump/main.go
T
Thomas Kosiewski 96ea2465b7 build(coderd/database/gen/dump): fall back to embedded postgres without docker (#25332)
Generating `coderd/database/dump.sql` previously required a
Docker-compatible socket via `ory/dockertest`. Contributors using
runtimes that don't expose one (e.g. Apple's `container` CLI) hit a
panic during `make gen`:

```
build: panic: open containerized database failed: open container: could not start resource: dial unix /var/run/docker.sock: connect: no such file or directory
```

Fall back to `fergusstrange/embedded-postgres` (already a direct module
dep, used by `scripts/develop/dbrecovery.go`) when
`dbtestutil.OpenContainerized` fails. The server's timezone is forced to
UTC so `timestamptz` DEFAULT expressions canonicalize identically to the
Docker-based path; otherwise the host's local TZ leaks into the dump as
values like `'0001-12-31 23:06:32+00 BC'`.

`PGDumpSchemaOnly` still needs `pg_dump` v13.x on PATH (the
embedded-postgres archive ships only `initdb`/`postgres`/`pg_ctl`). When
neither `pg_dump` nor `docker` is available, the existing error is
supplemented with install hints for `mise`, `brew`, and `apt`.

CI keeps using the Docker path unchanged; the fallback is local-dev-only
and produces a byte-identical `dump.sql`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:39:05 +02:00

222 lines
5.9 KiB
Go

package main
import (
"database/sql"
"fmt"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/migrations"
"github.com/coder/coder/v2/scripts/atomicwrite"
)
var preamble = []byte("-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.")
type mockTB struct {
cleanup []func()
}
func (*mockTB) Name() string {
return "mockTB"
}
func (t *mockTB) Cleanup(f func()) {
t.cleanup = append(t.cleanup, f)
}
func (*mockTB) Helper() {
// noop
}
func (*mockTB) Logf(format string, args ...any) {
_, _ = fmt.Printf(format, args...)
}
func (*mockTB) TempDir() string {
panic("not implemented")
}
func main() {
t := &mockTB{}
// Ensure cleanups run on both normal exit and SIGINT/SIGTERM.
// Go's default signal handlers call os.Exit, which skips deferred
// funcs and would leave an embedded-postgres daemon orphaned.
var cleanupOnce sync.Once
runCleanup := func() {
cleanupOnce.Do(func() {
for _, f := range t.cleanup {
f()
}
})
}
defer runCleanup()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh
runCleanup()
os.Exit(130)
}()
connection := os.Getenv("DB_DUMP_CONNECTION_URL")
if connection == "" {
var cleanup func()
var err error
connection, cleanup, err = dbtestutil.OpenContainerized(t, dbtestutil.DBContainerOptions{})
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "containerized postgres unavailable (%s); falling back to embedded postgres\n", err)
connection, cleanup, err = openEmbeddedPostgres()
if err != nil {
panic(err)
}
}
t.Cleanup(cleanup)
}
db, err := sql.Open("postgres", connection)
if err != nil {
err = xerrors.Errorf("open database failed: %w", err)
panic(err)
}
defer db.Close()
err = migrations.Up(db)
if err != nil {
err = xerrors.Errorf("run migrations failed: %w", err)
panic(err)
}
dumpBytes, err := dbtestutil.PGDumpSchemaOnly(connection)
if err != nil {
if !pgDumpUsable() {
_, _ = fmt.Fprintf(os.Stderr,
"\nThis step needs pg_dump (PostgreSQL v13 or later) on PATH OR a Docker-compatible daemon.\n"+
"Install pg_dump locally to avoid Docker:\n"+
" mise: mise use -g postgres@13\n"+
" brew: brew install libpq && brew link --force libpq\n"+
" apt: sudo apt-get install -y postgresql-client\n\n")
}
err = xerrors.Errorf("dump schema failed: %w", err)
panic(err)
}
_, mainPath, _, ok := runtime.Caller(0)
if !ok {
panic("couldn't get caller path")
}
err = atomicwrite.File(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), append(preamble, dumpBytes...))
if err != nil {
err = xerrors.Errorf("write dump failed: %w", err)
panic(err)
}
}
// pgDumpUsable mirrors PGDumpSchemaOnly's requirement (pg_dump on PATH at
// v13 or later). PGDumpSchemaOnly silently falls back to `docker run` when
// either condition fails, so we only show the install hint here when the
// local pg_dump is genuinely unusable. Otherwise an old pg_dump would
// produce a misleading Docker-not-found message.
func pgDumpUsable() bool {
path, err := exec.LookPath("pg_dump")
if err != nil {
return false
}
out, err := exec.Command(path, "--version").Output()
if err != nil {
return false
}
// Output format: "pg_dump (PostgreSQL) 14.5 ..."
parts := strings.Fields(string(out))
if len(parts) < 3 {
return false
}
major, err := strconv.Atoi(strings.SplitN(parts[2], ".", 2)[0])
if err != nil {
return false
}
return major >= 13
}
func openEmbeddedPostgres() (string, func(), error) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return "", nil, xerrors.Errorf("find ephemeral port: %w", err)
}
tcpAddr, ok := listener.Addr().(*net.TCPAddr)
if !ok {
_ = listener.Close()
return "", nil, xerrors.New("listener returned non-TCP addr")
}
port := tcpAddr.Port
_ = listener.Close()
cacheRoot, err := os.UserCacheDir()
if err != nil {
cacheRoot = os.TempDir()
}
cacheDir := filepath.Join(cacheRoot, "coder", "dbdump-postgres")
runtimeDir, err := os.MkdirTemp("", "coder-dbdump-postgres-")
if err != nil {
return "", nil, xerrors.Errorf("create runtime dir: %w", err)
}
const password = "postgres"
ep := embeddedpostgres.NewDatabase(
embeddedpostgres.DefaultConfig().
Version(embeddedpostgres.V13).
// repo1.maven.org is flaky; matches cli/server.go and scripts/embedded-pg/main.go.
BinaryRepositoryURL("https://repo.maven.apache.org/maven2").
BinariesPath(filepath.Join(cacheDir, "bin")).
CachePath(filepath.Join(cacheDir, "cache")).
DataPath(filepath.Join(runtimeDir, "data")).
RuntimePath(filepath.Join(runtimeDir, "runtime")).
Port(uint32(port)). //nolint:gosec // port from listener, fits uint32.
Username("postgres").
Password(password).
Database("postgres").
// Postgres canonicalizes timestamptz DEFAULT expressions at
// parse time using the server timezone GUC, then stores the
// canonical form in pg_attrdef. Without UTC, the host's TZ
// leaks into dump.sql as values like '0001-12-31 23:06:32+00 BC'.
StartParameters(map[string]string{"timezone": "UTC"}).
Logger(nil),
)
_, _ = fmt.Fprintln(os.Stderr, "starting embedded postgres (first run may download binaries)...")
if err := ep.Start(); err != nil {
_ = os.RemoveAll(runtimeDir)
return "", nil, xerrors.Errorf("start embedded postgres: %w", err)
}
dsn := dbtestutil.ConnectionParams{
Username: "postgres",
Password: password,
Host: "127.0.0.1",
Port: strconv.Itoa(port),
DBName: "postgres",
}.DSN()
cleanup := func() {
if stopErr := ep.Stop(); stopErr != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to stop embedded postgres: %s\n", stopErr)
}
_ = os.RemoveAll(runtimeDir)
}
return dsn, cleanup, nil
}