Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (postgres): support for creating and restoring Snapshots #2199

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/features/files_and_mounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ It is possible to map a Docker volume into the container using the `Mounts` attr
!!!warning
Bind mounts are not supported, as it could not work with remote Docker hosts.

!!!tip
It is recommended to copy data from your local host machine to a test container using the file copy API
described below, as it is much more portable

## Copying files to a container

If you would like to copy a file to a container, you can do it in two different manners:
Expand Down Expand Up @@ -67,7 +71,27 @@ It's important to notice that, when copying the directory to the container, the

You can leverage the very same mechanism used for copying files to a container, but for directories.:

1. The first way is using the `Files` field in the `ContainerRequest` struct, as shown in the previous section, but using the path of a directory as `HostFilePath`.
1. The first way is using the `Files` field in the `ContainerRequest` struct, as shown in the previous section, but using the path of a directory as `HostFilePath`. Like so:

```go
ctx := context.Background()
Minivera marked this conversation as resolved.
Show resolved Hide resolved

nginxC, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: "nginx:1.17.6",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForListeningPort("80/tcp"),
Files: []ContainerFile{
{
HostFilePath: "./testdata",
ContainerFilePath: "/scripts/",
FileMode: 0o700,
},
},
},
Started: false,
})
```

2. The second way uses the existing `CopyFileToContainer` method, which will internally check if the host path is a directory, calling the `CopyDirToContainer` method if needed:

Expand Down
20 changes: 20 additions & 0 deletions docs/features/override_container_command.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ req := ContainerRequest{
}
```

### Adding to a module's command

Modules generate their own `ContainerRequest`, which may include an overridden image command. You can add arguments
to this command by using the `testcontainers.CustomizeRequest` option when using a module.

```go
container, err := postgres.RunContainer(ctx,
/* Other module options */
testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Cmd: []string{"-c", "log_statement=all"},
},
}),
)
```

This option will merge the customized request into the module's request, appending any additional `Cmd` arguments to the
module's command. This can't be used to replace the command, only to append options.
Check the individual module's pages for more information on their commands.

## Executing a command

You can execute a command inside a running container, similar to a `docker exec` call:
Expand Down
3 changes: 3 additions & 0 deletions docs/features/wait/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Variations on the HTTP wait strategy are supported, including:
[Waiting for an HTTP endpoint using image's default port](../../../wait/http_test.go) inside_block:waitForHTTPWithDefaultPort
<!--/codeinclude-->

!!!tip
The HTTP endpoint wait strategy will default to the first port exported/published by the image.

## Match an HTTP method with Port

<!--codeinclude-->
Expand Down
11 changes: 11 additions & 0 deletions docs/modules/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,14 @@ It's possible to use the Postgres container with Timescale or Postgis, to name a
<!--codeinclude-->
[Image for Postgis](../../modules/postgres/postgres_test.go) inside_block:postgis
<!--/codeinclude-->

## Examples

### Using Snapshots
This example shows the usage of the postgres module's Snapshot feature to give each test a clean database without having
to recreate the database container on every test or run heavy scripts to clean your database. This makes the individual
tests very modular, since they always run on a brand-new database.

<!--codeinclude-->
[Test with a reusable Postgres container](../../modules/postgres/postgres_test.go) inside_block:snapshotAndReset
<!--/codeinclude-->
5 changes: 5 additions & 0 deletions modules/postgres/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/docker/go-connections v0.5.0
github.com/jackc/pgx/v5 v5.5.3
github.com/lib/pq v1.10.9
github.com/stretchr/testify v1.8.4
github.com/testcontainers/testcontainers-go v0.27.0
Expand All @@ -30,6 +31,8 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
Expand All @@ -54,9 +57,11 @@ require (
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.58.3 // indirect
Expand Down
13 changes: 12 additions & 1 deletion modules/postgres/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s=
github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
Expand Down Expand Up @@ -95,6 +102,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down Expand Up @@ -123,6 +131,8 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
Expand Down Expand Up @@ -152,7 +162,8 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down
90 changes: 87 additions & 3 deletions modules/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ const (
defaultUser = "postgres"
defaultPassword = "postgres"
defaultPostgresImage = "docker.io/postgres:11-alpine"
defaultSnapshotName = "migrated_template"
)

// PostgresContainer represents the postgres container type used in the module
type PostgresContainer struct {
testcontainers.Container
dbName string
user string
password string
dbName string
user string
password string
snapshotName string
}

// ConnectionString returns the connection string for the postgres container, using the default 5432 port, and
Expand Down Expand Up @@ -141,3 +143,85 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize

return &PostgresContainer{Container: container, dbName: dbName, password: password, user: user}, nil
}

type snapshotConfig struct {
snapshotName string
}

// SnapshotOption is the type for passing options to the snapshot function of the database
type SnapshotOption func(container *snapshotConfig) *snapshotConfig

// WithSnapshotName adds a specific name to the snapshot database created from the main database defined on the
// container. The snapshot must not have the same name as your main database, otherwise it will be overwritten
func WithSnapshotName(name string) SnapshotOption {
return func(container *snapshotConfig) *snapshotConfig {
container.snapshotName = name
return container
Minivera marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Snapshot takes a snapshot of the current state of the database as a template, which can then be restored using
// the Restore method. By default, the snapshot will be created under a database called migrated_template, you can
// customize the snapshot name with the options.
// If a snapshot already exists under the given/default name, it will be overwritten with the new snapshot.
func (c *PostgresContainer) Snapshot(ctx context.Context, opts ...SnapshotOption) error {
config := &snapshotConfig{}
for _, opt := range opts {
config = opt(config)
}

snapshotName := defaultSnapshotName
if config.snapshotName != "" {
snapshotName = config.snapshotName
}

// Drop the snapshot database if it already exists
_, _, err := c.Exec(ctx, []string{"psql", "-U", c.user, "-c", fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, snapshotName)})
if err != nil {
return err
}

// Create a copy of the database to another database to use as a template now that it was fully migrated
_, _, err = c.Exec(ctx, []string{"psql", "-U", c.user, "-c", fmt.Sprintf(`CREATE DATABASE "%s" WITH TEMPLATE "%s" OWNER "%s"`, snapshotName, c.dbName, c.user)})
if err != nil {
return err
}

// Snapshot the template database so we can restore it onto our original database going forward
_, _, err = c.Exec(ctx, []string{"psql", "-U", c.user, "-c", fmt.Sprintf(`ALTER DATABASE "%s" WITH is_template = TRUE`, snapshotName)})
if err != nil {
return err
}

c.snapshotName = snapshotName

return nil
}

// Restore will restore the database to a specific snapshot. By default, it will restore the last snapshot taken on the
// database by the Snapshot method. If a snapshot name is provided, it will instead try to restore the snapshot by name.
func (c *PostgresContainer) Restore(ctx context.Context, opts ...SnapshotOption) error {
config := &snapshotConfig{}
for _, opt := range opts {
config = opt(config)
}

snapshotName := c.snapshotName
if config.snapshotName != "" {
snapshotName = config.snapshotName
}

// Drop the entire database by connecting to the postgres global database
_, _, err := c.Exec(ctx, []string{"psql", "-U", c.user, "-d", "postgres", "-c", fmt.Sprintf(`DROP DATABASE "%s" with (FORCE)`, c.dbName)})
if err != nil {
return err
}

// Then restore the previous snapshot
_, _, err = c.Exec(ctx, []string{"psql", "-U", c.user, "-d", "postgres", "-c", fmt.Sprintf(`CREATE DATABASE "%s" WITH TEMPLATE "%s" OWNER "%s"`, c.dbName, snapshotName, c.user)})
if err != nil {
return err
}

return nil
}
Loading