mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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>
This commit is contained in:
@@ -3,10 +3,18 @@ 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"
|
||||
@@ -42,10 +50,26 @@ func (*mockTB) TempDir() string {
|
||||
|
||||
func main() {
|
||||
t := &mockTB{}
|
||||
defer func() {
|
||||
for _, f := range t.cleanup {
|
||||
f()
|
||||
}
|
||||
|
||||
// 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")
|
||||
@@ -54,10 +78,13 @@ func main() {
|
||||
var err error
|
||||
connection, cleanup, err = dbtestutil.OpenContainerized(t, dbtestutil.DBContainerOptions{})
|
||||
if err != nil {
|
||||
err = xerrors.Errorf("open containerized database failed: %w", err)
|
||||
panic(err)
|
||||
_, _ = fmt.Fprintf(os.Stderr, "containerized postgres unavailable (%s); falling back to embedded postgres\n", err)
|
||||
connection, cleanup, err = openEmbeddedPostgres()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
defer cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", connection)
|
||||
@@ -75,6 +102,14 @@ func main() {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -89,3 +124,98 @@ func main() {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user