Skip to content
Open
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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ Last updated: 2020/01/03

#### Why `dktest` is better

* Uses the [official Docker SDK](https://github.com/docker/docker)
* [docker/docker](https://github.com/docker/docker) (aka [moby/moby](https://github.com/moby/moby)) uses [import path checking](https://golang.org/cmd/go/#hdr-Import_path_checking), so needs to be imported as `github.com/docker/docker`
* Uses the [official Docker SDK](https://github.com/moby/moby)
* [moby/moby](https://github.com/moby/moby) uses [import path checking](https://golang.org/cmd/go/#hdr-Import_path_checking), so needs to be imported as `github.com/moby/moby`
* Designed to run in the Go testing environment
* Smaller API surface
* Running Docker containers are automatically cleaned up
Expand Down
28 changes: 26 additions & 2 deletions container_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package dktest
import (
"fmt"
"strconv"
)

import (
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/network"
)

func mapHost(h string) string {
Expand Down Expand Up @@ -98,6 +97,31 @@ func portMapToStrings(portMap nat.PortMap) []string {
return portBindingStrs
}

// toNatPortMap converts a [network.PortMap] to a [nat.PortMap]. This is used to convert the Docker API
// response back to nat types so the public [ContainerInfo] API remains stable.
func toNatPortMap(m network.PortMap) nat.PortMap {
if len(m) == 0 {
return nil
}
out := make(nat.PortMap, len(m))
for p, bindings := range m {
natPort := nat.Port(p.String())
natBindings := make([]nat.PortBinding, len(bindings))
for i, b := range bindings {
hostIP := ""
if b.HostIP.IsValid() {
hostIP = b.HostIP.String()
}
natBindings[i] = nat.PortBinding{
HostIP: hostIP,
HostPort: b.HostPort,
}
}
out[natPort] = natBindings
}
return out
}

// ContainerInfo holds information about a running Docker container
type ContainerInfo struct {
ID string
Expand Down
55 changes: 55 additions & 0 deletions container_info_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package dktest

import (
"net/netip"
"strconv"
"testing"

"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/network"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -150,3 +152,56 @@ func TestPortMapToStrings(t *testing.T) {
})
}
}

func TestToNatPortMap(t *testing.T) {
testCases := []struct {
name string
input network.PortMap
expected nat.PortMap
}{
{name: "nil", input: nil, expected: nil},
{name: "empty", input: network.PortMap{}, expected: nil},
{name: "single port", input: network.PortMap{
network.MustParsePort("80/tcp"): {{HostPort: "8080"}},
}, expected: nat.PortMap{
"80/tcp": {{HostIP: "", HostPort: "8080"}},
}},
{name: "with host ip", input: network.PortMap{
network.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("10.0.0.1"), HostPort: "8080"}},
}, expected: nat.PortMap{
"80/tcp": {{HostIP: "10.0.0.1", HostPort: "8080"}},
}},
{name: "with zero ip", input: network.PortMap{
network.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: "8080"}},
}, expected: nat.PortMap{
"80/tcp": {{HostIP: "0.0.0.0", HostPort: "8080"}},
}},
{name: "multiple ports", input: network.PortMap{
network.MustParsePort("80/tcp"): {{HostPort: "8080"}},
network.MustParsePort("443/tcp"): {{HostPort: "8443"}},
network.MustParsePort("53/udp"): {{HostPort: "5353"}},
}, expected: nat.PortMap{
"80/tcp": {{HostIP: "", HostPort: "8080"}},
"443/tcp": {{HostIP: "", HostPort: "8443"}},
"53/udp": {{HostIP: "", HostPort: "5353"}},
}},
{name: "multiple bindings per port", input: network.PortMap{
network.MustParsePort("80/tcp"): {
{HostPort: "8080"},
{HostIP: netip.MustParseAddr("192.168.1.1"), HostPort: "9090"},
},
}, expected: nat.PortMap{
"80/tcp": {
{HostIP: "", HostPort: "8080"},
{HostIP: "192.168.1.1", HostPort: "9090"},
},
}},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := toNatPortMap(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
7 changes: 1 addition & 6 deletions container_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ package dktest_test

import (
"testing"
)

import (
"github.com/docker/go-connections/nat"
)

import (
"github.com/dhui/dktest"
"github.com/docker/go-connections/nat"
)

func getTestContainerInfo(t *testing.T) dktest.ContainerInfo {
Expand Down
82 changes: 47 additions & 35 deletions dktest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"testing"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/jsonmessage"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

var (
Expand All @@ -31,12 +31,20 @@ const (
label = "dktest"
)

func pullImage(ctx context.Context, lgr Logger, dc client.ImageAPIClient, registryAuth, imgName, platform string) error {
func pullImage(ctx context.Context, lgr Logger, dc client.ImageAPIClient, registryAuth string, imgName string, platform string) error {
lgr.Log("Pulling image:", imgName)
// lgr.Log(dc.ImageList(ctx, types.ImageListOptions{All: true}))

resp, err := dc.ImagePull(ctx, imgName, image.PullOptions{
Platform: platform,
var platforms []v1.Platform
if len(platform) > 0 {
p, err := parsePlatform(platform)
if err != nil {
return err
}
platforms = []v1.Platform{p}
}
resp, err := dc.ImagePull(ctx, imgName, client.ImagePullOptions{
Platforms: platforms,
RegistryAuth: registryAuth,
})
if err != nil {
Expand All @@ -62,38 +70,42 @@ func pullImage(ctx context.Context, lgr Logger, dc client.ImageAPIClient, regist
func removeImage(ctx context.Context, lgr Logger, dc client.ImageAPIClient, imgName string) {
lgr.Log("Removing image:", imgName)

if _, err := dc.ImageRemove(ctx, imgName, image.RemoveOptions{Force: true, PruneChildren: true}); err != nil {
if _, err := dc.ImageRemove(ctx, imgName, client.ImageRemoveOptions{Force: true, PruneChildren: true}); err != nil {
lgr.Log("Failed to remove image: ", err.Error())
}
}

func runImage(ctx context.Context, lgr Logger, dc client.ContainerAPIClient, imgName string,
opts Options) (ContainerInfo, error) {
c := ContainerInfo{Name: genContainerName(), ImageName: imgName}
createResp, err := dc.ContainerCreate(ctx, &container.Config{
Image: imgName,
Labels: map[string]string{label: "true"},
Env: opts.env(),
Entrypoint: opts.Entrypoint,
Cmd: opts.Cmd,
Volumes: opts.volumes(),
Hostname: opts.Hostname,
ExposedPorts: opts.ExposedPorts,
}, &container.HostConfig{
PublishAllPorts: true,
PortBindings: opts.PortBindings,
ShmSize: opts.ShmSize,
Mounts: opts.Mounts,
}, &network.NetworkingConfig{},
nil,
c.Name)
createResp, err := dc.ContainerCreate(ctx,
client.ContainerCreateOptions{
Config: &container.Config{
Image: imgName,
Labels: map[string]string{label: "true"},
Env: opts.env(),
Entrypoint: opts.Entrypoint,
Cmd: opts.Cmd,
Volumes: opts.volumes(),
Hostname: opts.Hostname,
ExposedPorts: convertPortSet(opts.ExposedPorts),
},
HostConfig: &container.HostConfig{
PublishAllPorts: true,
PortBindings: convertPortMap(opts.PortBindings),
ShmSize: opts.ShmSize,
Mounts: opts.Mounts,
},
NetworkingConfig: &network.NetworkingConfig{},
Name: c.Name,
})
if err != nil {
return c, err
}
c.ID = createResp.ID
lgr.Log("Created container:", c.String())

if err := dc.ContainerStart(ctx, createResp.ID, container.StartOptions{}); err != nil {
if _, err := dc.ContainerStart(ctx, createResp.ID, client.ContainerStartOptions{}); err != nil {
return c, err
}
lgr.Log("Started container:", c.String())
Expand All @@ -102,24 +114,24 @@ func runImage(ctx context.Context, lgr Logger, dc client.ContainerAPIClient, img
return c, nil
}

inspectResp, err := dc.ContainerInspect(ctx, c.ID)
inspectResp, err := dc.ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
if err != nil {
return c, err
}
lgr.Log("Inspected container:", c.String())

if inspectResp.NetworkSettings == nil {
if inspectResp.Container.NetworkSettings == nil {
return c, errNoNetworkSettings
}
c.Ports = inspectResp.NetworkSettings.Ports
c.Ports = toNatPortMap(inspectResp.Container.NetworkSettings.Ports)

return c, nil
}

func stopContainer(ctx context.Context, lgr Logger, dc client.ContainerAPIClient, c ContainerInfo,
logStdout, logStderr bool) {
if logStdout || logStderr {
if logs, err := dc.ContainerLogs(ctx, c.ID, container.LogsOptions{
if logs, err := dc.ContainerLogs(ctx, c.ID, client.ContainerLogsOptions{
Timestamps: true, ShowStdout: logStdout, ShowStderr: logStderr,
}); err == nil {
b, err := io.ReadAll(logs)
Expand All @@ -138,13 +150,13 @@ func stopContainer(ctx context.Context, lgr Logger, dc client.ContainerAPIClient
}
}

if err := dc.ContainerStop(ctx, c.ID, container.StopOptions{}); err != nil {
if _, err := dc.ContainerStop(ctx, c.ID, client.ContainerStopOptions{}); err != nil {
lgr.Log("Error stopping container:", c.String(), "error:", err)
}
lgr.Log("Stopped container:", c.String())

if err := dc.ContainerRemove(ctx, c.ID,
container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
if _, err := dc.ContainerRemove(ctx, c.ID,
client.ContainerRemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
lgr.Log("Error removing container:", c.String(), "error:", err)
}
lgr.Log("Removed container:", c.String())
Expand Down Expand Up @@ -191,7 +203,7 @@ func Run(t *testing.T, imgName string, opts Options, testFunc func(*testing.T, C

// RunContext is similar to Run, but takes a parent context and returns an error and doesn't rely on a testing.T.
func RunContext(ctx context.Context, logger Logger, imgName string, opts Options, testFunc func(ContainerInfo) error) (retErr error) {
dc, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.41"))
dc, err := client.New(client.FromEnv)
if err != nil {
return fmt.Errorf("error getting Docker client: %w", err)
}
Expand Down
61 changes: 30 additions & 31 deletions dktest_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package dktest
import (
"context"
"io"
"net/netip"
"testing"
"time"

"github.com/dhui/dktest/mockdockerclient"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
)

const (
Expand Down Expand Up @@ -42,57 +44,54 @@ func TestPullImage(t *testing.T) {
expectErr bool
}{
{name: "success", client: mockdockerclient.ImageAPIClient{
PullResp: mockdockerclient.MockReadCloser{MockReader: successReader}}, expectErr: false},
PullResp: &mockdockerclient.MockImagePullResponse{ReadCloser: mockdockerclient.MockReadCloser{MockReader: successReader}}}, expectErr: false},
{name: "with specific platform", client: mockdockerclient.ImageAPIClient{
PullResp: mockdockerclient.MockReadCloser{MockReader: successReader}},
platform: "linux/x86_64", expectErr: false},
PullResp: &mockdockerclient.MockImagePullResponse{ReadCloser: mockdockerclient.MockReadCloser{MockReader: successReader}}},
platform: "linux/amd64", expectErr: false},
{name: "pull error", client: mockdockerclient.ImageAPIClient{}, expectErr: true},
{name: "read error", client: mockdockerclient.ImageAPIClient{
PullResp: mockdockerclient.MockReadCloser{
PullResp: &mockdockerclient.MockImagePullResponse{ReadCloser: mockdockerclient.MockReadCloser{
MockReader: mockdockerclient.MockReader{Err: mockdockerclient.Err},
}}, expectErr: false},
}}}, expectErr: false},
{name: "close error", client: mockdockerclient.ImageAPIClient{
PullResp: mockdockerclient.MockReadCloser{
PullResp: &mockdockerclient.MockImagePullResponse{ReadCloser: mockdockerclient.MockReadCloser{
MockReader: successReader,
MockCloser: mockdockerclient.MockCloser{Err: mockdockerclient.Err},
}}, expectErr: false},
}}}, expectErr: false},
}

ctx := context.Background()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := tc.client
err := pullImage(ctx, t, &client, "", imageName, tc.platform)
c := tc.client
err := pullImage(ctx, t, &c, "", imageName, tc.platform)
testErr(t, err, tc.expectErr)
})
}
}

func TestRunImage(t *testing.T) {
_, portBindingsNoIP, err := nat.ParsePortSpecs([]string{"8181:80"})
if err != nil {
t.Fatal("Error parsing port bindings:", err)
portBindingsNoIP := network.PortMap{
network.MustParsePort("80/tcp"): []network.PortBinding{{HostPort: "8181"}},
}
_, portBindingsIPZeros, err := nat.ParsePortSpecs([]string{"0.0.0.0:8181:80"})
if err != nil {
t.Fatal("Error parsing port bindings:", err)
portBindingsIPZeros := network.PortMap{
network.MustParsePort("80/tcp"): []network.PortBinding{{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: "8181"}},
}
_, portBindingsDiffIP, err := nat.ParsePortSpecs([]string{"10.0.0.1:8181:80"})
if err != nil {
t.Fatal("Error parsing port bindings:", err)
portBindingsDiffIP := network.PortMap{
network.MustParsePort("80/tcp"): []network.PortBinding{{HostIP: netip.MustParseAddr("10.0.0.1"), HostPort: "8181"}},
}

successCreateResp := &container.CreateResponse{}
successInspectResp := &container.InspectResponse{}
successInspectRespWithPortBindingNoIP := &container.InspectResponse{NetworkSettings: &container.NetworkSettings{
NetworkSettingsBase: container.NetworkSettingsBase{Ports: portBindingsNoIP},
}}
successInspectRespWithPortBindingIPZeros := &container.InspectResponse{NetworkSettings: &container.NetworkSettings{
NetworkSettingsBase: container.NetworkSettingsBase{Ports: portBindingsIPZeros},
}}
successInspectRespWithPortBindingDiffIP := &container.InspectResponse{NetworkSettings: &container.NetworkSettings{
NetworkSettingsBase: container.NetworkSettingsBase{Ports: portBindingsDiffIP},
}}
successCreateResp := &client.ContainerCreateResult{}
successInspectResp := &client.ContainerInspectResult{}
successInspectRespWithPortBindingNoIP := &client.ContainerInspectResult{Container: container.InspectResponse{NetworkSettings: &container.NetworkSettings{
Ports: portBindingsNoIP,
}}}
successInspectRespWithPortBindingIPZeros := &client.ContainerInspectResult{Container: container.InspectResponse{NetworkSettings: &container.NetworkSettings{
Ports: portBindingsIPZeros,
}}}
successInspectRespWithPortBindingDiffIP := &client.ContainerInspectResult{Container: container.InspectResponse{NetworkSettings: &container.NetworkSettings{
Ports: portBindingsDiffIP,
}}}

testCases := []struct {
name string
Expand Down
Loading
Loading