Skip to content

Commit

Permalink
feat: support passing registry credentials to the reaper (#647)
Browse files Browse the repository at this point in the history
* feat: support passing registry credentials to the reaper

* chore: support passing registry creds for the reaper when creating networks

* chore: support configuring the reaper as functional opts

* chore: remove unused field

* chore: deprecate public reaper constructor

We do not want to create reapers from outside the library

* chore: rename ReaperOption to ContainerOption

This will provide a better namespace for future work

* fix: extract reaper image before evaluating a new reaper

* chore: fallback to req.ReaperImage

Co-authored-by: Matthew McNew <me@mattmcnew.com>

* fix: missing comma delimiting struct field

* fix: indent

Co-authored-by: Matthew McNew <me@mattmcnew.com>
  • Loading branch information
mdelapenya and matthewmcnew authored Jan 2, 2023
1 parent 064a0c5 commit 67184ce
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 19 deletions.
38 changes: 31 additions & 7 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
6 changes: 4 additions & 2 deletions network.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testcontainers

import (
"context"

"github.com/docker/docker/api/types/network"

"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -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
}
27 changes: 22 additions & 5 deletions reaper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
49 changes: 48 additions & 1 deletion reaper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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")

Expand All @@ -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())
Expand Down Expand Up @@ -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)
}

0 comments on commit 67184ce

Please sign in to comment.