diff --git a/internal/testdb/clickhouse.go b/internal/testdb/clickhouse.go index 1df2c1797..fe4393eed 100644 --- a/internal/testdb/clickhouse.go +++ b/internal/testdb/clickhouse.go @@ -66,6 +66,10 @@ func newClickHouse(opts ...OptionsFunc) (*sql.DB, func(), error) { return nil, nil, err } cleanup := func() { + if option.debug { + // User must manually delete the Docker container. + return + } if err := pool.Purge(container); err != nil { log.Printf("failed to purge resource: %v", err) } diff --git a/internal/testdb/mariadb.go b/internal/testdb/mariadb.go new file mode 100644 index 000000000..225bcde99 --- /dev/null +++ b/internal/testdb/mariadb.go @@ -0,0 +1,97 @@ +package testdb + +import ( + "database/sql" + "fmt" + "log" + "strconv" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +const ( + // https://hub.docker.com/_/mariadb + MARIADB_IMAGE = "mariadb" + MARIADB_VERSION = "10" + + MARIADB_DB = "testdb" + MARIADB_USER = "tester" + MARIADB_PASSWORD = "password1" +) + +func newMariaDB(opts ...OptionsFunc) (*sql.DB, func(), error) { + option := &options{} + for _, f := range opts { + f(option) + } + // Uses a sensible default on windows (tcp/http) and linux/osx (socket). + pool, err := dockertest.NewPool("") + if err != nil { + return nil, nil, fmt.Errorf("failed to connect to docker: %v", err) + } + options := &dockertest.RunOptions{ + Repository: MARIADB_IMAGE, + Tag: MARIADB_VERSION, + Env: []string{ + "MARIADB_USER=" + MARIADB_USER, + "MARIADB_PASSWORD=" + MARIADB_PASSWORD, + "MARIADB_ROOT_PASSWORD=" + MARIADB_PASSWORD, + "MARIADB_DATABASE=" + MARIADB_DB, + }, + Labels: map[string]string{"goose_test": "1"}, + PortBindings: make(map[docker.Port][]docker.PortBinding), + } + if option.bindPort > 0 { + options.PortBindings[docker.Port("3306/tcp")] = []docker.PortBinding{ + {HostPort: strconv.Itoa(option.bindPort)}, + } + } + container, err := pool.RunWithOptions( + options, + func(config *docker.HostConfig) { + // Set AutoRemove to true so that stopped container goes away by itself. + config.AutoRemove = true + // config.PortBindings = options.PortBindings + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to create docker container: %v", err) + } + cleanup := func() { + if option.debug { + // User must manually delete the Docker container. + return + } + if err := pool.Purge(container); err != nil { + log.Printf("failed to purge resource: %v", err) + } + } + // MySQL DSN: username:password@protocol(address)/dbname?param=value + dsn := fmt.Sprintf("%s:%s@(%s:%s)/%s?parseTime=true&multiStatements=true", + MARIADB_USER, + MARIADB_PASSWORD, + "localhost", + container.GetPort("3306/tcp"), // Fetch port dynamically assigned to container + MARIADB_DB, + ) + var db *sql.DB + // Exponential backoff-retry, because the application in the container + // might not be ready to accept connections yet. Add an extra sleep + // because mariadb containers take much longer to startup. + time.Sleep(5 * time.Second) + if err := pool.Retry(func() error { + var err error + db, err = sql.Open("mysql", dsn) + if err != nil { + return err + } + return db.Ping() + }, + ); err != nil { + return nil, cleanup, fmt.Errorf("could not connect to docker database: %v", err) + } + return db, cleanup, nil +} diff --git a/internal/testdb/postgres.go b/internal/testdb/postgres.go new file mode 100644 index 000000000..7caaa9e1c --- /dev/null +++ b/internal/testdb/postgres.go @@ -0,0 +1,93 @@ +package testdb + +import ( + "database/sql" + "fmt" + "log" + "strconv" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +const ( + // https://hub.docker.com/_/postgres + POSTGRES_IMAGE = "postgres" + POSTGRES_VERSION = "14-alpine" + + POSTGRES_DB = "testdb" + POSTGRES_USER = "postgres" + POSTGRES_PASSWORD = "password1" +) + +func newPostgres(opts ...OptionsFunc) (*sql.DB, func(), error) { + option := &options{} + for _, f := range opts { + f(option) + } + // Uses a sensible default on windows (tcp/http) and linux/osx (socket). + pool, err := dockertest.NewPool("") + if err != nil { + return nil, nil, fmt.Errorf("failed to connect to docker: %v", err) + } + options := &dockertest.RunOptions{ + Repository: POSTGRES_IMAGE, + Tag: POSTGRES_VERSION, + Env: []string{ + "POSTGRES_USER=" + POSTGRES_USER, + "POSTGRES_PASSWORD=" + POSTGRES_PASSWORD, + "POSTGRES_DB=" + POSTGRES_DB, + "listen_addresses = '*'", + }, + Labels: map[string]string{"goose_test": "1"}, + PortBindings: make(map[docker.Port][]docker.PortBinding), + } + if option.bindPort > 0 { + options.PortBindings[docker.Port("5432/tcp")] = []docker.PortBinding{ + {HostPort: strconv.Itoa(option.bindPort)}, + } + } + container, err := pool.RunWithOptions( + options, + func(config *docker.HostConfig) { + // Set AutoRemove to true so that stopped container goes away by itself. + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to create docker container: %v", err) + } + cleanup := func() { + if option.debug { + // User must manually delete the Docker container. + return + } + if err := pool.Purge(container); err != nil { + log.Printf("failed to purge resource: %v", err) + } + } + psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + "localhost", + container.GetPort("5432/tcp"), // Fetch port dynamically assigned to container + POSTGRES_USER, + POSTGRES_PASSWORD, + POSTGRES_DB, + ) + var db *sql.DB + // Exponential backoff-retry, because the application in the container + // might not be ready to accept connections yet. + if err := pool.Retry( + func() error { + var err error + db, err = sql.Open("postgres", psqlInfo) + if err != nil { + return err + } + return db.Ping() + }, + ); err != nil { + return nil, cleanup, fmt.Errorf("could not connect to docker database: %v", err) + } + return db, cleanup, nil +} diff --git a/internal/testdb/testdb.go b/internal/testdb/testdb.go index 509af1215..a2dfdb2b7 100644 --- a/internal/testdb/testdb.go +++ b/internal/testdb/testdb.go @@ -2,9 +2,17 @@ package testdb import "database/sql" -// NewClickHouse starts a ClickHouse docker container, and returns -// a connection and a cleanup function. -// If bindPort is 0,b a random port will be used. -func NewClickHouse(options ...OptionsFunc) (_ *sql.DB, cleanup func(), _ error) { +// NewClickHouse starts a ClickHouse docker container. Returns db connection and a docker cleanup function. +func NewClickHouse(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) { return newClickHouse(options...) } + +// NewPostgres starts a PostgreSQL docker container. Returns db connection and a docker cleanup function. +func NewPostgres(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) { + return newPostgres(options...) +} + +// NewMariaDB starts a MariaDB docker container. Returns a db connection and a docker cleanup function. +func NewMariaDB(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) { + return newMariaDB(options...) +} diff --git a/tests/e2e/main_test.go b/tests/e2e/main_test.go index 7756863bd..4e9d699a4 100644 --- a/tests/e2e/main_test.go +++ b/tests/e2e/main_test.go @@ -8,16 +8,13 @@ import ( "os" "os/signal" "path/filepath" - "strconv" "syscall" "testing" - "time" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" - - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" + "github.com/pressly/goose/v3/internal/check" + "github.com/pressly/goose/v3/internal/testdb" ) const ( @@ -96,170 +93,24 @@ func TestMain(m *testing.M) { // newDockerDB starts a database container and returns a usable SQL connection. func newDockerDB(t *testing.T) (*sql.DB, error) { + options := []testdb.OptionsFunc{ + testdb.WithBindPort(*bindPort), + testdb.WithDebug(*debug), + } + var ( + db *sql.DB + cleanup func() + err error + ) switch *dialect { case dialectPostgres: - return newDockerPostgresDB(t, *bindPort) + db, cleanup, err = testdb.NewPostgres(options...) case dialectMySQL: - return newDockerMariaDB(t, *bindPort) - } - return nil, fmt.Errorf("unsupported dialect: %q", *dialect) -} - -func newDockerPostgresDB(t *testing.T, bindPort int) (*sql.DB, error) { - const ( - dbUsername = "postgres" - dbPassword = "password1" - dbHost = "localhost" - dbName = "testdb" - ) - // Uses a sensible default on windows (tcp/http) and linux/osx (socket). - pool, err := dockertest.NewPool("") - if err != nil { - return nil, fmt.Errorf("failed to connect to docker: %v", err) - } - options := &dockertest.RunOptions{ - Repository: "postgres", - Tag: "14-alpine", - Env: []string{ - "POSTGRES_USER=" + dbUsername, - "POSTGRES_PASSWORD=" + dbPassword, - "POSTGRES_DB=" + dbName, - "listen_addresses = '*'", - }, - Labels: map[string]string{"goose_test": "1"}, - PortBindings: make(map[docker.Port][]docker.PortBinding), - } - if bindPort > 0 { - options.PortBindings[docker.Port("5432/tcp")] = []docker.PortBinding{ - {HostPort: strconv.Itoa(bindPort)}, - } - } - - container, err := pool.RunWithOptions( - options, - func(config *docker.HostConfig) { - // Set AutoRemove to true so that stopped container goes away by itself. - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create docker container: %v", err) - } - t.Cleanup(func() { - if *debug { - // User must manually delete the Docker container. - return - } - if err := pool.Purge(container); err != nil { - log.Printf("failed to purge resource: %v", err) - } - }) - if err != nil { - return nil, fmt.Errorf("failed to start resource: %v", err) - } - psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - dbHost, - container.GetPort("5432/tcp"), // Fetch port dynamically assigned to container - dbUsername, - dbPassword, - dbName, - ) - var db *sql.DB - // Exponential backoff-retry, because the application in the container - // might not be ready to accept connections yet. - if err := pool.Retry( - func() error { - var err error - db, err = sql.Open(dialectPostgres, psqlInfo) - if err != nil { - return err - } - return db.Ping() - }, - ); err != nil { - return nil, fmt.Errorf("could not connect to docker database: %v", err) - } - return db, nil -} - -func newDockerMariaDB(t *testing.T, bindPort int) (*sql.DB, error) { - const ( - dbUsername = "tester" - dbPassword = "password1" - dbHost = "localhost" - dbName = "testdb" - ) - // Uses a sensible default on windows (tcp/http) and linux/osx (socket). - pool, err := dockertest.NewPool("") - if err != nil { - return nil, fmt.Errorf("failed to connect to docker: %v", err) - } - options := &dockertest.RunOptions{ - Repository: "mariadb", - Tag: "10", - Env: []string{ - "MARIADB_USER=" + dbUsername, - "MARIADB_PASSWORD=" + dbPassword, - "MARIADB_ROOT_PASSWORD=" + dbPassword, - "MARIADB_DATABASE=" + dbName, - }, - Labels: map[string]string{"goose_test": "1"}, - // PortBindings: make(map[docker.Port][]docker.PortBinding), - } - if bindPort > 0 { - options.PortBindings[docker.Port("3306/tcp")] = []docker.PortBinding{ - {HostPort: strconv.Itoa(bindPort)}, - } - } - - container, err := pool.RunWithOptions( - options, - func(config *docker.HostConfig) { - // Set AutoRemove to true so that stopped container goes away by itself. - config.AutoRemove = true - // config.PortBindings = options.PortBindings - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create docker container: %v", err) - } - t.Cleanup(func() { - if *debug { - // User must manually delete the Docker container. - return - } - if err := pool.Purge(container); err != nil { - log.Printf("failed to purge resource: %v", err) - } - }) - if err != nil { - return nil, fmt.Errorf("failed to start resource: %v", err) - } - // MySQL DSN: username:password@protocol(address)/dbname?param=value - dsn := fmt.Sprintf("%s:%s@(%s:%s)/%s?parseTime=true&multiStatements=true", - dbUsername, - dbPassword, - dbHost, - container.GetPort("3306/tcp"), // Fetch port dynamically assigned to container - dbName, - ) - var db *sql.DB - // Exponential backoff-retry, because the application in the container - // might not be ready to accept connections yet. Add an extra sleep - // because mariadb containers take much longer to startup. - time.Sleep(5 * time.Second) - if err := pool.Retry(func() error { - var err error - db, err = sql.Open(dialectMySQL, dsn) - if err != nil { - return err - } - return db.Ping() - }, - ); err != nil { - return nil, fmt.Errorf("could not connect to docker database: %v", err) + db, cleanup, err = testdb.NewMariaDB(options...) + default: + return nil, fmt.Errorf("unsupported dialect: %q", *dialect) } + check.NoError(t, err) + t.Cleanup(cleanup) return db, nil }