diff --git a/container.go b/container.go index dc03eb82ed..c23dfef228 100644 --- a/container.go +++ b/container.go @@ -112,13 +112,14 @@ type ContainerRequest struct { NetworkAliases map[string][]string // for specifying network aliases NetworkMode container.NetworkMode Resources container.Resources - Files []ContainerFile // files which will be copied when container starts - User string // for specifying uid:gid - SkipReaper bool // indicates whether we skip setting up a reaper for this - ReaperImage string // alternative reaper image - AutoRemove bool // if set to true, the container will be removed from the host when stopped - AlwaysPullImage bool // Always pull image - ImagePlatform string // ImagePlatform describes the platform which the image runs on. + Files []ContainerFile // files which will be copied when container starts + User string // for specifying uid:gid + SkipReaper bool // indicates whether we skip setting up a reaper for this + ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper image + ReaperOptions []ContainerOption // options for the reaper + AutoRemove bool // if set to true, the container will be removed from the host when stopped + AlwaysPullImage bool // Always pull image + ImagePlatform string // ImagePlatform describes the platform which the image runs on. Binds []string ShmSize int64 // Amount of memory shared with the host (in bytes) CapAdd []string // Add Linux capabilities @@ -149,6 +150,29 @@ func (f GenericProviderOptionFunc) ApplyGenericTo(opts *GenericProviderOptions) f(opts) } +// containerOptions functional options for a container +type containerOptions struct { + ImageName string + RegistryCredentials string +} + +// functional option for setting the reaper image +type ContainerOption func(*containerOptions) + +// WithImageName sets the reaper image name +func WithImageName(imageName string) ContainerOption { + return func(o *containerOptions) { + o.ImageName = imageName + } +} + +// WithRegistryCredentials sets the reaper registry credentials +func WithRegistryCredentials(registryCredentials string) ContainerOption { + return func(o *containerOptions) { + o.RegistryCredentials = registryCredentials + } +} + // possible provider types const ( ProviderDocker ProviderType = iota // Docker is default = 0 diff --git a/docker.go b/docker.go index 4f8192aa98..0767406a8c 100644 --- a/docker.go +++ b/docker.go @@ -961,11 +961,18 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque sessionID := sessionID() + reaperOpts := containerOptions{ + ImageName: req.ReaperImage, + } + for _, opt := range req.ReaperOptions { + opt(&reaperOpts) + } + var termSignal chan bool // the reaper does not need to start a reaper for itself - isReaperContainer := strings.EqualFold(req.Image, reaperImage(req.ReaperImage)) + isReaperContainer := strings.EqualFold(req.Image, reaperImage(reaperOpts.ImageName)) if !req.SkipReaper && !isReaperContainer { - r, err := NewReaper(context.WithValue(ctx, dockerHostContextKey, p.host), sessionID.String(), p, req.ReaperImage) + r, err := newReaper(context.WithValue(ctx, dockerHostContextKey, p.host), sessionID.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating reaper failed", err) } @@ -1182,7 +1189,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain sessionID := sessionID() var termSignal chan bool if !req.SkipReaper { - r, err := NewReaper(context.WithValue(ctx, dockerHostContextKey, p.host), sessionID.String(), p, req.ReaperImage) + r, err := newReaper(context.WithValue(ctx, dockerHostContextKey, p.host), sessionID.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating reaper failed", err) } @@ -1337,7 +1344,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) var termSignal chan bool if !req.SkipReaper { sessionID := sessionID() - r, err := NewReaper(context.WithValue(ctx, dockerHostContextKey, p.host), sessionID.String(), p, req.ReaperImage) + r, err := newReaper(context.WithValue(ctx, dockerHostContextKey, p.host), sessionID.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating network reaper failed", err) } diff --git a/network.go b/network.go index eac39a5cad..e8436240fc 100644 --- a/network.go +++ b/network.go @@ -2,6 +2,7 @@ package testcontainers import ( "context" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types" @@ -39,6 +40,7 @@ type NetworkRequest struct { Attachable bool IPAM *network.IPAM - SkipReaper bool // indicates whether we skip setting up a reaper for this - ReaperImage string //alternative reaper registry + SkipReaper bool // indicates whether we skip setting up a reaper for this + ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper registry + ReaperOptions []ContainerOption // Reaper options to use for this network } diff --git a/reaper.go b/reaper.go index a82a8ba54f..0cc5146e36 100644 --- a/reaper.go +++ b/reaper.go @@ -40,7 +40,13 @@ type ReaperProvider interface { } // NewReaper creates a Reaper with a sessionID to identify containers and a provider to use +// Deprecated: it's not possible to create a reaper anymore. func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, reaperImageName string) (*Reaper, error) { + return newReaper(ctx, sessionID, provider, WithImageName(reaperImageName)) +} + +// newReaper creates a Reaper with a sessionID to identify containers and a provider to use +func newReaper(ctx context.Context, sessionID string, provider ReaperProvider, opts ...ContainerOption) (*Reaper, error) { mutex.Lock() defer mutex.Unlock() // If reaper already exists re-use it @@ -58,19 +64,30 @@ func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, r listeningPort := nat.Port("8080/tcp") + reaperOpts := containerOptions{} + + for _, opt := range opts { + opt(&reaperOpts) + } + req := ContainerRequest{ - Image: reaperImage(reaperImageName), + Image: reaperImage(reaperOpts.ImageName), ExposedPorts: []string{string(listeningPort)}, NetworkMode: Bridge, Labels: map[string]string{ TestcontainerLabelIsReaper: "true", }, - SkipReaper: true, - Mounts: Mounts(BindMount(dockerHost, "/var/run/docker.sock")), - AutoRemove: true, - WaitingFor: wait.ForListeningPort(listeningPort), + SkipReaper: true, + RegistryCred: reaperOpts.RegistryCredentials, + Mounts: Mounts(BindMount(dockerHost, "/var/run/docker.sock")), + AutoRemove: true, + WaitingFor: wait.ForListeningPort(listeningPort), + ReaperOptions: opts, } + // keep backwards compatibility + req.ReaperImage = req.Image + // include reaper-specific labels to the reaper container for k, v := range reaper.Labels() { req.Labels[k] = v diff --git a/reaper_test.go b/reaper_test.go index fd22fff377..f90908dc0b 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -33,6 +33,7 @@ func (m *mockReaperProvider) Config() TestContainersConfig { func createContainerRequest(customize func(ContainerRequest) ContainerRequest) ContainerRequest { req := ContainerRequest{ Image: "reaperImage", + ReaperImage: "reaperImage", ExposedPorts: []string{"8080/tcp"}, Labels: map[string]string{ TestcontainerLabel: "true", @@ -44,6 +45,9 @@ func createContainerRequest(customize func(ContainerRequest) ContainerRequest) C AutoRemove: true, WaitingFor: wait.ForListeningPort(nat.Port("8080/tcp")), NetworkMode: "bridge", + ReaperOptions: []ContainerOption{ + WithImageName("reaperImage"), + }, } if customize == nil { return req @@ -53,6 +57,7 @@ func createContainerRequest(customize func(ContainerRequest) ContainerRequest) C } func Test_NewReaper(t *testing.T) { + defer func() { reaper = nil }() type cases struct { name string @@ -86,6 +91,16 @@ func Test_NewReaper(t *testing.T) { config: TestContainersConfig{}, ctx: context.WithValue(context.TODO(), dockerHostContextKey, "unix:///value/in/context.sock"), }, + { + name: "with registry credentials", + req: createContainerRequest(func(req ContainerRequest) ContainerRequest { + creds := "registry-creds" + req.RegistryCred = creds + req.ReaperOptions = append(req.ReaperOptions, WithRegistryCredentials(creds)) + return req + }), + config: TestContainersConfig{}, + }, } for _, test := range tests { @@ -100,7 +115,7 @@ func Test_NewReaper(t *testing.T) { test.ctx = context.TODO() } - _, err := NewReaper(test.ctx, "sessionId", provider, "reaperImage") + _, err := newReaper(test.ctx, "sessionId", provider, test.req.ReaperOptions...) // we should have errored out see mockReaperProvider.RunContainer assert.EqualError(t, err, "expected") @@ -110,6 +125,8 @@ func Test_NewReaper(t *testing.T) { } func Test_ExtractDockerHost(t *testing.T) { + defer func() { reaper = nil }() + t.Run("Docker Host as environment variable", func(t *testing.T) { t.Setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/path/to/docker.sock") host := extractDockerHost(context.Background()) @@ -147,3 +164,33 @@ func Test_ExtractDockerHost(t *testing.T) { assert.Equal(t, "/this/is/a/sample.sock", host) }) } + +func Test_ReaperForNetwork(t *testing.T) { + defer func() { reaper = nil }() + + ctx := context.Background() + + networkName := "test-network-with-custom-reaper" + + req := GenericNetworkRequest{ + NetworkRequest: NetworkRequest{ + Name: networkName, + CheckDuplicate: true, + ReaperOptions: []ContainerOption{ + WithRegistryCredentials("credentials"), + WithImageName("reaperImage"), + }, + }, + } + + provider := &mockReaperProvider{ + config: TestContainersConfig{}, + } + + _, err := newReaper(ctx, "sessionId", provider, req.ReaperOptions...) + assert.EqualError(t, err, "expected") + + assert.Equal(t, "credentials", provider.req.RegistryCred) + assert.Equal(t, "reaperImage", provider.req.Image) + assert.Equal(t, "reaperImage", provider.req.ReaperImage) +}