Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this nil-guard after finding that we have hit the classic “typed-nil inside an interface” pitfall: in eventhubs (and probably other modules) c.Container != nil is true because the interface is non-nil, but it holds a nil *DockerContainer. Calling its method dereferences a nil receiver and panics inside the Terminate method.

}

options := NewTerminateOptions(ctx, opts...)
err := c.Stop(options.Context(), options.StopTimeout())
if err != nil && !isCleanupSafe(err) {
Expand Down
42 changes: 15 additions & 27 deletions modules/azure/azurite/azurite.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,59 +72,47 @@ 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 {
waitingFor := make([]wait.Strategy, 0, len(settings.EnabledServices))
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
Expand Down
14 changes: 5 additions & 9 deletions modules/azure/azurite/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
45 changes: 17 additions & 28 deletions modules/azure/eventhubs/eventhubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/azure/azurite"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion modules/azure/eventhubs/eventhubs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
23 changes: 17 additions & 6 deletions modules/azure/eventhubs/options.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package eventhubs

import (
"errors"
"io"
"strings"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/azure/azurite"
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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
}
}
13 changes: 13 additions & 0 deletions modules/azure/servicebus/options.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package servicebus

import (
"errors"
"io"
"strings"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mssql"
Expand Down Expand Up @@ -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
}
}
52 changes: 21 additions & 31 deletions modules/azure/servicebus/servicebus.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mssql"
Expand Down Expand Up @@ -72,44 +71,31 @@ 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)
}
}
}

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 {
Expand All @@ -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://<hostname>:<port>;SharedAccessKeyName=<key-name>;SharedAccessKey=<key>;UseDevelopmentEmulator=true;
func (c *Container) ConnectionString(ctx context.Context) (string, error) {
Expand Down
2 changes: 1 addition & 1 deletion modules/azure/servicebus/servicebus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading