From 7389c945210b5861cebed553092f30e0202ed371 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 15:02:34 +0200 Subject: [PATCH 1/8] add some options to PostgresContainer --- .gitignore | 29 ++++++++++++++++++ option.go | 39 ++++++++++++++++++++++++ postgres_container.go | 70 ++++++++++++++++++++++++++++++------------- 3 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 .gitignore create mode 100644 option.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e30742 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# IDE +.idea/ +.idea + +# Mac +.DS_Store +*/.DS_Store +!sqldialects.xml + +# Dependencies +vendor + +# local files +/tmp + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +out/* +.editorconfig diff --git a/option.go b/option.go new file mode 100644 index 0000000..37484e7 --- /dev/null +++ b/option.go @@ -0,0 +1,39 @@ +package sqltestutil + +import "fmt" + +type Option func(*PostgresContainer) + +func WithPassword(password string) Option { + return func(container *PostgresContainer) { + if len(password) == 0 { + panic("sqltestutil: password option can not be empty") + } + container.password = password + } +} +func WithUser(user string) Option { + return func(container *PostgresContainer) { + if len(user) == 0 { + panic("sqltestutil: user option can not be empty") + } + container.user = user + } +} +func WithPort(port uint16) Option { + return func(container *PostgresContainer) { + if port <= 1000 { + panic("sqltestutil: port option can not be less than 1000") + } + container.port = fmt.Sprint(port) + } +} + +func WithVersion(version string) Option { + return func(container *PostgresContainer) { + if len(version) == 0 { + panic("sqltestutil: version option can not be empty") + } + container.version = version + } +} diff --git a/postgres_container.go b/postgres_container.go index 5f5473f..c219f45 100644 --- a/postgres_container.go +++ b/postgres_container.go @@ -21,7 +21,10 @@ import ( type PostgresContainer struct { id string password string + user string port string + dbName string + version string } // StartPostgresContainer starts a new Postgres Docker container. The version @@ -58,20 +61,55 @@ type PostgresContainer struct { // } // // func TestExampleTestSuite(t *testing.T) { -// pg, _ := sqltestutil.StartPostgresContainer(context.Background(), "12") +// pg, _ := sqltestutil.StartPostgresContainer(context.Background(), WithVersion("12")) // defer pg.Shutdown(ctx) // suite.Run(t, &ExampleTestSuite{}) // } // // [1]: https://github.com/golang/go/issues/37206 // [2]: https://github.com/stretchr/testify -func StartPostgresContainer(ctx context.Context, version string) (*PostgresContainer, error) { +func StartPostgresContainer(ctx context.Context, options ...Option) (*PostgresContainer, error) { cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } defer cli.Close() - image := "postgres:" + version + + containerObj := &PostgresContainer{} + // + // apply options, if any. + // + for i := range options { + options[i](containerObj) + } + // + // set default values + // + if len(containerObj.password) == 0 { + password, err := randomPassword() + if err != nil { + return nil, err + } + containerObj.password = password + } + if len(containerObj.port) == 0 { + port, err := randomPort() + if err != nil { + return nil, err + } + containerObj.port = port + } + if len(containerObj.user) == 0 { + containerObj.user = "pgtest" + } + if len(containerObj.dbName) == 0 { + containerObj.user = "pgtest" + } + if len(containerObj.version) == 0 { + containerObj.version = "12" + } + + image := "postgres:" + containerObj.version _, _, err = cli.ImageInspectWithRaw(ctx, image) if err != nil { _, notFound := err.(interface { @@ -91,20 +129,12 @@ func StartPostgresContainer(ctx context.Context, version string) (*PostgresConta } } - password, err := randomPassword() - if err != nil { - return nil, err - } - port, err := randomPort() - if err != nil { - return nil, err - } createResp, err := cli.ContainerCreate(ctx, &container.Config{ Image: image, Env: []string{ - "POSTGRES_DB=pgtest", - "POSTGRES_PASSWORD=" + password, - "POSTGRES_USER=pgtest", + "POSTGRES_DB=" + containerObj.dbName, + "POSTGRES_PASSWORD=" + containerObj.password, + "POSTGRES_USER=" + containerObj.user, }, Healthcheck: &container.HealthConfig{ Test: []string{"CMD-SHELL", "pg_isready -U pgtest"}, @@ -115,7 +145,7 @@ func StartPostgresContainer(ctx context.Context, version string) (*PostgresConta }, &container.HostConfig{ PortBindings: nat.PortMap{ "5432/tcp": []nat.PortBinding{ - {HostPort: port}, + {HostPort: containerObj.port}, }, }, }, nil, nil, "") @@ -160,17 +190,15 @@ HealthCheck: time.Sleep(500 * time.Millisecond) } } - return &PostgresContainer{ - id: createResp.ID, - password: password, - port: port, - }, nil + containerObj.id = createResp.ID + + return containerObj, nil } // ConnectionString returns a connection URL string that can be used to connect // to the running Postgres container. func (c *PostgresContainer) ConnectionString() string { - return fmt.Sprintf("postgres://pgtest:%s@127.0.0.1:%s/pgtest", c.password, c.port) + return fmt.Sprintf("postgres://%s:%s@127.0.0.1:%s/%s", c.user, c.password, c.port, c.dbName) } // Shutdown cleans up the Postgres container by stopping and removing it. This From 7473ea98321110c18ab3c3a97d2e12fa46edd7a9 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 15:26:36 +0200 Subject: [PATCH 2/8] fix module path --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 27c0648..23de6fd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/bitcomplete/sqltestutil +module github.com/mohammad-ahmadi-de/sqltestutil go 1.18 From 12d3e8ce411ef5f7f1263410abcb6ced454cc080 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 15:35:58 +0200 Subject: [PATCH 3/8] add database name option --- option.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/option.go b/option.go index 37484e7..3685f64 100644 --- a/option.go +++ b/option.go @@ -37,3 +37,11 @@ func WithVersion(version string) Option { container.version = version } } +func WithDBName(dbName string) Option { + return func(container *PostgresContainer) { + if len(dbName) == 0 { + panic("sqltestutil: dbName option can not be empty") + } + container.dbName = dbName + } +} From 55cf49bac1a79306fa8b3cde7e8f14ac0db5afc1 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 16:05:47 +0200 Subject: [PATCH 4/8] make RunMigrations more generic --- migration.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/migration.go b/migration.go index 45a3a78..293bb9a 100644 --- a/migration.go +++ b/migration.go @@ -2,11 +2,10 @@ package sqltestutil import ( "context" + "database/sql/driver" "io/ioutil" "path/filepath" "sort" - - "github.com/jmoiron/sqlx" ) // RunMigrations reads all of the files matching *.up.sql in migrationDir and @@ -19,7 +18,7 @@ import ( // // Note that this function does not check whether the migration has already been // run. Its primary purpose is to initialize a test database. -func RunMigrations(ctx context.Context, db sqlx.ExecerContext, migrationDir string) error { +func RunMigrations(ctx context.Context, db driver.ExecerContext, migrationDir string) error { filenames, err := filepath.Glob(filepath.Join(migrationDir, "*.up.sql")) if err != nil { return err @@ -30,7 +29,7 @@ func RunMigrations(ctx context.Context, db sqlx.ExecerContext, migrationDir stri if err != nil { return err } - _, err = db.ExecContext(ctx, string(data)) + _, err = db.ExecContext(ctx, string(data), nil) if err != nil { return err } From b5b09d2403c3777c360c6513a9ed1416623d7973 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 16:27:49 +0200 Subject: [PATCH 5/8] make RunMigration to run specific files --- migration.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/migration.go b/migration.go index 293bb9a..83b8de1 100644 --- a/migration.go +++ b/migration.go @@ -18,13 +18,25 @@ import ( // // Note that this function does not check whether the migration has already been // run. Its primary purpose is to initialize a test database. -func RunMigrations(ctx context.Context, db driver.ExecerContext, migrationDir string) error { +func RunMigrations(ctx context.Context, db driver.ExecerContext, migrationDir string, files ...string) error { filenames, err := filepath.Glob(filepath.Join(migrationDir, "*.up.sql")) if err != nil { return err } + var filter map[string]struct{} = nil + if len(files) > 0 { + filter = make(map[string]struct{}) + for i := range files { + filter[files[i]] = struct{}{} + } + } sort.Strings(filenames) for _, filename := range filenames { + if len(files) > 0 { + if _, exist := filter[filename]; !exist { + continue + } + } data, err := ioutil.ReadFile(filename) if err != nil { return err From e41d3c32930dec990487c36c2ac38f68d7120411 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 16:33:59 +0200 Subject: [PATCH 6/8] make RunMigration to run specific files --- migration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration.go b/migration.go index 83b8de1..7ef088d 100644 --- a/migration.go +++ b/migration.go @@ -33,7 +33,7 @@ func RunMigrations(ctx context.Context, db driver.ExecerContext, migrationDir st sort.Strings(filenames) for _, filename := range filenames { if len(files) > 0 { - if _, exist := filter[filename]; !exist { + if _, exist := filter[filepath.Base(filename)]; !exist { continue } } From a7d2138e27ed424bcf6fecec098aed156759ec09 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 17:35:58 +0200 Subject: [PATCH 7/8] remove leaked container incase of failure on shutdown call --- option.go | 8 ++++++ postgres_container.go | 51 ++++++++++++++++++++++++++++++++------ postgres_container_test.go | 15 +++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 postgres_container_test.go diff --git a/option.go b/option.go index 3685f64..25c59c4 100644 --- a/option.go +++ b/option.go @@ -45,3 +45,11 @@ func WithDBName(dbName string) Option { container.dbName = dbName } } +func WithContainerName(containerName string) Option { + return func(container *PostgresContainer) { + if len(containerName) == 0 { + panic("sqltestutil: containerName option can not be empty") + } + container.containerName = containerName + } +} diff --git a/postgres_container.go b/postgres_container.go index c219f45..c6b060d 100644 --- a/postgres_container.go +++ b/postgres_container.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "errors" "fmt" + "github.com/docker/docker/api/types/filters" "io" "math/big" "net" @@ -19,12 +20,13 @@ import ( // PostgresContainer is a Docker container running Postgres. It can be used to // cheaply start a throwaway Postgres instance for testing. type PostgresContainer struct { - id string - password string - user string - port string - dbName string - version string + id string + password string + user string + port string + dbName string + version string + containerName string } // StartPostgresContainer starts a new Postgres Docker container. The version @@ -103,11 +105,21 @@ func StartPostgresContainer(ctx context.Context, options ...Option) (*PostgresCo containerObj.user = "pgtest" } if len(containerObj.dbName) == 0 { - containerObj.user = "pgtest" + containerObj.dbName = "pgtest" } if len(containerObj.version) == 0 { containerObj.version = "12" } + if len(containerObj.containerName) == 0 { + containerObj.containerName = "sqltestutil" + } + // + // remove leaked containers + // + err = containerObj.fixContainerLeak(ctx) + if err != nil { + return nil, err + } image := "postgres:" + containerObj.version _, _, err = cli.ImageInspectWithRaw(ctx, image) @@ -148,7 +160,7 @@ func StartPostgresContainer(ctx context.Context, options ...Option) (*PostgresCo {HostPort: containerObj.port}, }, }, - }, nil, nil, "") + }, nil, nil, "sqltestutil") if err != nil { return nil, err } @@ -194,6 +206,29 @@ HealthCheck: return containerObj, nil } +func (c *PostgresContainer) fixContainerLeak(ctx context.Context) error { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + defer cli.Close() + + data, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true, Filters: filters.NewArgs(filters.Arg("name", c.containerName))}) + if err != nil { + return err + } + for i := range data { + err = cli.ContainerStop(ctx, data[i].ID, nil) + if err != nil { + return err + } + err = cli.ContainerRemove(ctx, data[i].ID, types.ContainerRemoveOptions{}) + if err != nil { + return err + } + } + return nil +} // ConnectionString returns a connection URL string that can be used to connect // to the running Postgres container. diff --git a/postgres_container_test.go b/postgres_container_test.go new file mode 100644 index 0000000..44eeba4 --- /dev/null +++ b/postgres_container_test.go @@ -0,0 +1,15 @@ +package sqltestutil_test + +import ( + "context" + "github.com/mohammad-ahmadi-de/sqltestutil" + "testing" +) + +func TestStartPostgresContainer(t *testing.T) { + c, err := sqltestutil.StartPostgresContainer(context.Background(), sqltestutil.WithPort(5321)) + if err != nil { + t.Fatal(err) + } + c.Shutdown(context.Background()) +} From 2cd71b5fecb8bcefd3adb9357c28f00f8ce50aa2 Mon Sep 17 00:00:00 2001 From: Mohammad Ahmadi Date: Thu, 1 Sep 2022 17:41:08 +0200 Subject: [PATCH 8/8] remove leaked container incase of failure on shutdown call --- postgres_container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres_container.go b/postgres_container.go index c6b060d..285409a 100644 --- a/postgres_container.go +++ b/postgres_container.go @@ -160,7 +160,7 @@ func StartPostgresContainer(ctx context.Context, options ...Option) (*PostgresCo {HostPort: containerObj.port}, }, }, - }, nil, nil, "sqltestutil") + }, nil, nil, containerObj.containerName) if err != nil { return nil, err }