From 4a9c7fedc1955021fe80f47040dabfa57b70f26b Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Sun, 25 Feb 2024 20:17:43 +0000 Subject: [PATCH 01/17] chore!: return error from Customize Change ContainerCustomizer.Customize method to return an error so that options can handle errors gracefully instead of relying on panic or just a log entry, neither of which are user friendly. Enable errcheck linter to ensure that errors that aren't handled are reported. Run go mod tidy on k3s and weaviate to allow tests to be run using go 1.22. Run gofumpt on a few files to satisfy golangci-lint. Fix direct comparison with http.ErrServerClosed flagged by errcheck. Fixes #2266 BREAKING CHANGE: `ContainerCustomizer.Customize` now returns an error. --- .golangci.yml | 1 + docker.go | 1 - docs/features/common_functional_options.md | 17 +++++++ modules/artemis/artemis.go | 16 +++++-- modules/cassandra/cassandra.go | 12 +++-- modules/chroma/chroma.go | 4 +- modules/clickhouse/clickhouse.go | 38 ++++++++++----- modules/cockroachdb/cockroachdb.go | 4 +- modules/cockroachdb/options.go | 3 +- modules/compose/compose_api.go | 1 - modules/consul/consul.go | 12 +++-- modules/couchbase/couchbase.go | 8 +++- modules/couchbase/options.go | 9 ++-- modules/elasticsearch/elasticsearch.go | 4 +- modules/elasticsearch/options.go | 3 +- modules/gcloud/bigquery.go | 5 +- modules/gcloud/bigtable.go | 5 +- modules/gcloud/datastore.go | 5 +- modules/gcloud/firestore.go | 5 +- modules/gcloud/gcloud.go | 11 +++-- modules/gcloud/pubsub.go | 5 +- modules/gcloud/spanner.go | 5 +- modules/inbucket/inbucket.go | 4 +- modules/k3s/k3s.go | 8 +++- modules/k6/k6.go | 20 ++++++-- modules/kafka/kafka.go | 8 +++- modules/localstack/localstack.go | 4 +- modules/localstack/types.go | 8 +++- modules/mariadb/mariadb.go | 36 ++++++++++---- modules/milvus/milvus.go | 4 +- modules/minio/minio.go | 12 +++-- modules/mockserver/mockserver.go | 4 +- modules/mongodb/mongodb.go | 12 +++-- modules/mssql/mssql.go | 12 +++-- modules/mysql/mysql.go | 28 ++++++++--- modules/nats/nats.go | 4 +- modules/nats/options.go | 3 +- modules/neo4j/config.go | 41 +++++++++++----- modules/neo4j/neo4j.go | 4 +- modules/openldap/openldap.go | 20 ++++++-- modules/opensearch/opensearch.go | 4 +- modules/opensearch/options.go | 3 +- modules/postgres/postgres.go | 24 +++++++--- modules/pulsar/pulsar.go | 20 ++++++-- modules/qdrant/qdrant.go | 4 +- modules/rabbitmq/options.go | 3 +- modules/rabbitmq/rabbitmq.go | 20 ++++++-- modules/redis/options_test.go | 9 ++-- modules/redis/redis.go | 17 +++++-- modules/redpanda/options.go | 5 +- modules/redpanda/redpanda.go | 5 +- modules/vault/vault.go | 12 +++-- modules/weaviate/weaviate.go | 4 +- network/network.go | 56 ++++++++++++++-------- network/network_test.go | 12 +++-- options.go | 54 ++++++++++++++------- options_test.go | 14 ++++-- reaper.go | 1 - wait/testdata/main.go | 3 +- 59 files changed, 487 insertions(+), 189 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 37f3bdc262..1791b9caac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,6 +7,7 @@ linters: - misspell - nonamedreturns - testifylint + - errcheck linters-settings: errorlint: diff --git a/docker.go b/docker.go index cb799dc05a..c6300172ce 100644 --- a/docker.go +++ b/docker.go @@ -1495,7 +1495,6 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl Attachable: true, Labels: core.DefaultLabels(core.SessionID()), }) - if err != nil { return "", err } diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md index 288093941c..28e53aca42 100644 --- a/docs/features/common_functional_options.md +++ b/docs/features/common_functional_options.md @@ -152,3 +152,20 @@ The above example is updating the predefined command of the image, **appending** !!!info This can't be used to replace the command, only to append options. + +!!!warning + The interface definition for `ContainerCustomizer` was changed to allow + errors the be correctly processed, specifically `Customize` method was + changed from: + +```go +Customize(req *GenericContainerRequest) +``` + +To: + +```go +Customize(req *GenericContainerRequest) error +``` + +- Not available until the next release of testcontainers-go :material-tag: main diff --git a/modules/artemis/artemis.go b/modules/artemis/artemis.go index a3c26702ad..33e03583cc 100644 --- a/modules/artemis/artemis.go +++ b/modules/artemis/artemis.go @@ -49,16 +49,20 @@ func (c *Container) ConsoleURL(ctx context.Context) (string, error) { // WithCredentials sets the administrator credentials. The default is artemis:artemis. func WithCredentials(user, password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["ARTEMIS_USER"] = user req.Env["ARTEMIS_PASSWORD"] = password + + return nil } } // WithAnonymousLogin enables anonymous logins. func WithAnonymousLogin() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["ANONYMOUS_LOGIN"] = "true" + + return nil } } @@ -67,8 +71,10 @@ func WithAnonymousLogin() testcontainers.CustomizeRequestOption { // Setting this value will override the default. // See the documentation on `artemis create` for available options. func WithExtraArgs(args string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["EXTRA_ARGS"] = args + + return nil } } @@ -91,7 +97,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&req) + if err := opt.Customize(&req); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, req) diff --git a/modules/cassandra/cassandra.go b/modules/cassandra/cassandra.go index c37c10d90d..10df6e991c 100644 --- a/modules/cassandra/cassandra.go +++ b/modules/cassandra/cassandra.go @@ -41,19 +41,21 @@ func (c *CassandraContainer) ConnectionHost(ctx context.Context) (string, error) // It will also set the "configFile" parameter to the path of the config file // as a command line argument to the container. func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/cassandra/cassandra.yaml", FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } // WithInitScripts sets the init cassandra queries to be run when the container starts func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { var initScripts []testcontainers.ContainerFile var execs []testcontainers.Executable for _, script := range scripts { @@ -68,7 +70,7 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { } req.Files = append(req.Files, initScripts...) - testcontainers.WithAfterReadyCommand(execs...)(req) + return testcontainers.WithAfterReadyCommand(execs...)(req) } } @@ -100,7 +102,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/chroma/chroma.go b/modules/chroma/chroma.go index 239e0e6fa0..8e79348807 100644 --- a/modules/chroma/chroma.go +++ b/modules/chroma/chroma.go @@ -33,7 +33,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/clickhouse/clickhouse.go b/modules/clickhouse/clickhouse.go index b133d98752..5c74ffa488 100644 --- a/modules/clickhouse/clickhouse.go +++ b/modules/clickhouse/clickhouse.go @@ -101,10 +101,10 @@ func renderZookeeperConfig(settings ZookeeperOptions) ([]byte, error) { // WithZookeeper pass a config to connect clickhouse with zookeeper and make clickhouse as cluster func WithZookeeper(host, port string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { f, err := os.CreateTemp("", "clickhouse-tc-config-") if err != nil { - panic(err) + return fmt.Errorf("temporary file: %w", err) } defer f.Close() @@ -112,10 +112,10 @@ func WithZookeeper(host, port string) testcontainers.CustomizeRequestOption { // write data to the temporary file data, err := renderZookeeperConfig(ZookeeperOptions{Host: host, Port: port}) if err != nil { - panic(err) + return fmt.Errorf("zookeeper config: %w", err) } if _, err := f.Write(data); err != nil { - panic(err) + return fmt.Errorf("write zookeeper config: %w", err) } cf := testcontainers.ContainerFile{ HostFilePath: f.Name(), @@ -123,12 +123,14 @@ func WithZookeeper(host, port string) testcontainers.CustomizeRequestOption { FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } // WithInitScripts sets the init scripts to be run when the container starts func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { initScripts := []testcontainers.ContainerFile{} for _, script := range scripts { cf := testcontainers.ContainerFile{ @@ -139,6 +141,8 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { initScripts = append(initScripts, cf) } req.Files = append(req.Files, initScripts...) + + return nil } } @@ -146,13 +150,15 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { // It will also set the "configFile" parameter to the path of the config file // as a command line argument to the container. func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/clickhouse-server/config.d/config.xml", FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } @@ -160,13 +166,15 @@ func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { // It will also set the "configFile" parameter to the path of the config file // as a command line argument to the container. func WithYamlConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/clickhouse-server/config.d/config.yaml", FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } @@ -174,8 +182,10 @@ func WithYamlConfigFile(configFile string) testcontainers.CustomizeRequestOption // It can be used to define a different name for the default database that is created when the image is first started. // If it is not specified, then the default value("clickhouse") will be used. func WithDatabase(dbName string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["CLICKHOUSE_DB"] = dbName + + return nil } } @@ -183,8 +193,10 @@ func WithDatabase(dbName string) testcontainers.CustomizeRequestOption { // It is required for you to use the ClickHouse image. It must not be empty or undefined. // This environment variable sets the password for ClickHouse. func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["CLICKHOUSE_PASSWORD"] = password + + return nil } } @@ -193,12 +205,14 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption { // It will create the specified user with superuser power. // If it is not specified, then the default user of clickhouse will be used. func WithUsername(user string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { if user == "" { user = defaultUser } req.Env["CLICKHOUSE_USER"] = user + + return nil } } @@ -225,7 +239,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/cockroachdb/cockroachdb.go b/modules/cockroachdb/cockroachdb.go index 14cd78292f..7ab24b98fb 100644 --- a/modules/cockroachdb/cockroachdb.go +++ b/modules/cockroachdb/cockroachdb.go @@ -96,7 +96,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize if apply, ok := opt.(Option); ok { apply(&o) } - opt.Customize(&req) + if err := opt.Customize(&req); err != nil { + return nil, err + } } // modify request diff --git a/modules/cockroachdb/options.go b/modules/cockroachdb/options.go index 84cc143593..a2211d77e7 100644 --- a/modules/cockroachdb/options.go +++ b/modules/cockroachdb/options.go @@ -26,8 +26,9 @@ var _ testcontainers.ContainerCustomizer = (*Option)(nil) type Option func(*options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o Option) Customize(*testcontainers.GenericContainerRequest) { +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } // WithDatabase sets the name of the database to use. diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go index b0ec87ed29..aabee1caa1 100644 --- a/modules/compose/compose_api.go +++ b/modules/compose/compose_api.go @@ -231,7 +231,6 @@ func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) error { Wait: upOptions.Wait, }, }) - if err != nil { return err } diff --git a/modules/consul/consul.go b/modules/consul/consul.go index e4ec01fd02..b1c79630b9 100644 --- a/modules/consul/consul.go +++ b/modules/consul/consul.go @@ -40,20 +40,24 @@ func (c *ConsulContainer) ApiEndpoint(ctx context.Context) (string, error) { // WithConfigString takes in a JSON string of keys and values to define a configuration to be used by the instance. func WithConfigString(config string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["CONSUL_LOCAL_CONFIG"] = config + + return nil } } // WithConfigFile takes in a path to a JSON file to define a configuration to be used by the instance. func WithConfigFile(configPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configPath, ContainerFilePath: "/consul/config/node.json", FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } @@ -77,7 +81,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&containerReq) + if err := opt.Customize(&containerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, containerReq) diff --git a/modules/couchbase/couchbase.go b/modules/couchbase/couchbase.go index c81c420c8b..d9b468edb0 100644 --- a/modules/couchbase/couchbase.go +++ b/modules/couchbase/couchbase.go @@ -85,7 +85,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } // transfer options to the config @@ -663,10 +665,12 @@ type serviceCustomizer struct { enabledService Service } -func (c serviceCustomizer) Customize(req *testcontainers.GenericContainerRequest) { +func (c serviceCustomizer) Customize(req *testcontainers.GenericContainerRequest) error { for _, port := range c.enabledService.ports { req.ExposedPorts = append(req.ExposedPorts, port+"/tcp") } + + return nil } // withService creates a serviceCustomizer for the given service. diff --git a/modules/couchbase/options.go b/modules/couchbase/options.go index 51c60009c9..64acf24292 100644 --- a/modules/couchbase/options.go +++ b/modules/couchbase/options.go @@ -40,8 +40,9 @@ type credentialsCustomizer struct { password string } -func (c credentialsCustomizer) Customize(req *testcontainers.GenericContainerRequest) { +func (c credentialsCustomizer) Customize(req *testcontainers.GenericContainerRequest) error { // NOOP, we want to simply transfer the credentials to the container + return nil } // WithAdminCredentials sets the username and password for the administrator user. @@ -73,8 +74,9 @@ type bucketCustomizer struct { buckets []bucket } -func (c bucketCustomizer) Customize(req *testcontainers.GenericContainerRequest) { +func (c bucketCustomizer) Customize(req *testcontainers.GenericContainerRequest) error { // NOOP, we want to simply transfer the buckets to the container + return nil } // WithBucket adds buckets to the couchbase container @@ -96,8 +98,9 @@ type indexStorageCustomizer struct { mode indexStorageMode } -func (c indexStorageCustomizer) Customize(req *testcontainers.GenericContainerRequest) { +func (c indexStorageCustomizer) Customize(req *testcontainers.GenericContainerRequest) error { // NOOP, we want to simply transfer the index storage mode to the container + return nil } // WithBucket adds buckets to the couchbase container diff --git a/modules/elasticsearch/elasticsearch.go b/modules/elasticsearch/elasticsearch.go index 79a364fd01..66a9e0fbcb 100644 --- a/modules/elasticsearch/elasticsearch.go +++ b/modules/elasticsearch/elasticsearch.go @@ -65,7 +65,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize if apply, ok := opt.(Option); ok { apply(settings) } - opt.Customize(&req) + if err := opt.Customize(&req); err != nil { + return nil, err + } } // Transfer the certificate settings to the container request diff --git a/modules/elasticsearch/options.go b/modules/elasticsearch/options.go index 97f75f5c52..ed801c3b09 100644 --- a/modules/elasticsearch/options.go +++ b/modules/elasticsearch/options.go @@ -28,8 +28,9 @@ var _ testcontainers.ContainerCustomizer = (*Option)(nil) type Option func(*Options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o Option) Customize(*testcontainers.GenericContainerRequest) { +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } // WithPassword sets the password for the Elasticsearch container. diff --git a/modules/gcloud/bigquery.go b/modules/gcloud/bigquery.go index b9672d56b8..fe93f54485 100644 --- a/modules/gcloud/bigquery.go +++ b/modules/gcloud/bigquery.go @@ -20,7 +20,10 @@ func RunBigQueryContainer(ctx context.Context, opts ...testcontainers.ContainerC Started: true, } - settings := applyOptions(&req, opts) + settings, err := applyOptions(&req, opts) + if err != nil { + return nil, err + } req.Cmd = []string{"--project", settings.ProjectID} diff --git a/modules/gcloud/bigtable.go b/modules/gcloud/bigtable.go index 664d01dccc..8294ad2678 100644 --- a/modules/gcloud/bigtable.go +++ b/modules/gcloud/bigtable.go @@ -19,7 +19,10 @@ func RunBigTableContainer(ctx context.Context, opts ...testcontainers.ContainerC Started: true, } - settings := applyOptions(&req, opts) + settings, err := applyOptions(&req, opts) + if err != nil { + return nil, err + } req.Cmd = []string{ "/bin/sh", diff --git a/modules/gcloud/datastore.go b/modules/gcloud/datastore.go index b4f0c38735..72b487f12b 100644 --- a/modules/gcloud/datastore.go +++ b/modules/gcloud/datastore.go @@ -19,7 +19,10 @@ func RunDatastoreContainer(ctx context.Context, opts ...testcontainers.Container Started: true, } - settings := applyOptions(&req, opts) + settings, err := applyOptions(&req, opts) + if err != nil { + return nil, err + } req.Cmd = []string{ "/bin/sh", diff --git a/modules/gcloud/firestore.go b/modules/gcloud/firestore.go index 413ce14a5c..ee998a55b3 100644 --- a/modules/gcloud/firestore.go +++ b/modules/gcloud/firestore.go @@ -19,7 +19,10 @@ func RunFirestoreContainer(ctx context.Context, opts ...testcontainers.Container Started: true, } - settings := applyOptions(&req, opts) + settings, err := applyOptions(&req, opts) + if err != nil { + return nil, err + } req.Cmd = []string{ "/bin/sh", diff --git a/modules/gcloud/gcloud.go b/modules/gcloud/gcloud.go index 83ff0f0854..a5886dc743 100644 --- a/modules/gcloud/gcloud.go +++ b/modules/gcloud/gcloud.go @@ -57,8 +57,9 @@ var _ testcontainers.ContainerCustomizer = (*Option)(nil) type Option func(*options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o Option) Customize(*testcontainers.GenericContainerRequest) { +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } // WithProjectID sets the project ID for the GCloud container. @@ -69,14 +70,16 @@ func WithProjectID(projectID string) Option { } // applyOptions applies the options to the container request and returns the settings. -func applyOptions(req *testcontainers.GenericContainerRequest, opts []testcontainers.ContainerCustomizer) options { +func applyOptions(req *testcontainers.GenericContainerRequest, opts []testcontainers.ContainerCustomizer) (options, error) { settings := defaultOptions() for _, opt := range opts { if apply, ok := opt.(Option); ok { apply(&settings) } - opt.Customize(req) + if err := opt.Customize(req); err != nil { + return options{}, err + } } - return settings + return settings, nil } diff --git a/modules/gcloud/pubsub.go b/modules/gcloud/pubsub.go index 65d87ae1ad..bf83f3a2f5 100644 --- a/modules/gcloud/pubsub.go +++ b/modules/gcloud/pubsub.go @@ -19,7 +19,10 @@ func RunPubsubContainer(ctx context.Context, opts ...testcontainers.ContainerCus Started: true, } - settings := applyOptions(&req, opts) + settings, err := applyOptions(&req, opts) + if err != nil { + return nil, err + } req.Cmd = []string{ "/bin/sh", diff --git a/modules/gcloud/spanner.go b/modules/gcloud/spanner.go index 12420eae14..eb7d10ea8c 100644 --- a/modules/gcloud/spanner.go +++ b/modules/gcloud/spanner.go @@ -18,7 +18,10 @@ func RunSpannerContainer(ctx context.Context, opts ...testcontainers.ContainerCu Started: true, } - settings := applyOptions(&req, opts) + settings, err := applyOptions(&req, opts) + if err != nil { + return nil, err + } container, err := testcontainers.GenericContainer(ctx, req) if err != nil { diff --git a/modules/inbucket/inbucket.go b/modules/inbucket/inbucket.go index d130c87bb0..c446404df3 100644 --- a/modules/inbucket/inbucket.go +++ b/modules/inbucket/inbucket.go @@ -61,7 +61,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 83fdde4338..5327baeb71 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -84,7 +84,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) @@ -99,7 +101,9 @@ func getContainerHost(ctx context.Context, opts ...testcontainers.ContainerCusto // Use a dummy request to get the provider from options. var req testcontainers.GenericContainerRequest for _, opt := range opts { - opt.Customize(&req) + if err := opt.Customize(&req); err != nil { + return "", err + } } logging := req.Logger diff --git a/modules/k6/k6.go b/modules/k6/k6.go index 82976b7ea3..7b31687e6d 100644 --- a/modules/k6/k6.go +++ b/modules/k6/k6.go @@ -21,7 +21,7 @@ type K6Container struct { // and passes it to k6 as the test to run. // The path to the script must be an absolute path func WithTestScript(scriptPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { script := filepath.Base(scriptPath) target := "/home/k6x/" + script req.Files = append( @@ -35,20 +35,26 @@ func WithTestScript(scriptPath string) testcontainers.CustomizeRequestOption { // add script to the k6 run command req.Cmd = append(req.Cmd, target) + + return nil } } // WithCmdOptions pass the given options to the k6 run command func WithCmdOptions(options ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Cmd = append(req.Cmd, options...) + + return nil } } // SetEnvVar adds a '--env' command-line flag to the k6 command in the container for setting an environment variable for the test script. func SetEnvVar(variable string, value string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Cmd = append(req.Cmd, "--env", fmt.Sprintf("%s=%s", variable, value)) + + return nil } } @@ -68,7 +74,7 @@ func WithCache() testcontainers.CustomizeRequestOption { } } - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { mount := testcontainers.ContainerMount{ Source: testcontainers.DockerVolumeMountSource{ Name: cacheVol, @@ -77,6 +83,8 @@ func WithCache() testcontainers.CustomizeRequestOption { Target: "/cache", } req.Mounts = append(req.Mounts, mount) + + return nil } } @@ -94,7 +102,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/kafka/kafka.go b/modules/kafka/kafka.go index 399f17fb70..f5d49e9db9 100644 --- a/modules/kafka/kafka.go +++ b/modules/kafka/kafka.go @@ -96,7 +96,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } err := validateKRaftVersion(genericContainerReq.Image) @@ -117,8 +119,10 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } func WithClusterID(clusterID string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["CLUSTER_ID"] = clusterID + + return nil } } diff --git a/modules/localstack/localstack.go b/modules/localstack/localstack.go index c2e8d64d26..ae4c7c8c01 100644 --- a/modules/localstack/localstack.go +++ b/modules/localstack/localstack.go @@ -90,7 +90,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&localStackReq.GenericContainerRequest) + if err := opt.Customize(&localStackReq.GenericContainerRequest); err != nil { + return nil, err + } } if isLegacyMode(localStackReq.Image) { diff --git a/modules/localstack/types.go b/modules/localstack/types.go index a57ae1808f..962975841a 100644 --- a/modules/localstack/types.go +++ b/modules/localstack/types.go @@ -26,8 +26,10 @@ var NoopOverrideContainerRequest = func(req testcontainers.ContainerRequest) tes return req } -func (opt OverrideContainerRequestOption) Customize(req *testcontainers.GenericContainerRequest) { +func (opt OverrideContainerRequestOption) Customize(req *testcontainers.GenericContainerRequest) error { req.ContainerRequest = opt(req.ContainerRequest) + + return nil } // OverrideContainerRequest returns a function that can be used to merge the passed container request with one that is created by the LocalStack container @@ -43,7 +45,9 @@ func OverrideContainerRequest(r testcontainers.ContainerRequest) func(req testco } opt := testcontainers.CustomizeRequest(destContainerReq) - opt.Customize(&srcContainerReq) + if err := opt.Customize(&srcContainerReq); err != nil { + panic(err) + } return srcContainerReq.ContainerRequest } diff --git a/modules/mariadb/mariadb.go b/modules/mariadb/mariadb.go index b84d82c2ec..9ad5c6f6b8 100644 --- a/modules/mariadb/mariadb.go +++ b/modules/mariadb/mariadb.go @@ -33,7 +33,7 @@ type MariaDBContainer struct { // WithDefaultCredentials applies the default credentials to the container request. // It will look up for MARIADB environment variables. func WithDefaultCredentials() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { username := req.Env["MARIADB_USER"] password := req.Env["MARIADB_PASSWORD"] if strings.EqualFold(rootUser, username) { @@ -46,6 +46,8 @@ func WithDefaultCredentials() testcontainers.CustomizeRequestOption { req.Env["MARIADB_ALLOW_EMPTY_ROOT_PASSWORD"] = "yes" delete(req.Env, "MARIADB_PASSWORD") } + + return nil } } @@ -54,7 +56,7 @@ func WithDefaultCredentials() testcontainers.CustomizeRequestOption { // the MARIADB_* equivalent variables are provided. MARIADB_* variants will always be // used in preference to MYSQL_* variants. func withMySQLEnvVars() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { // look up for MARIADB environment variables and apply the same to MYSQL for k, v := range req.Env { if strings.HasPrefix(k, "MARIADB_") { @@ -63,40 +65,50 @@ func withMySQLEnvVars() testcontainers.CustomizeRequestOption { req.Env[mysqlEnvVar] = v } } + + return nil } } func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MARIADB_USER"] = username + + return nil } } func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MARIADB_PASSWORD"] = password + + return nil } } func WithDatabase(database string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MARIADB_DATABASE"] = database + + return nil } } func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/mysql/conf.d/my.cnf", FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } func WithScripts(scripts ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { var initScripts []testcontainers.ContainerFile for _, script := range scripts { cf := testcontainers.ContainerFile{ @@ -107,6 +119,8 @@ func WithScripts(scripts ...string) testcontainers.CustomizeRequestOption { initScripts = append(initScripts, cf) } req.Files = append(req.Files, initScripts...) + + return nil } } @@ -131,13 +145,17 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize opts = append(opts, WithDefaultCredentials()) for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } // Apply MySQL environment variables after user customization // In future releases of MariaDB, they could remove the MYSQL_* environment variables // at all. Then we can remove this customization. - withMySQLEnvVars().Customize(&genericContainerReq) + if err := withMySQLEnvVars().Customize(&genericContainerReq); err != nil { + return nil, err + } username, ok := req.Env["MARIADB_USER"] if !ok { diff --git a/modules/milvus/milvus.go b/modules/milvus/milvus.go index 5f64021a93..a33ae27935 100644 --- a/modules/milvus/milvus.go +++ b/modules/milvus/milvus.go @@ -67,7 +67,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/minio/minio.go b/modules/minio/minio.go index e61d2a2913..b81586934a 100644 --- a/modules/minio/minio.go +++ b/modules/minio/minio.go @@ -25,8 +25,10 @@ type MinioContainer struct { // It is used in conjunction with WithPassword to set a user and its password. // It will create the specified user. It must not be empty or undefined. func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MINIO_ROOT_USER"] = username + + return nil } } @@ -34,8 +36,10 @@ func WithUsername(username string) testcontainers.CustomizeRequestOption { // It is required for you to use the Minio image. It must not be empty or undefined. // This environment variable sets the root user password for Minio. func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MINIO_ROOT_PASSWORD"] = password + + return nil } } @@ -72,7 +76,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } username := req.Env["MINIO_ROOT_USER"] diff --git a/modules/mockserver/mockserver.go b/modules/mockserver/mockserver.go index 37ebc1b1b5..e3bb5fbd28 100644 --- a/modules/mockserver/mockserver.go +++ b/modules/mockserver/mockserver.go @@ -34,7 +34,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 761aa99e54..565e8bc466 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -36,7 +36,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } username := req.Env["MONGO_INITDB_ROOT_USERNAME"] password := req.Env["MONGO_INITDB_ROOT_PASSWORD"] @@ -59,8 +61,10 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize // It is used in conjunction with WithPassword to set a username and its password. // It will create the specified user with superuser power. func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MONGO_INITDB_ROOT_USERNAME"] = username + + return nil } } @@ -68,8 +72,10 @@ func WithUsername(username string) testcontainers.CustomizeRequestOption { // It is used in conjunction with WithUsername to set a username and its password. // It will set the superuser password for MongoDB. func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MONGO_INITDB_ROOT_PASSWORD"] = password + + return nil } } diff --git a/modules/mssql/mssql.go b/modules/mssql/mssql.go index 386df760d4..78321beab3 100644 --- a/modules/mssql/mssql.go +++ b/modules/mssql/mssql.go @@ -24,17 +24,21 @@ type MSSQLServerContainer struct { } func WithAcceptEULA() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["ACCEPT_EULA"] = "Y" + + return nil } } func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { if password == "" { password = defaultPassword } req.Env["MSSQL_SA_PASSWORD"] = password + + return nil } } @@ -55,7 +59,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/mysql/mysql.go b/modules/mysql/mysql.go index 2ebfce1710..9c7552d0ea 100644 --- a/modules/mysql/mysql.go +++ b/modules/mysql/mysql.go @@ -31,7 +31,7 @@ type MySQLContainer struct { } func WithDefaultCredentials() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { username := req.Env["MYSQL_USER"] password := req.Env["MYSQL_PASSWORD"] if strings.EqualFold(rootUser, username) { @@ -43,6 +43,8 @@ func WithDefaultCredentials() testcontainers.CustomizeRequestOption { req.Env["MYSQL_ALLOW_EMPTY_PASSWORD"] = "yes" delete(req.Env, "MYSQL_PASSWORD") } + + return nil } } @@ -67,7 +69,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize opts = append(opts, WithDefaultCredentials()) for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } username, ok := req.Env["MYSQL_USER"] @@ -114,36 +118,44 @@ func (c *MySQLContainer) ConnectionString(ctx context.Context, args ...string) ( } func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MYSQL_USER"] = username + + return nil } } func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MYSQL_PASSWORD"] = password + + return nil } } func WithDatabase(database string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["MYSQL_DATABASE"] = database + + return nil } } func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/mysql/conf.d/my.cnf", FileMode: 0o755, } req.Files = append(req.Files, cf) + + return nil } } func WithScripts(scripts ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { var initScripts []testcontainers.ContainerFile for _, script := range scripts { cf := testcontainers.ContainerFile{ @@ -154,5 +166,7 @@ func WithScripts(scripts ...string) testcontainers.CustomizeRequestOption { initScripts = append(initScripts, cf) } req.Files = append(req.Files, initScripts...) + + return nil } } diff --git a/modules/nats/nats.go b/modules/nats/nats.go index 42cebc1590..aa0b501d16 100644 --- a/modules/nats/nats.go +++ b/modules/nats/nats.go @@ -41,7 +41,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize if apply, ok := opt.(CmdOption); ok { apply(&settings) } - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } // Include the command line arguments diff --git a/modules/nats/options.go b/modules/nats/options.go index f43cf9b9d5..38856d68a9 100644 --- a/modules/nats/options.go +++ b/modules/nats/options.go @@ -23,8 +23,9 @@ var _ testcontainers.ContainerCustomizer = (*CmdOption)(nil) type CmdOption func(opts *options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o CmdOption) Customize(req *testcontainers.GenericContainerRequest) { +func (o CmdOption) Customize(req *testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } func WithUsername(username string) CmdOption { diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go index 9d0bc70816..7b469e0bbe 100644 --- a/modules/neo4j/config.go +++ b/modules/neo4j/config.go @@ -32,20 +32,22 @@ func WithoutAuthentication() testcontainers.CustomizeRequestOption { // An empty string disables authentication. // The default password is "password". func WithAdminPassword(adminPassword string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { pwd := "none" if adminPassword != "" { pwd = fmt.Sprintf("neo4j/%s", adminPassword) } req.Env["NEO4J_AUTH"] = pwd + + return nil } } // WithLabsPlugin registers one or more Neo4jLabsPlugin for download and server startup. // There might be plugins not supported by your selected version of Neo4j. func WithLabsPlugin(plugins ...LabsPlugin) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { rawPluginValues := make([]string, len(plugins)) for i := 0; i < len(plugins); i++ { rawPluginValues[i] = string(plugins[i]) @@ -54,6 +56,8 @@ func WithLabsPlugin(plugins ...LabsPlugin) testcontainers.CustomizeRequestOption if len(plugins) > 0 { req.Env["NEO4JLABS_PLUGINS"] = fmt.Sprintf(`["%s"]`, strings.Join(rawPluginValues, `","`)) } + + return nil } } @@ -64,8 +68,8 @@ func WithLabsPlugin(plugins ...LabsPlugin) testcontainers.CustomizeRequestOption // See WithNeo4jSettings to add multiple settings at once // Note: credentials must be configured with WithAdminPassword func WithNeo4jSetting(key, value string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { - addSetting(req, key, value) + return func(req *testcontainers.GenericContainerRequest) error { + return addSetting(req, key, value) } } @@ -76,33 +80,40 @@ func WithNeo4jSetting(key, value string) testcontainers.CustomizeRequestOption { // See WithNeo4jSetting to add a single setting // Note: credentials must be configured with WithAdminPassword func WithNeo4jSettings(settings map[string]string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { for key, value := range settings { - addSetting(req, key, value) + if err := addSetting(req, key, value); err != nil { + return err + } } + + return nil } } // WithLogger sets a custom logger to be used by the container // Consider calling this before other "With functions" as these may generate logs func WithLogger(logger testcontainers.Logging) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Logger = logger + + return nil } } -func addSetting(req *testcontainers.GenericContainerRequest, key string, newVal string) { +func addSetting(req *testcontainers.GenericContainerRequest, key string, newVal string) error { normalizedKey := formatNeo4jConfig(key) if oldVal, found := req.Env[normalizedKey]; found { // make sure AUTH is not overwritten by a setting if key == "AUTH" { - req.Logger.Printf("setting %q is not permitted, WithAdminPassword has already been set\n", normalizedKey) - return + return fmt.Errorf("setting %q is not permitted, WithAdminPassword has already been set", normalizedKey) } - req.Logger.Printf("setting %q with value %q is now overwritten with value %q\n", []any{key, oldVal, newVal}...) + return fmt.Errorf("setting %q with value %q is now overwritten with value %q", []any{key, oldVal, newVal}...) } req.Env[normalizedKey] = newVal + + return nil } func validate(req *testcontainers.GenericContainerRequest) error { @@ -123,8 +134,10 @@ func formatNeo4jConfig(name string) string { // the commercial licence agreement of Neo4j Enterprise Edition. The license // agreement is available at https://neo4j.com/terms/licensing/. func WithAcceptCommercialLicenseAgreement() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["NEO4J_ACCEPT_LICENSE_AGREEMENT"] = "yes" + + return nil } } @@ -134,7 +147,9 @@ func WithAcceptCommercialLicenseAgreement() testcontainers.CustomizeRequestOptio // agreement is available at https://neo4j.com/terms/enterprise_us/. Please // read the terms of the evaluation agreement before you accept. func WithAcceptEvaluationLicenseAgreement() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["NEO4J_ACCEPT_LICENSE_AGREEMENT"] = "eval" + + return nil } } diff --git a/modules/neo4j/neo4j.go b/modules/neo4j/neo4j.go index 1a7f3507bc..70566aa0cd 100644 --- a/modules/neo4j/neo4j.go +++ b/modules/neo4j/neo4j.go @@ -83,7 +83,9 @@ func RunContainer(ctx context.Context, options ...testcontainers.ContainerCustom } for _, option := range options { - option.Customize(&genericContainerReq) + if err := option.Customize(&genericContainerReq); err != nil { + return nil, err + } } err := validate(&genericContainerReq) diff --git a/modules/openldap/openldap.go b/modules/openldap/openldap.go index e29658c639..8e0cc271de 100644 --- a/modules/openldap/openldap.go +++ b/modules/openldap/openldap.go @@ -63,8 +63,10 @@ func (c *OpenLDAPContainer) LoadLdif(ctx context.Context, ldif []byte) error { // It is used in conjunction with WithAdminPassword to set a username and its password. // It will create the specified user with admin power. func WithAdminUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["LDAP_ADMIN_USERNAME"] = username + + return nil } } @@ -72,21 +74,25 @@ func WithAdminUsername(username string) testcontainers.CustomizeRequestOption { // It is used in conjunction with WithAdminUsername to set a username and its password. // It will set the admin password for OpenLDAP. func WithAdminPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["LDAP_ADMIN_PASSWORD"] = password + + return nil } } // WithRoot sets the root of the OpenLDAP instance func WithRoot(root string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["LDAP_ROOT"] = root + + return nil } } // WithInitialLdif sets the initial ldif file to be loaded into the OpenLDAP container func WithInitialLdif(ldif string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Files = append(req.Files, testcontainers.ContainerFile{ HostFilePath: ldif, ContainerFilePath: "/initial_ldif.ldif", @@ -111,6 +117,8 @@ func WithInitialLdif(ldif string) testcontainers.CustomizeRequestOption { }, }, }) + + return nil } } @@ -141,7 +149,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/opensearch/opensearch.go b/modules/opensearch/opensearch.go index 019f68f287..61290c9ed2 100644 --- a/modules/opensearch/opensearch.go +++ b/modules/opensearch/opensearch.go @@ -66,7 +66,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize if apply, ok := opt.(Option); ok { apply(settings) } - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } // set credentials if they are provided, otherwise use the defaults diff --git a/modules/opensearch/options.go b/modules/opensearch/options.go index 3792d08c6f..f1223762a3 100644 --- a/modules/opensearch/options.go +++ b/modules/opensearch/options.go @@ -22,8 +22,9 @@ var _ testcontainers.ContainerCustomizer = (*Option)(nil) type Option func(*Options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o Option) Customize(*testcontainers.GenericContainerRequest) { +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } // WithPassword sets the password for the OpenSearch container. diff --git a/modules/postgres/postgres.go b/modules/postgres/postgres.go index 111a2a0953..43c5c81113 100644 --- a/modules/postgres/postgres.go +++ b/modules/postgres/postgres.go @@ -50,7 +50,7 @@ func (c *PostgresContainer) ConnectionString(ctx context.Context, args ...string // It will also set the "config_file" parameter to the path of the config file // as a command line argument to the container func WithConfigFile(cfg string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cfgFile := testcontainers.ContainerFile{ HostFilePath: cfg, ContainerFilePath: "/etc/postgresql.conf", @@ -59,6 +59,8 @@ func WithConfigFile(cfg string) testcontainers.CustomizeRequestOption { req.Files = append(req.Files, cfgFile) req.Cmd = append(req.Cmd, "-c", "config_file=/etc/postgresql.conf") + + return nil } } @@ -66,14 +68,16 @@ func WithConfigFile(cfg string) testcontainers.CustomizeRequestOption { // It can be used to define a different name for the default database that is created when the image is first started. // If it is not specified, then the value of WithUser will be used. func WithDatabase(dbName string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["POSTGRES_DB"] = dbName + + return nil } } // WithInitScripts sets the init scripts to be run when the container starts func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { initScripts := []testcontainers.ContainerFile{} for _, script := range scripts { cf := testcontainers.ContainerFile{ @@ -84,6 +88,8 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { initScripts = append(initScripts, cf) } req.Files = append(req.Files, initScripts...) + + return nil } } @@ -91,8 +97,10 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { // It is required for you to use the PostgreSQL image. It must not be empty or undefined. // This environment variable sets the superuser password for PostgreSQL. func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["POSTGRES_PASSWORD"] = password + + return nil } } @@ -101,12 +109,14 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption { // It will create the specified user with superuser power and a database with the same name. // If it is not specified, then the default user of postgres will be used. func WithUsername(user string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { if user == "" { user = defaultUser } req.Env["POSTGRES_USER"] = user + + return nil } } @@ -129,7 +139,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/pulsar/pulsar.go b/modules/pulsar/pulsar.go index f8ad59dfd9..bdb45ab69e 100644 --- a/modules/pulsar/pulsar.go +++ b/modules/pulsar/pulsar.go @@ -71,7 +71,7 @@ func (c *Container) resolveURL(ctx context.Context, port nat.Port) (string, erro // WithFunctionsWorker enables the functions worker, which will override the default pulsar command // and add a waiting strategy for the functions worker func WithFunctionsWorker() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Cmd = []string{"/bin/bash", "-c", defaultPulsarCmd} ss := []wait.Strategy{ @@ -81,6 +81,8 @@ func WithFunctionsWorker() testcontainers.CustomizeRequestOption { ss = append(ss, defaultWaitStrategies.Strategies...) req.WaitingFor = wait.ForAll(ss...) + + return nil } } @@ -100,14 +102,18 @@ func (c *Container) WithLogConsumers(ctx context.Context, consumer ...testcontai // WithPulsarEnv allows to use the native APIs and set each variable with PULSAR_PREFIX_ as prefix. func WithPulsarEnv(configVar string, configValue string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["PULSAR_PREFIX_"+configVar] = configValue + + return nil } } func WithTransactions() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { - WithPulsarEnv("transactionCoordinatorEnabled", "true")(req) + return func(req *testcontainers.GenericContainerRequest) error { + if err := WithPulsarEnv("transactionCoordinatorEnabled", "true")(req); err != nil { + return err + } // clone defaultWaitStrategies ss := []wait.Strategy{ @@ -119,6 +125,8 @@ func WithTransactions() testcontainers.CustomizeRequestOption { ss = append(ss, defaultWaitStrategies.Strategies...) req.WaitingFor = wait.ForAll(ss...) + + return nil } } @@ -146,7 +154,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } c, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/qdrant/qdrant.go b/modules/qdrant/qdrant.go index 675a8ebfc8..c5a4244707 100644 --- a/modules/qdrant/qdrant.go +++ b/modules/qdrant/qdrant.go @@ -31,7 +31,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/rabbitmq/options.go b/modules/rabbitmq/options.go index fd4266b0fc..885768ab5e 100644 --- a/modules/rabbitmq/options.go +++ b/modules/rabbitmq/options.go @@ -44,8 +44,9 @@ var _ testcontainers.ContainerCustomizer = (*Option)(nil) type Option func(*options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o Option) Customize(*testcontainers.GenericContainerRequest) { +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } // WithAdminPassword sets the password for the default admin user diff --git a/modules/rabbitmq/rabbitmq.go b/modules/rabbitmq/rabbitmq.go index 9fb28212e1..118b39aca4 100644 --- a/modules/rabbitmq/rabbitmq.go +++ b/modules/rabbitmq/rabbitmq.go @@ -100,11 +100,15 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize if apply, ok := opt.(Option); ok { apply(&settings) } - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } if settings.SSLSettings != nil { - applySSLSettings(settings.SSLSettings)(&genericContainerReq) + if err := applySSLSettings(settings.SSLSettings)(&genericContainerReq); err != nil { + return nil, err + } } nodeConfig, err := renderRabbitMQConfig(settings) @@ -118,7 +122,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize return nil, err } - withConfig(tmpConfigFile)(&genericContainerReq) + if err := withConfig(tmpConfigFile)(&genericContainerReq); err != nil { + return nil, err + } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) if err != nil { @@ -135,7 +141,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } func withConfig(hostPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["RABBITMQ_CONFIG_FILE"] = defaultCustomConfPath req.Files = append(req.Files, testcontainers.ContainerFile{ @@ -143,6 +149,8 @@ func withConfig(hostPath string) testcontainers.CustomizeRequestOption { ContainerFilePath: defaultCustomConfPath, FileMode: 0o644, }) + + return nil } } @@ -154,7 +162,7 @@ func applySSLSettings(sslSettings *SSLSettings) testcontainers.CustomizeRequestO const defaultPermission = 0o644 - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Files = append(req.Files, testcontainers.ContainerFile{ HostFilePath: sslSettings.CACertFile, ContainerFilePath: rabbitCaCertPath, @@ -174,6 +182,8 @@ func applySSLSettings(sslSettings *SSLSettings) testcontainers.CustomizeRequestO // To verify that TLS has been enabled on the node, container logs should contain an entry about a TLS listener being enabled // See https://www.rabbitmq.com/ssl.html#enabling-tls-verify-configuration req.WaitingFor = wait.ForAll(req.WaitingFor, wait.ForLog("started TLS (SSL) listener on [::]:5671")) + + return nil } } diff --git a/modules/redis/options_test.go b/modules/redis/options_test.go index 7150af6df2..0ff8bd201c 100644 --- a/modules/redis/options_test.go +++ b/modules/redis/options_test.go @@ -39,7 +39,8 @@ func TestWithConfigFile(t *testing.T) { }, } - WithConfigFile("redis.conf")(req) + err := WithConfigFile("redis.conf")(req) + require.NoError(t, err) require.Equal(t, tt.expectedCmds, req.Cmd) }) @@ -77,7 +78,8 @@ func TestWithLogLevel(t *testing.T) { }, } - WithLogLevel(LogLevelDebug)(req) + err := WithLogLevel(LogLevelDebug)(req) + require.NoError(t, err) require.Equal(t, tt.expectedCmds, req.Cmd) }) @@ -130,7 +132,8 @@ func TestWithSnapshotting(t *testing.T) { }, } - WithSnapshotting(tt.seconds, tt.changedKeys)(req) + err := WithSnapshotting(tt.seconds, tt.changedKeys)(req) + require.NoError(t, err) require.Equal(t, tt.expectedCmds, req.Cmd) }) diff --git a/modules/redis/redis.go b/modules/redis/redis.go index a828816d73..d24cfac676 100644 --- a/modules/redis/redis.go +++ b/modules/redis/redis.go @@ -60,7 +60,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) @@ -76,7 +78,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { const defaultConfigFile = "/usr/local/redis.conf" - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: defaultConfigFile, @@ -86,7 +88,7 @@ func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { if len(req.Cmd) == 0 { req.Cmd = []string{redisServerProcess, defaultConfigFile} - return + return nil } // prepend the command to run the redis server with the config file, which must be the first argument of the redis server process @@ -97,14 +99,18 @@ func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { // prepend the redis server and the confif file, then the rest of the args req.Cmd = append([]string{redisServerProcess, defaultConfigFile}, req.Cmd...) } + + return nil } } // WithLogLevel sets the log level for the redis server process // See https://redis.io/docs/reference/modules/modules-api-ref/#redismodule_log for more information. func WithLogLevel(level LogLevel) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { processRedisServerArgs(req, []string{"--loglevel", string(level)}) + + return nil } } @@ -120,8 +126,9 @@ func WithSnapshotting(seconds int, changedKeys int) testcontainers.CustomizeRequ seconds = 1 } - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { processRedisServerArgs(req, []string{"--save", fmt.Sprintf("%d", seconds), fmt.Sprintf("%d", changedKeys)}) + return nil } } diff --git a/modules/redpanda/options.go b/modules/redpanda/options.go index 4340be30d8..f78766115b 100644 --- a/modules/redpanda/options.go +++ b/modules/redpanda/options.go @@ -59,14 +59,15 @@ func defaultOptions() options { } // Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface. -var _ testcontainers.ContainerCustomizer = (*Option)(nil) +var _ testcontainers.ContainerCustomizer = (Option)(nil) // Option is an option for the Redpanda container. type Option func(*options) // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. -func (o Option) Customize(*testcontainers.GenericContainerRequest) { +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { // NOOP to satisfy interface. + return nil } func WithNewServiceAccount(username, password string) Option { diff --git a/modules/redpanda/redpanda.go b/modules/redpanda/redpanda.go index 6213809d4b..b0a73dae80 100644 --- a/modules/redpanda/redpanda.go +++ b/modules/redpanda/redpanda.go @@ -91,7 +91,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize if apply, ok := opt.(Option); ok { apply(&settings) } - opt.Customize(&req) + if err := opt.Customize(&req); err != nil { + return nil, err + } } // 2.1. If the image is not at least v23.3, disable wasm transform @@ -198,7 +200,6 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize wait.ForListeningPort(defaultKafkaAPIPort), wait.ForLog("Successfully started Redpanda!").WithPollInterval(100*time.Millisecond)). WaitUntilReady(ctx, container) - if err != nil { return nil, fmt.Errorf("failed to wait for Redpanda readiness: %w", err) } diff --git a/modules/vault/vault.go b/modules/vault/vault.go index ca5ab4bb13..5c6f47aa6e 100644 --- a/modules/vault/vault.go +++ b/modules/vault/vault.go @@ -41,7 +41,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) @@ -54,15 +56,17 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize // WithToken is a container option function that sets the root token for the Vault func WithToken(token string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["VAULT_DEV_ROOT_TOKEN_ID"] = token req.Env["VAULT_TOKEN"] = token + + return nil } } // WithInitCommand is an option function that adds a set of initialization commands to the Vault's configuration func WithInitCommand(commands ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { commandsList := make([]string, 0, len(commands)) for _, command := range commands { commandsList = append(commandsList, "vault "+command) @@ -70,6 +74,8 @@ func WithInitCommand(commands ...string) testcontainers.CustomizeRequestOption { cmd := []string{"/bin/sh", "-c", strings.Join(commandsList, " && ")} req.WaitingFor = wait.ForAll(req.WaitingFor, wait.ForExec(cmd)) + + return nil } } diff --git a/modules/weaviate/weaviate.go b/modules/weaviate/weaviate.go index 02950ce38c..06bf9ca56e 100644 --- a/modules/weaviate/weaviate.go +++ b/modules/weaviate/weaviate.go @@ -40,7 +40,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/network/network.go b/network/network.go index 646ed463a8..8a057d4110 100644 --- a/network/network.go +++ b/network/network.go @@ -2,6 +2,7 @@ package network import ( "context" + "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/network" @@ -24,7 +25,9 @@ func New(ctx context.Context, opts ...NetworkCustomizer) (*testcontainers.Docker } for _, opt := range opts { - opt.Customize(&nc) + if err := opt.Customize(&nc); err != nil { + return nil, err + } } //nolint:staticcheck @@ -54,76 +57,90 @@ func New(ctx context.Context, opts ...NetworkCustomizer) (*testcontainers.Docker // NetworkCustomizer is an interface that can be used to configure the network create request. type NetworkCustomizer interface { - Customize(req *types.NetworkCreate) + Customize(req *types.NetworkCreate) error } // CustomizeNetworkOption is a type that can be used to configure the network create request. -type CustomizeNetworkOption func(req *types.NetworkCreate) +type CustomizeNetworkOption func(req *types.NetworkCreate) error // Customize implements the NetworkCustomizer interface, // applying the option to the network create request. -func (opt CustomizeNetworkOption) Customize(req *types.NetworkCreate) { - opt(req) +func (opt CustomizeNetworkOption) Customize(req *types.NetworkCreate) error { + return opt(req) } // WithAttachable allows to set the network as attachable. func WithAttachable() CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { original.Attachable = true + + return nil } } // WithCheckDuplicate allows to check if a network with the same name already exists. func WithCheckDuplicate() CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { //nolint:staticcheck original.CheckDuplicate = true + + return nil } } // WithDriver allows to override the default network driver, which is "bridge". func WithDriver(driver string) CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { original.Driver = driver + + return nil } } // WithEnableIPv6 allows to set the network as IPv6 enabled. // Please use this option if and only if IPv6 is enabled on the Docker daemon. func WithEnableIPv6() CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { original.EnableIPv6 = true + + return nil } } // WithInternal allows to set the network as internal. func WithInternal() CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { original.Internal = true + + return nil } } // WithLabels allows to set the network labels, adding the new ones // to the default Testcontainers for Go labels. func WithLabels(labels map[string]string) CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { for k, v := range labels { original.Labels[k] = v } + + return nil } } // WithIPAM allows to change the default IPAM configuration. func WithIPAM(ipam *network.IPAM) CustomizeNetworkOption { - return func(original *types.NetworkCreate) { + return func(original *types.NetworkCreate) error { original.IPAM = ipam + + return nil } } // WithNetwork reuses an already existing network, attaching the container to it. // Finally it sets the network alias on that network to the given alias. func WithNetwork(aliases []string, nw *testcontainers.DockerNetwork) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { networkName := nw.Name // attaching to the network because it was created with success or it already existed. @@ -133,21 +150,18 @@ func WithNetwork(aliases []string, nw *testcontainers.DockerNetwork) testcontain req.NetworkAliases = make(map[string][]string) } req.NetworkAliases[networkName] = aliases + + return nil } } // WithNewNetwork creates a new network with random name and customizers, and attaches the container to it. // Finally it sets the network alias on that network to the given alias. func WithNewNetwork(ctx context.Context, aliases []string, opts ...NetworkCustomizer) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { newNetwork, err := New(ctx, opts...) if err != nil { - logger := req.Logger - if logger == nil { - logger = testcontainers.Logger - } - logger.Printf("failed to create network. Container won't be attached to it: %v", err) - return + return fmt.Errorf("new network: %w", err) } networkName := newNetwork.Name @@ -159,5 +173,7 @@ func WithNewNetwork(ctx context.Context, aliases []string, opts ...NetworkCustom req.NetworkAliases = make(map[string][]string) } req.NetworkAliases[networkName] = aliases + + return nil } } diff --git a/network/network_test.go b/network/network_test.go index 1756c7d5e6..4d081638cd 100644 --- a/network/network_test.go +++ b/network/network_test.go @@ -422,7 +422,8 @@ func TestWithNetwork(t *testing.T) { ContainerRequest: testcontainers.ContainerRequest{}, } - network.WithNetwork([]string{"alias"}, nw)(&req) + err := network.WithNetwork([]string{"alias"}, nw)(&req) + require.NoError(t, err) assert.Len(t, req.Networks, 1) assert.Equal(t, networkName, req.Networks[0]) @@ -468,7 +469,8 @@ func TestWithSyntheticNetwork(t *testing.T) { }, } - network.WithNetwork([]string{"alias"}, nw)(&req) + err := network.WithNetwork([]string{"alias"}, nw)(&req) + require.NoError(t, err) assert.Len(t, req.Networks, 1) assert.Equal(t, networkName, req.Networks[0]) @@ -502,11 +504,12 @@ func TestWithNewNetwork(t *testing.T) { ContainerRequest: testcontainers.ContainerRequest{}, } - network.WithNewNetwork(context.Background(), []string{"alias"}, + err := network.WithNewNetwork(context.Background(), []string{"alias"}, network.WithAttachable(), network.WithInternal(), network.WithLabels(map[string]string{"this-is-a-test": "value"}), )(&req) + require.NoError(t, err) assert.Len(t, req.Networks, 1) @@ -549,11 +552,12 @@ func TestWithNewNetworkContextTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() - network.WithNewNetwork(ctx, []string{"alias"}, + err := network.WithNewNetwork(ctx, []string{"alias"}, network.WithAttachable(), network.WithInternal(), network.WithLabels(map[string]string{"this-is-a-test": "value"}), )(&req) + require.Error(t, err) // we do not want to fail, just skip the network creation assert.Empty(t, req.Networks) diff --git a/options.go b/options.go index 391d3e1a82..2860cd39a4 100644 --- a/options.go +++ b/options.go @@ -17,46 +17,51 @@ import ( // ContainerCustomizer is an interface that can be used to configure the Testcontainers container // request. The passed request will be merged with the default one. type ContainerCustomizer interface { - Customize(req *GenericContainerRequest) + Customize(req *GenericContainerRequest) error } // CustomizeRequestOption is a type that can be used to configure the Testcontainers container request. // The passed request will be merged with the default one. -type CustomizeRequestOption func(req *GenericContainerRequest) +type CustomizeRequestOption func(req *GenericContainerRequest) error -func (opt CustomizeRequestOption) Customize(req *GenericContainerRequest) { - opt(req) +func (opt CustomizeRequestOption) Customize(req *GenericContainerRequest) error { + return opt(req) } // CustomizeRequest returns a function that can be used to merge the passed container request with the one that is used by the container. // Slices and Maps will be appended. func CustomizeRequest(src GenericContainerRequest) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { if err := mergo.Merge(req, &src, mergo.WithOverride, mergo.WithAppendSlice); err != nil { - Logger.Printf("error merging container request, keeping the original one. Error: %v", err) - return + return fmt.Errorf("error merging container request, keeping the original one: %w", err) } + + return nil } } // WithConfigModifier allows to override the default container config func WithConfigModifier(modifier func(config *container.Config)) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { req.ConfigModifier = modifier + + return nil } } // WithEndpointSettingsModifier allows to override the default endpoint settings func WithEndpointSettingsModifier(modifier func(settings map[string]*network.EndpointSettings)) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { req.EnpointSettingsModifier = modifier + + return nil } } // WithEnv sets the environment variables for a container. // If the environment variable already exists, it will be overridden. func WithEnv(envs map[string]string) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { if req.Env == nil { req.Env = map[string]string{} } @@ -64,20 +69,26 @@ func WithEnv(envs map[string]string) CustomizeRequestOption { for key, val := range envs { req.Env[key] = val } + + return nil } } // WithHostConfigModifier allows to override the default host config func WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { req.HostConfigModifier = modifier + + return nil } } // WithImage sets the image for a container func WithImage(image string) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { req.Image = image + + return nil } } @@ -138,19 +149,22 @@ func (p prependHubRegistry) Substitute(image string) (string, error) { // WithImageSubstitutors sets the image substitutors for a container func WithImageSubstitutors(fn ...ImageSubstitutor) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { req.ImageSubstitutors = fn + + return nil } } // WithLogConsumers sets the log consumers for a container func WithLogConsumers(consumer ...LogConsumer) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { if req.LogConsumerCfg == nil { req.LogConsumerCfg = &LogConsumerConfig{} } req.LogConsumerCfg.Consumers = consumer + return nil } } @@ -198,7 +212,7 @@ func (r RawCommand) AsCommand() []string { // It will leverage the container lifecycle hooks to call the command right after the container // is started. func WithStartupCommand(execs ...Executable) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { startupCommandsHook := ContainerLifecycleHooks{ PostStarts: []ContainerHook{}, } @@ -213,6 +227,8 @@ func WithStartupCommand(execs ...Executable) CustomizeRequestOption { } req.LifecycleHooks = append(req.LifecycleHooks, startupCommandsHook) + + return nil } } @@ -220,7 +236,7 @@ func WithStartupCommand(execs ...Executable) CustomizeRequestOption { // It will leverage the container lifecycle hooks to call the command right after the container // is ready. func WithAfterReadyCommand(execs ...Executable) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { postReadiesHook := []ContainerHook{} for _, exec := range execs { @@ -235,6 +251,8 @@ func WithAfterReadyCommand(execs ...Executable) CustomizeRequestOption { req.LifecycleHooks = append(req.LifecycleHooks, ContainerLifecycleHooks{ PostReadies: postReadiesHook, }) + + return nil } } @@ -245,7 +263,9 @@ func WithWaitStrategy(strategies ...wait.Strategy) CustomizeRequestOption { // WithWaitStrategyAndDeadline sets the wait strategy for a container, including deadline func WithWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { req.WaitingFor = wait.ForAll(strategies...).WithDeadline(deadline) + + return nil } } diff --git a/options_test.go b/options_test.go index bbd4a944d5..1bf1c07826 100644 --- a/options_test.go +++ b/options_test.go @@ -49,7 +49,8 @@ func TestOverrideContainerRequest(t *testing.T) { } // the toBeMergedRequest should be merged into the req - testcontainers.CustomizeRequest(toBeMergedRequest)(&req) + err := testcontainers.CustomizeRequest(toBeMergedRequest)(&req) + require.NoError(t, err) // toBeMergedRequest should not be changed assert.Equal(t, "", toBeMergedRequest.Env["BAR"]) @@ -87,7 +88,8 @@ func TestWithLogConsumers(t *testing.T) { lc := &msgsLogConsumer{} - testcontainers.WithLogConsumers(lc)(&req) + err := testcontainers.WithLogConsumers(lc)(&req) + require.NoError(t, err) c, err := testcontainers.GenericContainer(context.Background(), req) // we expect an error because the MySQL environment variables are not set @@ -112,7 +114,8 @@ func TestWithStartupCommand(t *testing.T) { testExec := testcontainers.NewRawCommand([]string{"touch", "/tmp/.testcontainers"}) - testcontainers.WithStartupCommand(testExec)(&req) + err := testcontainers.WithStartupCommand(testExec)(&req) + require.NoError(t, err) assert.Len(t, req.LifecycleHooks, 1) assert.Len(t, req.LifecycleHooks[0].PostStarts, 1) @@ -143,7 +146,8 @@ func TestWithAfterReadyCommand(t *testing.T) { testExec := testcontainers.NewRawCommand([]string{"touch", "/tmp/.testcontainers"}) - testcontainers.WithAfterReadyCommand(testExec)(&req) + err := testcontainers.WithAfterReadyCommand(testExec)(&req) + require.NoError(t, err) assert.Len(t, req.LifecycleHooks, 1) assert.Len(t, req.LifecycleHooks[0].PostReadies, 1) @@ -205,7 +209,7 @@ func TestWithEnv(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { opt := testcontainers.WithEnv(tc.env) - opt.Customize(tc.req) + require.NoError(t, opt.Customize(tc.req)) require.Equal(t, tc.expect, tc.req.Env) }) } diff --git a/reaper.go b/reaper.go index 3dff6a7c9d..859f8b76de 100644 --- a/reaper.go +++ b/reaper.go @@ -120,7 +120,6 @@ func lookUpReaperContainer(ctx context.Context, sessionID string) (*DockerContai return nil }, backoff.WithContext(exp, ctx)) - if err != nil { return nil, err } diff --git a/wait/testdata/main.go b/wait/testdata/main.go index 4e89caf9f8..42a006887c 100644 --- a/wait/testdata/main.go +++ b/wait/testdata/main.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/base64" + "errors" "io" "log" "net/http" @@ -58,7 +59,7 @@ func main() { server := http.Server{Addr: ":6443", Handler: mux} go func() { log.Println("serving...") - if err := server.ListenAndServeTLS("tls.pem", "tls-key.pem"); err != nil && err != http.ErrServerClosed { + if err := server.ListenAndServeTLS("tls.pem", "tls-key.pem"); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } }() From a3aa85b3803589bd2caa7a3e9af1639afcb91b81 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Tue, 12 Mar 2024 21:12:26 +0000 Subject: [PATCH 02/17] fix(mongodb): captured loop variable Fix captured loop variable in mongodb test reported by govet. --- modules/mongodb/mongodb_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 9326509f09..994b4c448c 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -33,12 +33,13 @@ func TestMongoDB(t *testing.T) { } for _, tc := range testCases { - t.Run(tc.image, func(t *testing.T) { + image := tc.image + t.Run(image, func(t *testing.T) { t.Parallel() ctx := context.Background() - mongodbContainer, err := mongodb.RunContainer(ctx, testcontainers.WithImage(tc.image)) + mongodbContainer, err := mongodb.RunContainer(ctx, testcontainers.WithImage(image)) if err != nil { t.Fatalf("failed to start container: %s", err) } From 634ae00aa13ae1407c8bcb62798ab24a5746fee8 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Tue, 12 Mar 2024 21:14:30 +0000 Subject: [PATCH 03/17] fix(k3s): test formatting Fix formatting in test file reported by gci during linting. --- modules/k3s/k3s_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/k3s/k3s_test.go b/modules/k3s/k3s_test.go index 2a91a335ec..273f12a6af 100644 --- a/modules/k3s/k3s_test.go +++ b/modules/k3s/k3s_test.go @@ -169,7 +169,7 @@ func Test_WithManifestOption(t *testing.T) { k3sContainer, err := k3s.RunContainer(ctx, testcontainers.WithImage("docker.io/rancher/k3s:v1.27.1-k3s1"), k3s.WithManifest("nginx-manifest.yaml"), - testcontainers.WithWaitStrategy(wait.ForExec([]string{"kubectl", "wait", "pod", "nginx","--for=condition=Ready"})), + testcontainers.WithWaitStrategy(wait.ForExec([]string{"kubectl", "wait", "pod", "nginx", "--for=condition=Ready"})), ) if err != nil { t.Fatal(err) From 3542c5c9fb18029dcd0fdfb771045480033e96b5 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Tue, 12 Mar 2024 21:15:23 +0000 Subject: [PATCH 04/17] chore: add missing Customize error returns Add missing error returns for implementations of CustomizeRequestOption. --- docs/modules/index.md | 4 +++- logger.go | 3 ++- logger_test.go | 2 +- modulegen/_template/module.go.tmpl | 4 +++- modules/k3s/k3s.go | 4 +++- modules/ollama/ollama.go | 4 +++- modules/ollama/options.go | 2 +- modules/openfga/openfga.go | 4 +++- modules/surrealdb/surrealdb.go | 24 ++++++++++++++++++------ 9 files changed, 37 insertions(+), 14 deletions(-) diff --git a/docs/modules/index.md b/docs/modules/index.md index 5d54b2e38f..7cf304735d 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -130,7 +130,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } ... for _, opt := range opts { - req = opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customise: %w", err) + } // If you need to transfer some state from the options to the container, you can do it here if myCustomizer, ok := opt.(MyCustomizer); ok { diff --git a/logger.go b/logger.go index b137fdca66..4f77ce7b53 100644 --- a/logger.go +++ b/logger.go @@ -68,8 +68,9 @@ func (o LoggerOption) ApplyDockerTo(opts *DockerProviderOptions) { } // Customize implements ContainerCustomizer. -func (o LoggerOption) Customize(req *GenericContainerRequest) { +func (o LoggerOption) Customize(req *GenericContainerRequest) error { req.Logger = o.logger + return nil } type testLogger struct { diff --git a/logger_test.go b/logger_test.go index d4debef016..7b6d0cb9d2 100644 --- a/logger_test.go +++ b/logger_test.go @@ -11,7 +11,7 @@ func TestWithLogger(t *testing.T) { logOpt := WithLogger(logger) t.Run("container", func(t *testing.T) { var req GenericContainerRequest - logOpt.Customize(&req) + require.NoError(t, logOpt.Customize(&req)) require.Equal(t, logger, req.Logger) }) diff --git a/modulegen/_template/module.go.tmpl b/modulegen/_template/module.go.tmpl index e6b99fd90b..2a08b01942 100644 --- a/modulegen/_template/module.go.tmpl +++ b/modulegen/_template/module.go.tmpl @@ -23,7 +23,9 @@ func {{ $entrypoint }}(ctx context.Context, opts ...testcontainers.ContainerCust } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 5327baeb71..4190cfba71 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -34,7 +34,7 @@ const k3sManifests = "/var/lib/rancher/k3s/server/manifests/" // WithManifest loads the manifest into the cluster. K3s applies it automatically during the startup process func WithManifest(manifestPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { manifest := filepath.Base(manifestPath) target := k3sManifests + manifest @@ -42,6 +42,8 @@ func WithManifest(manifestPath string) testcontainers.CustomizeRequestOption { HostFilePath: manifestPath, ContainerFilePath: target, }) + + return nil } } diff --git a/modules/ollama/ollama.go b/modules/ollama/ollama.go index 58ddbcf070..cb0a8784b5 100644 --- a/modules/ollama/ollama.go +++ b/modules/ollama/ollama.go @@ -88,7 +88,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize opts = append(opts, withGpu()) for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/ollama/options.go b/modules/ollama/options.go index 7d74aeddaf..605768a379 100644 --- a/modules/ollama/options.go +++ b/modules/ollama/options.go @@ -8,7 +8,7 @@ import ( "github.com/testcontainers/testcontainers-go" ) -var noopCustomizeRequestOption = func(req *testcontainers.GenericContainerRequest) {} +var noopCustomizeRequestOption = func(req *testcontainers.GenericContainerRequest) error { return nil } // withGpu requests a GPU for the container, which could improve performance for some models. // This option will be automaticall added to the Ollama container to check if the host supports nvidia. diff --git a/modules/openfga/openfga.go b/modules/openfga/openfga.go index a88285a062..1f1fbbd3bd 100644 --- a/modules/openfga/openfga.go +++ b/modules/openfga/openfga.go @@ -66,7 +66,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) diff --git a/modules/surrealdb/surrealdb.go b/modules/surrealdb/surrealdb.go index 04e38b3d75..78d0707fe1 100644 --- a/modules/surrealdb/surrealdb.go +++ b/modules/surrealdb/surrealdb.go @@ -34,8 +34,10 @@ func (c *SurrealDBContainer) URL(ctx context.Context) (string, error) { // It is used in conjunction with WithPassword to set a username and its password. // It will create the specified user with superuser power. func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["SURREAL_USER"] = username + + return nil } } @@ -43,29 +45,37 @@ func WithUsername(username string) testcontainers.CustomizeRequestOption { // It is used in conjunction with WithUsername to set a username and its password. // It will set the superuser password for SurrealDB. func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["SURREAL_PASS"] = password + + return nil } } // WithAuthentication enables authentication for the SurrealDB instance func WithAuthentication() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["SURREAL_AUTH"] = "true" + + return nil } } // WithStrict enables strict mode for the SurrealDB instance func WithStrictMode() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["SURREAL_STRICT"] = "true" + + return nil } } // WithAllowAllCaps enables all caps for the SurrealDB instance func WithAllowAllCaps() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["SURREAL_CAPS_ALLOW_ALL"] = "false" + + return nil } } @@ -94,7 +104,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize } for _, opt := range opts { - opt.Customize(&genericContainerReq) + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) From 30468dce9221e6774300a21d08a9f73fd0cfb587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Wed, 24 Apr 2024 18:36:52 +0200 Subject: [PATCH 05/17] chore: update modules with the new error API --- modules/dolt/dolt.go | 28 +++++++++++++++++++--------- modules/influxdb/influxdb.go | 15 ++++++++++----- modules/k6/k6.go | 9 +++++++-- modules/registry/options.go | 16 +++++++++------- port_forwarding.go | 3 ++- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/modules/dolt/dolt.go b/modules/dolt/dolt.go index a6da442aa0..3a9a77bce1 100644 --- a/modules/dolt/dolt.go +++ b/modules/dolt/dolt.go @@ -29,12 +29,14 @@ type DoltContainer struct { } func WithDefaultCredentials() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { username := req.Env["DOLT_USER"] if strings.EqualFold(rootUser, username) { delete(req.Env, "DOLT_USER") delete(req.Env, "DOLT_PASSWORD") } + + return nil } } @@ -184,59 +186,66 @@ func (c *DoltContainer) ConnectionString(ctx context.Context, args ...string) (s } func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["DOLT_USER"] = username + return nil } } func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["DOLT_PASSWORD"] = password + return nil } } func WithDoltCredsPublicKey(key string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["DOLT_CREDS_PUB_KEY"] = key + return nil } } func WithDoltCloneRemoteUrl(url string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["DOLT_REMOTE_CLONE_URL"] = url + return nil } } func WithDatabase(database string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["DOLT_DATABASE"] = database + return nil } } func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/dolt/servercfg.d/server.cnf", FileMode: 0o755, } req.Files = append(req.Files, cf) + return nil } } func WithCredsFile(credsFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: credsFile, ContainerFilePath: "/root/.dolt/creds/" + filepath.Base(credsFile), FileMode: 0o755, } req.Files = append(req.Files, cf) + return nil } } func WithScripts(scripts ...string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { var initScripts []testcontainers.ContainerFile for _, script := range scripts { cf := testcontainers.ContainerFile{ @@ -247,5 +256,6 @@ func WithScripts(scripts ...string) testcontainers.CustomizeRequestOption { initScripts = append(initScripts, cf) } req.Files = append(req.Files, initScripts...) + return nil } } diff --git a/modules/influxdb/influxdb.go b/modules/influxdb/influxdb.go index 5880715fcd..887da08e52 100644 --- a/modules/influxdb/influxdb.go +++ b/modules/influxdb/influxdb.go @@ -107,31 +107,35 @@ func (c *InfluxDbContainer) ConnectionUrl(ctx context.Context) (string, error) { } func WithUsername(username string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["INFLUXDB_USER"] = username + return nil } } func WithPassword(password string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["INFLUXDB_PASSWORD"] = password + return nil } } func WithDatabase(database string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Env["INFLUXDB_DATABASE"] = database + return nil } } func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: configFile, ContainerFilePath: "/etc/influxdb/influxdb.conf", FileMode: 0o755, } req.Files = append(req.Files, cf) + return nil } } @@ -139,12 +143,13 @@ func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { // The secPath is the path to the directory on the host machine. // The directory will be copied to the root of the container. func WithInitDb(srcPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ HostFilePath: path.Join(srcPath, "docker-entrypoint-initdb.d"), ContainerFilePath: "/", FileMode: 0o755, } req.Files = append(req.Files, cf) + return nil } } diff --git a/modules/k6/k6.go b/modules/k6/k6.go index 16153d8e32..c0e9f01fe0 100644 --- a/modules/k6/k6.go +++ b/modules/k6/k6.go @@ -72,8 +72,11 @@ func WithTestScript(scriptPath string) testcontainers.CustomizeRequestOption { scriptBaseName := filepath.Base(scriptPath) f, err := os.Open(scriptPath) if err != nil { - panic("Cannot create reader for test file ") + return func(req *testcontainers.GenericContainerRequest) error { + return fmt.Errorf("cannot create reader for test file: %w", err) + } } + return WithTestScriptReader(f, scriptBaseName) } @@ -105,7 +108,9 @@ func WithTestScriptReader(reader io.Reader, scriptBaseName string) testcontainer func WithRemoteTestScript(d DownloadableFile) testcontainers.CustomizeRequestOption { err := downloadFileFromDescription(d) if err != nil { - panic("Not able to download required test script") + return func(req *testcontainers.GenericContainerRequest) error { + return fmt.Errorf("not able to download required test script: %w", err) + } } return WithTestScript(d.getDownloadPath()) diff --git a/modules/registry/options.go b/modules/registry/options.go index a3304d1ffa..adfe3fff07 100644 --- a/modules/registry/options.go +++ b/modules/registry/options.go @@ -1,6 +1,7 @@ package registry import ( + "fmt" "os" "path/filepath" @@ -19,13 +20,14 @@ const ( // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY environment variable. // The dataPath must have the same structure as the registry data directory. func WithData(dataPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Files = append(req.Files, testcontainers.ContainerFile{ HostFilePath: dataPath, ContainerFilePath: containerDataPath, }) req.Env["REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY"] = containerDataPath + return nil } } @@ -34,23 +36,22 @@ func WithData(dataPath string) testcontainers.CustomizeRequestOption { // in the /auth/htpasswd path. The container will be configured to use this file as // the htpasswd file, thanks to the REGISTRY_AUTH_HTPASSWD_PATH environment variable. func WithHtpasswd(credentials string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { tmpFile, err := os.Create(filepath.Join(os.TempDir(), "htpasswd")) if err != nil { tmpFile, err = os.Create(".") if err != nil { - // cannot create the file in the temp dir or in the current dir - panic(err) + return fmt.Errorf("cannot create the file in the temp dir or in the current dir: %w", err) } } defer tmpFile.Close() _, err = tmpFile.WriteString(credentials) if err != nil { - panic(err) + return fmt.Errorf("cannot write the credentials to the file: %w", err) } - WithHtpasswdFile(tmpFile.Name())(req) + return WithHtpasswdFile(tmpFile.Name())(req) } } @@ -59,7 +60,7 @@ func WithHtpasswd(credentials string) testcontainers.CustomizeRequestOption { // The container will be configured to use this file as the htpasswd file, // thanks to the REGISTRY_AUTH_HTPASSWD_PATH environment variable. func WithHtpasswdFile(htpasswdPath string) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { req.Files = append(req.Files, testcontainers.ContainerFile{ HostFilePath: htpasswdPath, ContainerFilePath: containerHtpasswdPath, @@ -70,5 +71,6 @@ func WithHtpasswdFile(htpasswdPath string) testcontainers.CustomizeRequestOption req.Env["REGISTRY_AUTH_HTPASSWD_REALM"] = "Registry" req.Env["REGISTRY_AUTH_HTPASSWD_PATH"] = containerHtpasswdPath req.Env["REGISTRY_AUTH_HTPASSWD_PATH"] = containerHtpasswdPath + return nil } } diff --git a/port_forwarding.go b/port_forwarding.go index 14cead708f..84cc77e7ef 100644 --- a/port_forwarding.go +++ b/port_forwarding.go @@ -66,7 +66,7 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, p ...int) (Cont // Finally it sets the network alias on that network to the given alias. // TODO: Using an anonymous function to avoid cyclic dependencies with the network package. withNetwork := func(aliases []string, nw *DockerNetwork) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { networkName := nw.Name // attaching to the network because it was created with success or it already existed. @@ -76,6 +76,7 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, p ...int) (Cont req.NetworkAliases = make(map[string][]string) } req.NetworkAliases[networkName] = aliases + return nil } } From d284e23cfaa83ec0cbb0364a08e4a8cb77073d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Wed, 24 Apr 2024 20:07:44 +0200 Subject: [PATCH 06/17] fix: use logger --- modules/neo4j/config.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go index b7b8494ee7..02c6afb878 100644 --- a/modules/neo4j/config.go +++ b/modules/neo4j/config.go @@ -106,11 +106,13 @@ func addSetting(req *testcontainers.GenericContainerRequest, key string, newVal if oldVal, found := req.Env[normalizedKey]; found { // make sure AUTH is not overwritten by a setting if key == "AUTH" { - return fmt.Errorf("setting %q is not permitted, WithAdminPassword has already been set", normalizedKey) + req.Logger.Printf("setting %q is not permitted, WithAdminPassword has already been set\n", normalizedKey) + return nil } - return fmt.Errorf("setting %q with value %q is now overwritten with value %q", []any{key, oldVal, newVal}...) + req.Logger.Printf("setting %q with value %q is now overwritten with value %q\n", []any{key, oldVal, newVal}...) } + req.Env[normalizedKey] = newVal return nil From eeb7f137ccb95d7a815debba5fbc22018ef4dcd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Wed, 24 Apr 2024 20:15:18 +0200 Subject: [PATCH 07/17] fix: update modulegen tests --- modulegen/main_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modulegen/main_test.go b/modulegen/main_test.go index f108ddb0dd..819cfa9ad0 100644 --- a/modulegen/main_test.go +++ b/modulegen/main_test.go @@ -428,7 +428,9 @@ func assertModuleContent(t *testing.T, module context.TestcontainersModule, exam assert.Equal(t, data[13], "// "+entrypoint+" creates an instance of the "+exampleName+" container type") assert.Equal(t, data[14], "func "+entrypoint+"(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*"+containerName+", error) {") assert.Equal(t, data[16], "\t\tImage: \""+module.Image+"\",") - assert.Equal(t, data[33], "\treturn &"+containerName+"{Container: container}, nil") + assert.Equal(t, data[25], "\t\tif err := opt.Customize(&genericContainerReq); err != nil {") + assert.Equal(t, data[26], "\t\t\treturn nil, fmt.Errorf(\"customize: %w\", err)") + assert.Equal(t, data[35], "\treturn &"+containerName+"{Container: container}, nil") } // assert content GitHub workflow for the module From b955846a6fed6c2ae02108526fb3bc04ac19d15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Wed, 24 Apr 2024 22:48:49 +0200 Subject: [PATCH 08/17] chore: fix lint --- docker_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker_test.go b/docker_test.go index f41a002a3b..77b7021b62 100644 --- a/docker_test.go +++ b/docker_test.go @@ -2123,7 +2123,7 @@ func TestDockerProvider_BuildImage_Retries(t *testing.T) { }, { name: "no retry when not implemented by provider", - errReturned: errdefs.NotImplemented(errors.New("unkown method")), + errReturned: errdefs.NotImplemented(errors.New("unknown method")), shouldRetry: false, }, { @@ -2169,7 +2169,7 @@ func TestDockerProvider_waitContainerCreation_retries(t *testing.T) { }, { name: "no retry when not implemented by provider", - errReturned: errdefs.NotImplemented(errors.New("unkown method")), + errReturned: errdefs.NotImplemented(errors.New("unknown method")), shouldRetry: false, }, { @@ -2235,7 +2235,7 @@ func TestDockerProvider_attemptToPullImage_retries(t *testing.T) { }, { name: "no retry when not implemented by provider", - errReturned: errdefs.NotImplemented(errors.New("unkown method")), + errReturned: errdefs.NotImplemented(errors.New("unknown method")), shouldRetry: false, }, { From 58a3e4bdd89db8acade6d9bb899bf0ad7078a485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 00:10:48 +0200 Subject: [PATCH 09/17] chore: handle customise errors --- port_forwarding.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/port_forwarding.go b/port_forwarding.go index 84cc77e7ef..eaf5897e04 100644 --- a/port_forwarding.go +++ b/port_forwarding.go @@ -128,8 +128,7 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, p ...int) (Cont sshdConnectHook = ContainerLifecycleHooks{ PostReadies: []ContainerHook{ func(ctx context.Context, c Container) error { - sshdContainer.exposeHostPort(ctx, req.HostAccessPorts...) - return nil + return sshdContainer.exposeHostPort(ctx, req.HostAccessPorts...) }, }, PreTerminates: []ContainerHook{ @@ -156,7 +155,9 @@ func newSshdContainer(ctx context.Context, opts ...ContainerCustomizer) (*sshdCo } for _, opt := range opts { - opt.Customize(&req) + if err := opt.Customize(&req); err != nil { + return nil, err + } } c, err := GenericContainer(ctx, req) @@ -323,12 +324,12 @@ func (pf *PortForwarder) runTunnel(ctx context.Context, remote net.Conn) { done := make(chan struct{}, 2) go func() { - io.Copy(local, remote) + _, _ = io.Copy(local, remote) done <- struct{}{} }() go func() { - io.Copy(remote, local) + _, _ = io.Copy(remote, local) done <- struct{}{} }() From 48c8691b2777b42beace0e0e7ac9886cc67d29ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 12:04:45 +0200 Subject: [PATCH 10/17] chore: update new host port access option --- options.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/options.go b/options.go index a39a16cd18..95b6d9394f 100644 --- a/options.go +++ b/options.go @@ -85,12 +85,13 @@ func WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) Cus // WithHostPortAccess allows to expose the host ports to the container func WithHostPortAccess(ports ...int) CustomizeRequestOption { - return func(req *GenericContainerRequest) { + return func(req *GenericContainerRequest) error { if req.HostAccessPorts == nil { req.HostAccessPorts = []int{} } req.HostAccessPorts = append(req.HostAccessPorts, ports...) + return nil } } From 45705bac6d37f8c7ad75741aff34d0b1b9a85a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 13:10:21 +0200 Subject: [PATCH 11/17] docs: move new error API to the right doc --- docs/features/common_functional_options.md | 17 ----------------- docs/modules/index.md | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md index 0cf9b0bb03..d7a23c8559 100644 --- a/docs/features/common_functional_options.md +++ b/docs/features/common_functional_options.md @@ -164,20 +164,3 @@ The above example is updating the predefined command of the image, **appending** !!!info This can't be used to replace the command, only to append options. - -!!!warning - The interface definition for `ContainerCustomizer` was changed to allow - errors the be correctly processed, specifically `Customize` method was - changed from: - -```go -Customize(req *GenericContainerRequest) -``` - -To: - -```go -Customize(req *GenericContainerRequest) error -``` - -- Not available until the next release of testcontainers-go :material-tag: main diff --git a/docs/modules/index.md b/docs/modules/index.md index 7cf304735d..fb8095f0db 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -105,7 +105,22 @@ We are going to propose a set of steps to follow when adding types and methods t - Make sure a public `Container` type exists for the module. This type has to use composition to embed the `testcontainers.Container` type, promoting all the methods from it. - Make sure a `RunContainer` function exists and is public. This function is the entrypoint to the module and will define the initial values for a `testcontainers.GenericContainerRequest` struct, including the image, the default exposed ports, wait strategies, etc. Therefore, the function must initialise the container request with the default values. -- Define container options for the module leveraging the `testcontainers.ContainerCustomizer` interface, that has one single method: `Customize(req *GenericContainerRequest)`. +- Define container options for the module leveraging the `testcontainers.ContainerCustomizer` interface, that has one single method: `Customize(req *GenericContainerRequest) error`. + +!!!warning + The interface definition for `ContainerCustomizer` was changed to allow errors to be correctly processed. + More specifically, the `Customize` method was changed from: + + ```go + Customize(req *GenericContainerRequest) + ``` + + To: + + ```go + Customize(req *GenericContainerRequest) error + ``` + - We consider that a best practice for the options is define a function using the `With` prefix, that returns a function returning a modified `testcontainers.GenericContainerRequest` type. For that, the library already provides a `testcontainers.CustomizeRequestOption` type implementing the `ContainerCustomizer` interface, and we encourage you to use this type for creating your own customizer functions. - At the same time, you could need to create your own container customizers for your module. Make sure they implement the `testcontainers.ContainerCustomizer` interface. Defining your own customizer functions is useful when you need to transfer a certain state that is not present at the `ContainerRequest` for the container, possibly using an intermediate Config struct. - The options will be passed to the `RunContainer` function as variadic arguments after the Go context, and they will be processed right after defining the initial `testcontainers.GenericContainerRequest` struct using a for loop. From efe1c2d4c7c4f444d9fe15d95444fea1eb0b0318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 13:13:47 +0200 Subject: [PATCH 12/17] docs: move customise request to the bottom --- docs/modules/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/index.md b/docs/modules/index.md index fb8095f0db..1502c83fdf 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -191,13 +191,13 @@ func (c *Container) ConnectionString(ctx context.Context) (string, error) {...} In order to simplify the creation of the container for a given module, `Testcontainers for Go` provides a set of `testcontainers.CustomizeRequestOption` functions to customize the container request for the module. These options are: -- `testcontainers.CustomizeRequest`: a function that merges the default options with the ones provided by the user. Recommended for completely customizing the container request. - `testcontainers.WithImage`: a function that sets the image for the container request. - `testcontainers.WithConfigModifier`: a function that sets the config Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information. - `testcontainers.WithEndpointSettingsModifier`: a function that sets the endpoint settings Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information. - `testcontainers.WithHostConfigModifier`: a function that sets the host config Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information. - `testcontainers.WithWaitStrategy`: a function that sets the wait strategy for the container request, adding all the passed wait strategies to the container request, using a `testcontainers.MultiStrategy` with 60 seconds of deadline. Please see [Wait strategies](../features/wait/multi.md) for more information. - `testcontainers.WithWaitStrategyAndDeadline`: a function that sets the wait strategy for the container request, adding all the passed wait strategies to the container request, using a `testcontainers.MultiStrategy` with the passed deadline. Please see [Wait strategies](../features/wait/multi.md) for more information. +- `testcontainers.CustomizeRequest`: a function that merges the default options with the ones provided by the user. Recommended for completely customizing the container request. ### Update Go dependencies in the modules From 67bd3895aff29d7bb9978be80ea8b78e64011de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 13:20:30 +0200 Subject: [PATCH 13/17] docs: sync functional options --- docs/modules/index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/modules/index.md b/docs/modules/index.md index 1502c83fdf..b279765e9d 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -192,6 +192,17 @@ func (c *Container) ConnectionString(ctx context.Context) (string, error) {...} In order to simplify the creation of the container for a given module, `Testcontainers for Go` provides a set of `testcontainers.CustomizeRequestOption` functions to customize the container request for the module. These options are: - `testcontainers.WithImage`: a function that sets the image for the container request. +- `testcontainers.WithImageSubstitutors`: a function that sets your own substitutions to the container images. +- `testcontainers.WithEnv`: a function that sets the environment variables for the container request. +- `testcontainers.WithHostPortAccess`: a function that enables the container to access a port that is already running in the host. +- `testcontainers.WithLogConsumers`: a function that sets the log consumers for the container request. +- `testcontainers.WithLogger`: a function that sets the logger for the container request. +- `testcontainers.WithWaitStrategy`: a function that sets the wait strategy for the container request. +- `testcontainers.WithWaitStrategyAndDeadline`: a function that sets the wait strategy for the container request with a deadline. +- `testcontainers.WithStartupCommand`: a function that sets the execution of a command when the container starts. +- `testcontainers.WithAfterReadyCommand`: a function that sets the execution of a command right after the container is ready (its wait strategy is satisfied). +- `testcontainers.WithNetwork`: a function that sets the network and the network aliases for the container request. +- `testcontainers.WithNewNetwork`: a function that sets the network aliases for a throw-away network for the container request. - `testcontainers.WithConfigModifier`: a function that sets the config Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information. - `testcontainers.WithEndpointSettingsModifier`: a function that sets the endpoint settings Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information. - `testcontainers.WithHostConfigModifier`: a function that sets the host config Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information. From f26f89155100899d00b55da38f74d931e98bfb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 17:38:35 +0200 Subject: [PATCH 14/17] chore: avoid panics in localstack --- docs/modules/localstack.md | 2 +- modules/localstack/localstack.go | 2 +- modules/localstack/types.go | 28 +++++++++++++++++----------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/modules/localstack.md b/docs/modules/localstack.md index 52a07c3e22..206f40d82f 100644 --- a/docs/modules/localstack.md +++ b/docs/modules/localstack.md @@ -23,7 +23,7 @@ Running LocalStack as a stand-in for multiple AWS services during a test: Environment variables listed in [Localstack's README](https://github.com/localstack/localstack#configurations) may be used to customize Localstack's configuration. -Use the `OverrideContainerRequest` option when creating the `LocalStackContainer` to apply configuration settings. +Use the `testcontainers.WithEnv` option when creating the `LocalStackContainer` to apply those variables. ## Module reference diff --git a/modules/localstack/localstack.go b/modules/localstack/localstack.go index ae4c7c8c01..4dbd08d53d 100644 --- a/modules/localstack/localstack.go +++ b/modules/localstack/localstack.go @@ -121,9 +121,9 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize return c, nil } +// Deprecated: use RunContainer instead // StartContainer creates an instance of the LocalStack container type, being possible to pass a custom request and options: // - overrideReq: a function that can be used to override the default container request, usually used to set the image version, environment variables for localstack, etc. -// Deprecated: use RunContainer instead func StartContainer(ctx context.Context, overrideReq OverrideContainerRequestOption) (*LocalStackContainer, error) { return RunContainer(ctx, overrideReq) } diff --git a/modules/localstack/types.go b/modules/localstack/types.go index 962975841a..c62555b803 100644 --- a/modules/localstack/types.go +++ b/modules/localstack/types.go @@ -15,40 +15,46 @@ type LocalStackContainerRequest struct { testcontainers.GenericContainerRequest } +// Deprecated: use testcontainers.ContainerCustomizer instead // OverrideContainerRequestOption is a type that can be used to configure the Testcontainers container request. // The passed request will be merged with the default one. -// Deprecated: use testcontainers.ContainerCustomizer instead -type OverrideContainerRequestOption func(req testcontainers.ContainerRequest) testcontainers.ContainerRequest +type OverrideContainerRequestOption func(req testcontainers.ContainerRequest) (testcontainers.ContainerRequest, error) -// NoopOverrideContainerRequest returns a helper function that does not override the container request // Deprecated: use testcontainers.ContainerCustomizer instead -var NoopOverrideContainerRequest = func(req testcontainers.ContainerRequest) testcontainers.ContainerRequest { - return req +// NoopOverrideContainerRequest returns a helper function that does not override the container request +var NoopOverrideContainerRequest = func(req testcontainers.ContainerRequest) (testcontainers.ContainerRequest, error) { + return req, nil } +// Deprecated: use testcontainers.ContainerCustomizer instead func (opt OverrideContainerRequestOption) Customize(req *testcontainers.GenericContainerRequest) error { - req.ContainerRequest = opt(req.ContainerRequest) + r, err := opt(req.ContainerRequest) + if err != nil { + return err + } + + req.ContainerRequest = r return nil } -// OverrideContainerRequest returns a function that can be used to merge the passed container request with one that is created by the LocalStack container // Deprecated: use testcontainers.CustomizeRequest instead -func OverrideContainerRequest(r testcontainers.ContainerRequest) func(req testcontainers.ContainerRequest) testcontainers.ContainerRequest { +// OverrideContainerRequest returns a function that can be used to merge the passed container request with one that is created by the LocalStack container +func OverrideContainerRequest(r testcontainers.ContainerRequest) func(req testcontainers.ContainerRequest) (testcontainers.ContainerRequest, error) { destContainerReq := testcontainers.GenericContainerRequest{ ContainerRequest: r, } - return func(req testcontainers.ContainerRequest) testcontainers.ContainerRequest { + return func(req testcontainers.ContainerRequest) (testcontainers.ContainerRequest, error) { srcContainerReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, } opt := testcontainers.CustomizeRequest(destContainerReq) if err := opt.Customize(&srcContainerReq); err != nil { - panic(err) + return testcontainers.ContainerRequest{}, err } - return srcContainerReq.ContainerRequest + return srcContainerReq.ContainerRequest, nil } } From 18df9d02c8270b79737cf3ee261628ec5651b89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 18:03:20 +0200 Subject: [PATCH 15/17] chore: require no error in test --- options_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options_test.go b/options_test.go index 7674822128..94d01adc59 100644 --- a/options_test.go +++ b/options_test.go @@ -242,7 +242,7 @@ func TestWithHostPortAccess(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { opt := testcontainers.WithHostPortAccess(tc.hostPorts...) - opt.Customize(tc.req) + require.NoError(t, opt.Customize(tc.req)) require.Equal(t, tc.expect, tc.req.HostAccessPorts) }) } From 70f1eac89dbb05cbd0c1b4d722db3bcb8b587fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 18:04:28 +0200 Subject: [PATCH 16/17] chore: ignore error in copy API, using nolint:errcheck --- port_forwarding.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/port_forwarding.go b/port_forwarding.go index eb9f7456cf..98b66dc307 100644 --- a/port_forwarding.go +++ b/port_forwarding.go @@ -326,12 +326,12 @@ func (pf *PortForwarder) runTunnel(ctx context.Context, remote net.Conn) { done := make(chan struct{}, 2) go func() { - _, _ = io.Copy(local, remote) + io.Copy(local, remote) //nolint:errcheck // Nothing we can usefully do with the error done <- struct{}{} }() go func() { - _, _ = io.Copy(remote, local) + io.Copy(remote, local) //nolint:errcheck // Nothing we can usefully do with the error done <- struct{}{} }() From 0e7cb731eb3dc98871901b2a935d0b40103716f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Thu, 25 Apr 2024 20:39:31 +0200 Subject: [PATCH 17/17] chore: convert log into error --- modules/neo4j/config.go | 3 +-- modules/neo4j/neo4j_test.go | 20 ++++++-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go index 02c6afb878..91eb3e415b 100644 --- a/modules/neo4j/config.go +++ b/modules/neo4j/config.go @@ -106,8 +106,7 @@ func addSetting(req *testcontainers.GenericContainerRequest, key string, newVal if oldVal, found := req.Env[normalizedKey]; found { // make sure AUTH is not overwritten by a setting if key == "AUTH" { - req.Logger.Printf("setting %q is not permitted, WithAdminPassword has already been set\n", normalizedKey) - return nil + return fmt.Errorf("setting %q is not permitted, WithAdminPassword has already been set", normalizedKey) } req.Logger.Printf("setting %q with value %q is now overwritten with value %q\n", []any{key, oldVal, newVal}...) diff --git a/modules/neo4j/neo4j_test.go b/modules/neo4j/neo4j_test.go index 4c8413a5f4..23717d4585 100644 --- a/modules/neo4j/neo4j_test.go +++ b/modules/neo4j/neo4j_test.go @@ -115,24 +115,16 @@ func TestNeo4jWithWrongSettings(outer *testing.T) { }) }) - outer.Run("ignores auth setting outside WithAdminPassword", func(t *testing.T) { + outer.Run("auth setting outside WithAdminPassword raises error", func(t *testing.T) { container, err := neo4j.RunContainer(ctx, neo4j.WithAdminPassword(testPassword), - neo4j.WithNeo4jSetting("AUTH", "neo4j/thisisgonnabeignored"), + neo4j.WithNeo4jSetting("AUTH", "neo4j/thisisgonnafail"), ) - if err != nil { - t.Fatalf("expected env to successfully run but did not: %s", err) + if err == nil { + t.Fatalf("expected env to fail due to conflicting auth settings but did not") } - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - outer.Fatalf("failed to terminate container: %s", err) - } - }) - - env := getContainerEnv(t, ctx, container) - - if !strings.Contains(env, "NEO4J_AUTH=neo4j/"+testPassword) { - t.Fatalf("expected WithAdminPassword to have higher precedence than auth set with WithNeo4jSetting") + if container != nil { + t.Fatalf("container must not be created with conflicting auth settings") } })