diff --git a/docker.go b/docker.go index fbb7298f74..f8a9e49081 100644 --- a/docker.go +++ b/docker.go @@ -311,6 +311,10 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro // // Default: timeout is 10 seconds. func (c *DockerContainer) Terminate(ctx context.Context, opts ...TerminateOption) error { + if c == nil { + return nil + } + options := NewTerminateOptions(ctx, opts...) err := c.Stop(options.Context(), options.StopTimeout()) if err != nil && !isCleanupSafe(err) { diff --git a/modules/azure/azurite/azurite.go b/modules/azure/azurite/azurite.go index e58fb53b72..331b7d6152 100644 --- a/modules/azure/azurite/azurite.go +++ b/modules/azure/azurite/azurite.go @@ -72,26 +72,15 @@ func (c *Container) serviceURL(ctx context.Context, srv service) (string, error) // Run creates an instance of the Azurite container type func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { - req := testcontainers.ContainerRequest{ - Image: img, - ExposedPorts: []string{BlobPort, QueuePort, TablePort}, - Env: map[string]string{}, - Entrypoint: []string{"azurite"}, - Cmd: []string{}, - } + moduleCmd := []string{} - genericContainerReq := testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, + moduleOpts := []testcontainers.ContainerCustomizer{ + testcontainers.WithEntrypoint("azurite"), + testcontainers.WithExposedPorts(BlobPort, QueuePort, TablePort), } // 1. Gather all config options (defaults and then apply provided options) settings := defaultOptions() - for _, opt := range opts { - if err := opt.Customize(&genericContainerReq); err != nil { - return nil, fmt.Errorf("customize: %w", err) - } - } // 2. evaluate the enabled services to apply the right wait strategy and Cmd options if len(settings.EnabledServices) > 0 { @@ -99,32 +88,31 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom for _, srv := range settings.EnabledServices { switch srv { case BlobService: - genericContainerReq.Cmd = append(genericContainerReq.Cmd, "--blobHost", "0.0.0.0") + moduleCmd = append(moduleCmd, "--blobHost", "0.0.0.0") waitingFor = append(waitingFor, wait.ForListeningPort(BlobPort)) case QueueService: - genericContainerReq.Cmd = append(genericContainerReq.Cmd, "--queueHost", "0.0.0.0") + moduleCmd = append(moduleCmd, "--queueHost", "0.0.0.0") waitingFor = append(waitingFor, wait.ForListeningPort(QueuePort)) case TableService: - genericContainerReq.Cmd = append(genericContainerReq.Cmd, "--tableHost", "0.0.0.0") + moduleCmd = append(moduleCmd, "--tableHost", "0.0.0.0") waitingFor = append(waitingFor, wait.ForListeningPort(TablePort)) } } - if genericContainerReq.WaitingFor != nil { - genericContainerReq.WaitingFor = wait.ForAll(genericContainerReq.WaitingFor, wait.ForAll(waitingFor...)) - } else { - genericContainerReq.WaitingFor = wait.ForAll(waitingFor...) - } + moduleOpts = append(moduleOpts, testcontainers.WithCmd(moduleCmd...)) + moduleOpts = append(moduleOpts, testcontainers.WithWaitStrategy(wait.ForAll(waitingFor...))) } - container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + moduleOpts = append(moduleOpts, opts...) + + ctr, err := testcontainers.Run(ctx, img, moduleOpts...) var c *Container - if container != nil { - c = &Container{Container: container, opts: settings} + if ctr != nil { + c = &Container{Container: ctr, opts: settings} } if err != nil { - return c, fmt.Errorf("generic container: %w", err) + return c, fmt.Errorf("run azurite: %w", err) } return c, nil diff --git a/modules/azure/azurite/options.go b/modules/azure/azurite/options.go index 70f464fcff..7e8d2dc780 100644 --- a/modules/azure/azurite/options.go +++ b/modules/azure/azurite/options.go @@ -20,15 +20,11 @@ func defaultOptions() options { // WithInMemoryPersistence is a custom option to enable in-memory persistence for Azurite. // This option is only available for Azurite v3.28.0 and later. func WithInMemoryPersistence(megabytes float64) testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) error { - cmd := []string{"--inMemoryPersistence"} + cmd := []string{"--inMemoryPersistence"} - if megabytes > 0 { - cmd = append(cmd, "--extentMemoryLimit", fmt.Sprintf("%f", megabytes)) - } - - req.Cmd = append(req.Cmd, cmd...) - - return nil + if megabytes > 0 { + cmd = append(cmd, "--extentMemoryLimit", fmt.Sprintf("%f", megabytes)) } + + return testcontainers.WithCmdArgs(cmd...) } diff --git a/modules/azure/eventhubs/eventhubs.go b/modules/azure/eventhubs/eventhubs.go index d4b4fd4b20..fda5bae85f 100644 --- a/modules/azure/eventhubs/eventhubs.go +++ b/modules/azure/eventhubs/eventhubs.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "strings" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/azure/azurite" @@ -69,40 +68,26 @@ func (c *Container) Terminate(ctx context.Context, opts ...testcontainers.Termin // Run creates an instance of the Azure Event Hubs container type func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { - req := testcontainers.ContainerRequest{ - Image: img, - ExposedPorts: []string{defaultAMPQPort, defaultHTTPPort}, - Env: make(map[string]string), - WaitingFor: wait.ForAll( + moduleOpts := []testcontainers.ContainerCustomizer{ + testcontainers.WithExposedPorts(defaultAMPQPort, defaultHTTPPort), + testcontainers.WithWaitStrategy(wait.ForAll( wait.ForListeningPort(defaultAMPQPort), wait.ForListeningPort(defaultHTTPPort), wait.ForHTTP("/health").WithPort(defaultHTTPPort).WithStatusCodeMatcher(func(status int) bool { return status == http.StatusOK }), - ), - } - - genericContainerReq := testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, + )), } defaultOptions := defaultOptions() for _, opt := range opts { - if err := opt.Customize(&genericContainerReq); err != nil { - return nil, fmt.Errorf("customize: %w", err) - } if o, ok := opt.(Option); ok { if err := o(&defaultOptions); err != nil { - return nil, fmt.Errorf("eventhubsoption: %w", err) + return nil, fmt.Errorf("eventhubs option: %w", err) } } } - if strings.ToUpper(genericContainerReq.Env["ACCEPT_EULA"]) != "Y" { - return nil, errors.New("EULA not accepted. Please use the WithAcceptEULA option to accept the EULA") - } - c := &Container{azuriteOptions: &defaultOptions} if defaultOptions.azuriteContainer == nil { @@ -124,20 +109,24 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } defaultOptions.azuriteContainer = azuriteContainer - genericContainerReq.Env["BLOB_SERVER"] = aliasAzurite - genericContainerReq.Env["METADATA_SERVER"] = aliasAzurite + moduleOpts = append(moduleOpts, testcontainers.WithEnv(map[string]string{ + "BLOB_SERVER": aliasAzurite, + "METADATA_SERVER": aliasAzurite, + })) // apply the network to the eventhubs container - err = network.WithNetwork([]string{aliasEventhubs}, azuriteNetwork)(&genericContainerReq) - if err != nil { - return c, fmt.Errorf("with network: %w", err) - } + moduleOpts = append(moduleOpts, network.WithNetwork([]string{aliasEventhubs}, azuriteNetwork)) } + moduleOpts = append(moduleOpts, opts...) + + // validate the EULA after all the options are applied + moduleOpts = append(moduleOpts, validateEula()) + var err error - c.Container, err = testcontainers.GenericContainer(ctx, genericContainerReq) + c.Container, err = testcontainers.Run(ctx, img, moduleOpts...) if err != nil { - return c, fmt.Errorf("generic container: %w", err) + return c, fmt.Errorf("run eventhubs: %w", err) } return c, nil diff --git a/modules/azure/eventhubs/eventhubs_test.go b/modules/azure/eventhubs/eventhubs_test.go index 2e49b16872..919450f525 100644 --- a/modules/azure/eventhubs/eventhubs_test.go +++ b/modules/azure/eventhubs/eventhubs_test.go @@ -78,6 +78,6 @@ func TestEventHubs_noEULA(t *testing.T) { ctx := context.Background() ctr, err := eventhubs.Run(ctx, "mcr.microsoft.com/azure-messaging/eventhubs-emulator:2.1.0") + testcontainers.CleanupContainer(t, ctr) require.Error(t, err) - require.Nil(t, ctr) } diff --git a/modules/azure/eventhubs/options.go b/modules/azure/eventhubs/options.go index f439d3df2d..25f19ee0a4 100644 --- a/modules/azure/eventhubs/options.go +++ b/modules/azure/eventhubs/options.go @@ -1,7 +1,9 @@ package eventhubs import ( + "errors" "io" + "strings" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/azure/azurite" @@ -24,7 +26,7 @@ func defaultOptions() options { // Satisfy the testcontainers.CustomizeRequestOption interface var _ testcontainers.ContainerCustomizer = (Option)(nil) -// Option is an option for the Redpanda container. +// Option is an option for the EventHubs container. type Option func(*options) error // Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. @@ -45,11 +47,9 @@ func WithAzurite(img string, opts ...testcontainers.ContainerCustomizer) Option // WithAcceptEULA sets the ACCEPT_EULA environment variable to "Y" for the eventhubs container. func WithAcceptEULA() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) error { - req.Env["ACCEPT_EULA"] = "Y" - - return nil - } + return testcontainers.WithEnv(map[string]string{ + "ACCEPT_EULA": "Y", + }) } // WithConfig sets the eventhubs config file for the eventhubs container, @@ -66,3 +66,14 @@ func WithConfig(r io.Reader) testcontainers.CustomizeRequestOption { return nil } } + +// validateEula validates that the EULA is accepted for the eventhubs container. +func validateEula() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + if strings.ToUpper(req.Env["ACCEPT_EULA"]) != "Y" { + return errors.New("EULA not accepted. Please use the WithAcceptEULA option to accept the EULA") + } + + return nil + } +} diff --git a/modules/azure/servicebus/options.go b/modules/azure/servicebus/options.go index b1af0b6a32..e29dbf645f 100644 --- a/modules/azure/servicebus/options.go +++ b/modules/azure/servicebus/options.go @@ -1,7 +1,9 @@ package servicebus import ( + "errors" "io" + "strings" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mssql" @@ -66,3 +68,14 @@ func WithConfig(r io.Reader) testcontainers.CustomizeRequestOption { return nil } } + +// validateEula validates that the EULA is accepted for the servicebus container. +func validateEula() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + if strings.ToUpper(req.Env["ACCEPT_EULA"]) != "Y" { + return errors.New("EULA not accepted. Please use the WithAcceptEULA option to accept the EULA") + } + + return nil + } +} diff --git a/modules/azure/servicebus/servicebus.go b/modules/azure/servicebus/servicebus.go index 2a3fe0f8e9..45c3ac8698 100644 --- a/modules/azure/servicebus/servicebus.go +++ b/modules/azure/servicebus/servicebus.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "strings" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mssql" @@ -72,33 +71,24 @@ func (c *Container) Terminate(ctx context.Context, opts ...testcontainers.Termin return errors.Join(errs...) } -// Run creates an instance of the Azure Event Hubs container type +// Run creates an instance of the Azure ServiceBus container type func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { - req := testcontainers.ContainerRequest{ - Image: img, - Env: map[string]string{ + moduleOpts := []testcontainers.ContainerCustomizer{ + testcontainers.WithExposedPorts(defaultPort, defaultHTTPPort), + testcontainers.WithEnv(map[string]string{ "SQL_WAIT_INTERVAL": "0", // default is zero because the MSSQL container is started first - }, - ExposedPorts: []string{defaultPort, defaultHTTPPort}, - WaitingFor: wait.ForAll( + }), + testcontainers.WithWaitStrategy(wait.ForAll( wait.ForListeningPort(defaultPort), wait.ForListeningPort(defaultHTTPPort), wait.ForHTTP("/health").WithPort(defaultHTTPPort).WithStatusCodeMatcher(func(status int) bool { return status == http.StatusOK }), - ), - } - - genericContainerReq := testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, + )), } defaultOptions := defaultOptions() for _, opt := range opts { - if err := opt.Customize(&genericContainerReq); err != nil { - return nil, fmt.Errorf("customize: %w", err) - } if o, ok := opt.(Option); ok { if err := o(&defaultOptions); err != nil { return nil, fmt.Errorf("servicebus option: %w", err) @@ -106,10 +96,6 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } } - if strings.ToUpper(genericContainerReq.Env["ACCEPT_EULA"]) != "Y" { - return nil, errors.New("EULA not accepted. Please use the WithAcceptEULA option to accept the EULA") - } - c := &Container{mssqlOptions: &defaultOptions} if defaultOptions.mssqlContainer == nil { @@ -133,26 +119,30 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } defaultOptions.mssqlContainer = mssqlContainer - genericContainerReq.Env["SQL_SERVER"] = aliasMSSQL - genericContainerReq.Env["MSSQL_SA_PASSWORD"] = mssqlContainer.Password() + moduleOpts = append(moduleOpts, testcontainers.WithEnv(map[string]string{ + "SQL_SERVER": aliasMSSQL, + "MSSQL_SA_PASSWORD": mssqlContainer.Password(), + })) - // apply the network to the eventhubs container - err = network.WithNetwork([]string{aliasServiceBus}, mssqlNetwork)(&genericContainerReq) - if err != nil { - return c, fmt.Errorf("with network: %w", err) - } + // apply the network to the servicebus container + moduleOpts = append(moduleOpts, network.WithNetwork([]string{aliasServiceBus}, mssqlNetwork)) } + moduleOpts = append(moduleOpts, opts...) + + // validate the EULA after all the options are applied + moduleOpts = append(moduleOpts, validateEula()) + var err error - c.Container, err = testcontainers.GenericContainer(ctx, genericContainerReq) + c.Container, err = testcontainers.Run(ctx, img, moduleOpts...) if err != nil { - return c, fmt.Errorf("generic container: %w", err) + return c, fmt.Errorf("run servicebus: %w", err) } return c, nil } -// ConnectionString returns the connection string for the eventhubs container, +// ConnectionString returns the connection string for the servicebus container, // using the following format: // Endpoint=sb://:;SharedAccessKeyName=;SharedAccessKey=;UseDevelopmentEmulator=true; func (c *Container) ConnectionString(ctx context.Context) (string, error) { diff --git a/modules/azure/servicebus/servicebus_test.go b/modules/azure/servicebus/servicebus_test.go index e8db5e47ee..7037b68d3a 100644 --- a/modules/azure/servicebus/servicebus_test.go +++ b/modules/azure/servicebus/servicebus_test.go @@ -78,6 +78,6 @@ func TestServiceBus_noEULA(t *testing.T) { ctx := context.Background() ctr, err := servicebus.Run(ctx, "mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2") + testcontainers.CleanupContainer(t, ctr) require.Error(t, err) - require.Nil(t, ctr) }