package seededpostgres import ( "database/sql" "fmt" "net" "os" "path" edgesql "edge-infra.dev/pkg/edge/api/sql" "edge-infra.dev/pkg/edge/api/sql/plugin" "edge-infra.dev/pkg/lib/build/bazel" "edge-infra.dev/pkg/lib/compression" "edge-infra.dev/pkg/lib/gcp/cloudsql" "edge-infra.dev/pkg/lib/logging" "github.com/bazelbuild/rules_go/go/runfiles" embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/golang-migrate/migrate/v4/database/postgres" ) var PostgresVersion = embeddedpostgres.V14 // SeededPostgres wraps a github.com/fergusstrange/embedded-postgres.EmbeddedPostgres object. type SeededPostgres struct { dbname string username string password string port int tempDir string ep *embeddedpostgres.EmbeddedPostgres } // New creates an embedded postgres database and seeds it with data. func New() (*SeededPostgres, error) { return NewWithUser("postgres", "postgres", "postgres") } func NewWithUser(dbname, username, password string) (*SeededPostgres, error) { if dbname == "" || username == "" || password == "" { return nil, fmt.Errorf("NewWithUser arguments must not be empty: dbname=%q username=%q password=%q", dbname, username, password) } var cfg = embeddedpostgres.DefaultConfig() cfg = cfg.Version(PostgresVersion) cfg = cfg.Database(dbname) cfg = cfg.Username(username) cfg = cfg.Password(password) var port, err = findUnusedPort() if err != nil { return nil, err } cfg = cfg.Port(uint32(port)) /* #nosec G115 */ var tempDir string if bazel.IsBazelTest() || bazel.IsBazelRun() { embeddedTxzFile, err := runfiles.Rlocation(path.Join("edge_infra", "hack", "tools", "postgres.txz")) if err != nil { return nil, err } tempDir, err = bazel.NewTestTmpDir("edge-infra-api-test-*") if err != nil { return nil, err } err = compression.DecompressTarXz(embeddedTxzFile, tempDir) if err != nil { return nil, err } cfg = cfg.RuntimePath(path.Join(tempDir, "runtime")) cfg = cfg.BinariesPath(tempDir) } var sp = &SeededPostgres{ dbname: dbname, username: username, password: password, port: port, tempDir: tempDir, ep: embeddedpostgres.NewDatabase(cfg), } err = sp.ep.Start() if err != nil { _ = sp.Close() return nil, err } db, err := sp.DB() if err != nil { _ = sp.Close() return nil, err } defer db.Close() driver, err := postgres.WithInstance(db, &postgres.Config{}) if err != nil { _ = sp.Close() return nil, err } defer driver.Close() _, err = db.Exec("CREATE EXTENSION IF NOT EXISTS \"pgcrypto\"") if err != nil { _ = sp.Close() return nil, err } var logger = logging.NewLogger().WithName("seededpostgres") var pluginConfig = &plugin.Config{ MigrationAction: "up", Ordered: true, TestMode: true, Data: Seed, } err = edgesql.SetupEdgeTables(pluginConfig, driver, logger, db) if err != nil { _ = sp.Close() return nil, err } return sp, nil } // Close should be called when done testing to stop the embedded postgres database and free up resources used. func (sp *SeededPostgres) Close() error { var errStop error if sp.ep != nil { // Stop the embedded postgres, but wait to return the error until after deleting tempDir errStop = sp.ep.Stop() } errDeleteTempDir := os.RemoveAll(sp.tempDir) if errDeleteTempDir != nil { return errDeleteTempDir } return errStop } // DB connects to the database for the desired user. It uses `edge-infra.dev/pkg/edge/api/sql.PostgresConnection` to create the sql.DB object. func (sp *SeededPostgres) DB() (*sql.DB, error) { return sp.EdgePostgres().NewConnection() } func (sp *SeededPostgres) EdgePostgres() *cloudsql.EdgePostgres { return cloudsql.PostgresConnection("localhost", fmt.Sprint(sp.port)).DBName(sp.dbname).Username(sp.username).Password(sp.password) } func (sp *SeededPostgres) Port() int { return sp.port } func findUnusedPort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") if err != nil { return 0, err } l, err := net.ListenTCP("tcp", addr) if err != nil { return 0, err } return l.Addr().(*net.TCPAddr).Port, l.Close() }