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_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, }, { diff --git a/docs/modules/index.md b/docs/modules/index.md index 5d54b2e38f..b279765e9d 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. @@ -130,7 +145,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 { @@ -174,13 +191,24 @@ 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.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. - `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 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/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/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 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 e2ef604c99..16488b1cb5 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/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/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/elasticsearch/elasticsearch.go b/modules/elasticsearch/elasticsearch.go index ea846ecbfd..2ea0a8b8ba 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/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/k3s/k3s.go b/modules/k3s/k3s.go index 83fdde4338..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 } } @@ -84,7 +86,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 +103,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 664f7e0695..c0e9f01fe0 100644 --- a/modules/k6/k6.go +++ b/modules/k6/k6.go @@ -69,12 +69,14 @@ func downloadFileFromDescription(d DownloadableFile) error { // 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 { - 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) } @@ -83,7 +85,7 @@ func WithTestScript(scriptPath string) testcontainers.CustomizeRequestOption { // The script base name is not a path, neither absolute or relative and should // be just the file name of the script func WithTestScriptReader(reader io.Reader, scriptBaseName string) testcontainers.CustomizeRequestOption { - opt := func(req *testcontainers.GenericContainerRequest) { + opt := func(req *testcontainers.GenericContainerRequest) error { target := "/home/k6x/" + scriptBaseName req.Files = append( req.Files, @@ -96,16 +98,19 @@ func WithTestScriptReader(reader io.Reader, scriptBaseName string) testcontainer // add script to the k6 run command req.Cmd = append(req.Cmd, target) + + return nil } return opt } // WithRemoteTestScript takes a RemoteTestFileDescription and copies to container 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()) @@ -113,15 +118,19 @@ func WithRemoteTestScript(d DownloadableFile) testcontainers.CustomizeRequestOpt // 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 } } @@ -141,7 +150,7 @@ func WithCache() testcontainers.CustomizeRequestOption { } } - return func(req *testcontainers.GenericContainerRequest) { + return func(req *testcontainers.GenericContainerRequest) error { mount := testcontainers.ContainerMount{ Source: testcontainers.DockerVolumeMountSource{ Name: cacheVol, @@ -150,6 +159,8 @@ func WithCache() testcontainers.CustomizeRequestOption { Target: "/cache", } req.Mounts = append(req.Mounts, mount) + + return nil } } @@ -167,7 +178,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..4dbd08d53d 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) { @@ -119,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 a57ae1808f..c62555b803 100644 --- a/modules/localstack/types.go +++ b/modules/localstack/types.go @@ -15,36 +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 } -func (opt OverrideContainerRequestOption) Customize(req *testcontainers.GenericContainerRequest) { - req.ContainerRequest = opt(req.ContainerRequest) +// Deprecated: use testcontainers.ContainerCustomizer instead +func (opt OverrideContainerRequestOption) Customize(req *testcontainers.GenericContainerRequest) error { + 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) - opt.Customize(&srcContainerReq) + if err := opt.Customize(&srcContainerReq); err != nil { + return testcontainers.ContainerRequest{}, err + } - return srcContainerReq.ContainerRequest + return srcContainerReq.ContainerRequest, nil } } diff --git a/modules/mariadb/mariadb.go b/modules/mariadb/mariadb.go index 503473f3a5..a84410f262 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/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) } 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 4c004d38e2..c4e2bd0b7e 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"] @@ -123,36 +127,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{ @@ -163,5 +175,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 3471824658..3204f57796 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 1b62363cb9..91eb3e415b 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,41 @@ 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}...) } + req.Env[normalizedKey] = newVal + + return nil } func validate(req *testcontainers.GenericContainerRequest) error { 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/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") } }) 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/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 ccc66ba12a..f8a3a1c630 100644 --- a/modules/postgres/postgres.go +++ b/modules/postgres/postgres.go @@ -60,7 +60,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", @@ -69,6 +69,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 } } @@ -76,14 +78,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{ @@ -94,6 +98,8 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { initScripts = append(initScripts, cf) } req.Files = append(req.Files, initScripts...) + + return nil } } @@ -101,8 +107,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 } } @@ -111,12 +119,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 } } @@ -139,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/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 a6dec1a779..612c760d90 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 bea0abf691..f6b4603fde 100644 --- a/modules/redpanda/redpanda.go +++ b/modules/redpanda/redpanda.go @@ -90,7 +90,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 @@ -197,7 +199,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/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/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) 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 b844a9e2b5..3782e148de 100644 --- a/modules/weaviate/weaviate.go +++ b/modules/weaviate/weaviate.go @@ -42,7 +42,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 39d9fc7a13..95b6d9394f 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,31 +69,38 @@ 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 } } // 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 } } // 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 } } @@ -149,19 +161,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 } } @@ -209,7 +224,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{}, } @@ -224,6 +239,8 @@ func WithStartupCommand(execs ...Executable) CustomizeRequestOption { } req.LifecycleHooks = append(req.LifecycleHooks, startupCommandsHook) + + return nil } } @@ -231,7 +248,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 { @@ -246,6 +263,8 @@ func WithAfterReadyCommand(execs ...Executable) CustomizeRequestOption { req.LifecycleHooks = append(req.LifecycleHooks, ContainerLifecycleHooks{ PostReadies: postReadiesHook, }) + + return nil } } @@ -256,7 +275,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 465e18ade5..94d01adc59 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) }) } @@ -238,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) }) } diff --git a/port_forwarding.go b/port_forwarding.go index 98c2e45154..98b66dc307 100644 --- a/port_forwarding.go +++ b/port_forwarding.go @@ -68,7 +68,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. @@ -78,6 +78,7 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, p ...int) (Cont req.NetworkAliases = make(map[string][]string) } req.NetworkAliases[networkName] = aliases + return nil } } @@ -129,8 +130,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{ @@ -157,7 +157,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) @@ -324,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{}{} }() diff --git a/wait/testdata/main.go b/wait/testdata/main.go index 95baa2f5b7..b18008db75 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" @@ -80,7 +81,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) } }()