Skip to content

Commit

Permalink
Merge pull request #386 from baez90/384-container-mounts
Browse files Browse the repository at this point in the history
Replace ContainerRequest.BindMounts and ContainerRequest.VolumeMounts with ContainerRequest.Mounts as dedicated type
  • Loading branch information
gianarb authored Dec 21, 2021
2 parents 6b77020 + acb1983 commit 9c3076f
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 32 deletions.
20 changes: 17 additions & 3 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

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

"github.com/docker/docker/pkg/archive"
"github.com/docker/go-connections/nat"

Expand Down Expand Up @@ -89,8 +88,7 @@ type ContainerRequest struct {
ExposedPorts []string // allow specifying protocol info
Cmd []string
Labels map[string]string
BindMounts map[string]string
VolumeMounts map[string]string
Mounts ContainerMounts
Tmpfs map[string]string
RegistryCred string
WaitingFor wait.Strategy
Expand Down Expand Up @@ -135,6 +133,7 @@ func (c *ContainerRequest) Validate() error {
validationMethods := []func() error{
c.validateContextAndImage,
c.validateContextOrImageIsSpecified,
c.validateMounts,
}

var err error
Expand Down Expand Up @@ -200,3 +199,18 @@ func (c *ContainerRequest) validateContextOrImageIsSpecified() error {

return nil
}

func (c *ContainerRequest) validateMounts() error {
targets := make(map[string]bool, len(c.Mounts))

for idx := range c.Mounts {
m := c.Mounts[idx]
targetPath := m.Target.Target()
if targets[targetPath] {
return fmt.Errorf("%w: %s", ErrDuplicateMountTarget, targetPath)
} else {
targets[targetPath] = true
}
}
return nil
}
79 changes: 76 additions & 3 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/testcontainers/testcontainers-go/wait"
)

Expand Down Expand Up @@ -50,6 +52,22 @@ func Test_ContainerValidation(t *testing.T) {
},
},
},
ContainerValidationTestCase{
Name: "Can mount same source to multiple targets",
ExpectedError: nil,
ContainerRequest: ContainerRequest{
Image: "redis:latest",
Mounts: Mounts(BindMount("/data", "/srv"), BindMount("/data", "/data")),
},
},
ContainerValidationTestCase{
Name: "Cannot mount multiple sources to same target",
ExpectedError: errors.New("duplicate mount target detected: /data"),
ContainerRequest: ContainerRequest{
Image: "redis:latest",
Mounts: Mounts(BindMount("/srv", "/data"), BindMount("/data", "/data")),
},
},
}

for _, testCase := range testTable {
Expand All @@ -66,7 +84,6 @@ func Test_ContainerValidation(t *testing.T) {
}
})
}

}

func Test_GetDockerfile(t *testing.T) {
Expand Down Expand Up @@ -230,7 +247,7 @@ func Test_BuildImageWithContexts(t *testing.T) {
ContextArchive: func() (io.Reader, error) {
return nil, nil
},
ExpectedError: errors.New("failed to create container: you must specify either a build context or an image"),
ExpectedError: errors.New("you must specify either a build context or an image: failed to create container"),
},
}

Expand Down Expand Up @@ -282,7 +299,7 @@ func Test_GetLogsFromFailedContainer(t *testing.T) {
Started: true,
})

if err != nil && err.Error() != "failed to start container: context deadline exceeded" {
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
} else if err == nil {
c.Terminate(ctx)
Expand Down Expand Up @@ -341,3 +358,59 @@ func createTestContainer(t *testing.T, ctx context.Context) int {

return port.Int()
}

func TestBindMount(t *testing.T) {
type args struct {
hostPath string
mountTarget ContainerMountTarget
}
tests := []struct {
name string
args args
want ContainerMount
}{
{
name: "/var/run/docker.sock:/var/run/docker.sock",
args: args{hostPath: "/var/run/docker.sock", mountTarget: "/var/run/docker.sock"},
want: ContainerMount{Source: GenericBindMountSource{HostPath: "/var/run/docker.sock"}, Target: "/var/run/docker.sock"},
},
{
name: "/var/lib/app/data:/data",
args: args{hostPath: "/var/lib/app/data", mountTarget: "/data"},
want: ContainerMount{Source: GenericBindMountSource{HostPath: "/var/lib/app/data"}, Target: "/data"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, BindMount(tt.args.hostPath, tt.args.mountTarget), "BindMount(%v, %v)", tt.args.hostPath, tt.args.mountTarget)
})
}
}

func TestVolumeMount(t *testing.T) {
type args struct {
volumeName string
mountTarget ContainerMountTarget
}
tests := []struct {
name string
args args
want ContainerMount
}{
{
name: "sample-data:/data",
args: args{volumeName: "sample-data", mountTarget: "/data"},
want: ContainerMount{Source: GenericVolumeMountSource{Name: "sample-data"}, Target: "/data"},
},
{
name: "web:/var/nginx/html",
args: args{volumeName: "web", mountTarget: "/var/nginx/html"},
want: ContainerMount{Source: GenericVolumeMountSource{Name: "web"}, Target: "/var/nginx/html"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, VolumeMount(tt.args.volumeName, tt.args.mountTarget), "VolumeMount(%v, %v)", tt.args.volumeName, tt.args.mountTarget)
})
}
}
25 changes: 7 additions & 18 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
Expand All @@ -32,8 +31,12 @@ import (
"github.com/testcontainers/testcontainers-go/wait"
)

// Implement interfaces
var _ Container = (*DockerContainer)(nil)
var (
// Implement interfaces
_ Container = (*DockerContainer)(nil)

ErrDuplicateMountTarget = errors.New("duplicate mount target detected")
)

const (
Bridge = "bridge" // Bridge network name (as well as driver)
Expand Down Expand Up @@ -741,21 +744,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}

// prepare mounts
mounts := []mount.Mount{}
for innerPath, hostPath := range req.BindMounts {
mounts = append(mounts, mount.Mount{
Type: mount.TypeBind,
Source: hostPath,
Target: innerPath,
})
}
for innerPath, volumeName := range req.VolumeMounts {
mounts = append(mounts, mount.Mount{
Type: mount.TypeVolume,
Source: volumeName,
Target: innerPath,
})
}
mounts := mapToDockerMounts(req.Mounts)

hostConfig := &container.HostConfig{
PortBindings: exposedPortMap,
Expand Down
116 changes: 116 additions & 0 deletions docker_mounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package testcontainers

import "github.com/docker/docker/api/types/mount"

var (
mountTypeMapping = map[MountType]mount.Type{
MountTypeBind: mount.TypeBind,
MountTypeVolume: mount.TypeVolume,
MountTypeTmpfs: mount.TypeTmpfs,
MountTypePipe: mount.TypeNamedPipe,
}
)

// BindMounter can optionally be implemented by mount sources
// to support advanced scenarios based on mount.BindOptions
type BindMounter interface {
GetBindOptions() *mount.BindOptions
}

// VolumeMounter can optionally be implemented by mount sources
// to support advanced scenarios based on mount.VolumeOptions
type VolumeMounter interface {
GetVolumeOptions() *mount.VolumeOptions
}

// TmpfsMounter can optionally be implemented by mount sources
// to support advanced scenarios based on mount.TmpfsOptions
type TmpfsMounter interface {
GetTmpfsOptions() *mount.TmpfsOptions
}

type DockerBindMountSource struct {
*mount.BindOptions

// HostPath is the path mounted into the container
// the same host path might be mounted to multiple locations withing a single container
HostPath string
}

func (s DockerBindMountSource) Source() string {
return s.HostPath
}

func (DockerBindMountSource) Type() MountType {
return MountTypeBind
}

func (s DockerBindMountSource) GetBindOptions() *mount.BindOptions {
return s.BindOptions
}

type DockerVolumeMountSource struct {
*mount.VolumeOptions

// Name refers to the name of the volume to be mounted
// the same volume might be mounted to multiple locations within a single container
Name string
}

func (s DockerVolumeMountSource) Source() string {
return s.Name
}

func (DockerVolumeMountSource) Type() MountType {
return MountTypeVolume
}

func (s DockerVolumeMountSource) GetVolumeOptions() *mount.VolumeOptions {
return s.VolumeOptions
}

type DockerTmpfsMountSource struct {
GenericTmpfsMountSource
*mount.TmpfsOptions
}

func (s DockerTmpfsMountSource) GetTmpfsOptions() *mount.TmpfsOptions {
return s.TmpfsOptions
}

// mapToDockerMounts maps the given []ContainerMount to the corresponding
// []mount.Mount for further processing
func mapToDockerMounts(containerMounts ContainerMounts) []mount.Mount {
mounts := make([]mount.Mount, 0, len(containerMounts))

for idx := range containerMounts {
m := containerMounts[idx]

var mountType mount.Type
if mt, ok := mountTypeMapping[m.Source.Type()]; ok {
mountType = mt
} else {
continue
}

containerMount := mount.Mount{
Type: mountType,
Source: m.Source.Source(),
ReadOnly: m.ReadOnly,
Target: m.Target.Target(),
}

switch typedMounter := m.Source.(type) {
case BindMounter:
containerMount.BindOptions = typedMounter.GetBindOptions()
case VolumeMounter:
containerMount.VolumeOptions = typedMounter.GetVolumeOptions()
case TmpfsMounter:
containerMount.TmpfsOptions = typedMounter.GetTmpfsOptions()
}

mounts = append(mounts, containerMount)
}

return mounts
}
9 changes: 4 additions & 5 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1391,11 +1391,10 @@ func TestContainerCreationWithBindAndVolume(t *testing.T) {
// Create the container that writes into the mounted volume.
bashC, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: "bash",
BindMounts: map[string]string{"/hello.sh": absPath},
VolumeMounts: map[string]string{"/data": volumeName},
Cmd: []string{"bash", "/hello.sh"},
WaitingFor: wait.ForLog("done"),
Image: "bash",
Mounts: Mounts(BindMount(absPath, "/hello.sh"), VolumeMount(volumeName, "/data")),
Cmd: []string{"bash", "/hello.sh"},
WaitingFor: wait.ForLog("done"),
},
Started: true,
})
Expand Down
Loading

0 comments on commit 9c3076f

Please sign in to comment.