Skip to content

Commit c8ca370

Browse files
authored
Add docker package tests (#2812)
1 parent 117a106 commit c8ca370

File tree

7 files changed

+386
-19
lines changed

7 files changed

+386
-19
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/kyma-project/api-gateway v0.0.0-20250814120053-7d617def4106
1818
github.com/moby/go-archive v0.1.0
1919
github.com/moby/term v0.5.2
20+
github.com/opencontainers/image-spec v1.1.1
2021
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
2122
github.com/pkg/errors v0.9.1
2223
github.com/spf13/cobra v1.10.2
@@ -180,7 +181,6 @@ require (
180181
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
181182
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
182183
github.com/opencontainers/go-digest v1.0.0 // indirect
183-
github.com/opencontainers/image-spec v1.1.1 // indirect
184184
github.com/opencontainers/selinux v1.13.0 // indirect
185185
github.com/openshift/api v0.0.0-20250806102053-6a7223edb2fc // indirect
186186
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect

internal/docker/containerfollowrun.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,65 @@ import (
44
"context"
55
"io"
66

7+
"github.com/docker/docker/api/types"
78
"github.com/docker/docker/api/types/container"
89
"github.com/kyma-project/cli.v3/internal/out"
910
)
1011

12+
type followRunUtils struct {
13+
containerAttach func(
14+
ctx context.Context,
15+
containerID string,
16+
opts container.AttachOptions,
17+
) (types.HijackedResponse, error)
18+
19+
stopOnSigInt func(
20+
containerID string,
21+
dstout io.Writer,
22+
dsterr io.Writer,
23+
reader io.Reader,
24+
)
25+
26+
msgWriter func() io.Writer
27+
errWriter func() io.Writer
28+
}
29+
1130
func (c *Client) ContainerFollowRun(containerID string, forwardOutput bool) error {
12-
buf, err := c.ContainerAttach(context.Background(), containerID, container.AttachOptions{
13-
Stdout: true,
14-
Stderr: true,
15-
Stream: true,
31+
return c.containerFollowRun(containerID, forwardOutput, followRunUtils{
32+
containerAttach: c.ContainerAttach,
33+
stopOnSigInt: c.StopContainerOnSigInt,
34+
msgWriter: out.Default.MsgWriter,
35+
errWriter: out.Default.ErrWriter,
1636
})
37+
}
38+
39+
func (c *Client) containerFollowRun(
40+
containerID string,
41+
forwardOutput bool,
42+
utils followRunUtils,
43+
) error {
44+
buf, err := utils.containerAttach(
45+
context.Background(),
46+
containerID,
47+
container.AttachOptions{
48+
Stdout: true,
49+
Stderr: true,
50+
Stream: true,
51+
},
52+
)
1753
if err != nil {
1854
return err
1955
}
56+
2057
defer buf.Close()
2158

2259
dstout, dsterr := io.Discard, io.Discard
2360
if forwardOutput {
24-
dstout = out.Default.MsgWriter()
25-
dsterr = out.Default.ErrWriter()
61+
dstout = utils.msgWriter()
62+
dsterr = utils.errWriter()
2663
}
2764

28-
c.stopContainerOnSigInt(
65+
utils.stopOnSigInt(
2966
containerID,
3067
dstout,
3168
dsterr,
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package docker
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"errors"
8+
"io"
9+
"net"
10+
"testing"
11+
12+
"github.com/docker/docker/api/types"
13+
"github.com/docker/docker/api/types/container"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func Test_containerFollowRun(t *testing.T) {
18+
t.Run("forwards output when enabled", func(t *testing.T) {
19+
outBuf := &bytes.Buffer{}
20+
errBuf := &bytes.Buffer{}
21+
22+
utils := followRunUtils{
23+
containerAttach: func(
24+
ctx context.Context,
25+
containerID string,
26+
opts container.AttachOptions,
27+
) (types.HijackedResponse, error) {
28+
require.Equal(t, context.Background(), ctx)
29+
require.Equal(t, "container-id", containerID)
30+
require.True(t, opts.Stdout)
31+
require.True(t, opts.Stderr)
32+
require.True(t, opts.Stream)
33+
return types.HijackedResponse{
34+
Reader: bufio.NewReader(bytes.NewBufferString("test-output")),
35+
Conn: fixTestConn(),
36+
}, nil
37+
},
38+
stopOnSigInt: func(containerID string, dstout, dsterr io.Writer, reader io.Reader) {
39+
_, _ = io.Copy(dstout, reader)
40+
_, _ = dsterr.Write([]byte("test-output"))
41+
},
42+
msgWriter: func() io.Writer { return outBuf },
43+
errWriter: func() io.Writer { return errBuf },
44+
}
45+
46+
c := &Client{}
47+
err := c.containerFollowRun("container-id", true, utils)
48+
49+
require.NoError(t, err)
50+
require.Equal(t, "test-output", outBuf.String())
51+
require.Equal(t, "test-output", errBuf.String())
52+
})
53+
54+
t.Run("returns error when container attach fails", func(t *testing.T) {
55+
outBuf := &bytes.Buffer{}
56+
errBuf := &bytes.Buffer{}
57+
58+
utils := followRunUtils{
59+
containerAttach: func(
60+
ctx context.Context,
61+
containerID string,
62+
opts container.AttachOptions,
63+
) (types.HijackedResponse, error) {
64+
require.Equal(t, context.Background(), ctx)
65+
require.Equal(t, "container-id", containerID)
66+
require.True(t, opts.Stdout)
67+
require.True(t, opts.Stderr)
68+
require.True(t, opts.Stream)
69+
return types.HijackedResponse{}, io.ErrUnexpectedEOF
70+
},
71+
msgWriter: func() io.Writer { return outBuf },
72+
errWriter: func() io.Writer { return errBuf },
73+
}
74+
75+
c := &Client{}
76+
err := c.containerFollowRun("container-id", true, utils)
77+
78+
require.ErrorIs(t, err, io.ErrUnexpectedEOF)
79+
require.Empty(t, outBuf.String())
80+
require.Empty(t, errBuf.String())
81+
})
82+
83+
t.Run("fails when one of the parameters is missing", func(t *testing.T) {
84+
utils := followRunUtils{
85+
containerAttach: func(
86+
ctx context.Context,
87+
containerID string,
88+
opts container.AttachOptions,
89+
) (types.HijackedResponse, error) {
90+
91+
if containerID == "" ||
92+
ctx == nil ||
93+
!opts.Stdout ||
94+
!opts.Stderr ||
95+
!opts.Stream {
96+
return types.HijackedResponse{}, errors.New("missing parameter")
97+
}
98+
99+
t.Fatal("containerAttach should not be called with missing parameters")
100+
return types.HijackedResponse{}, nil
101+
},
102+
msgWriter: func() io.Writer { return io.Discard },
103+
errWriter: func() io.Writer { return io.Discard },
104+
}
105+
106+
c := &Client{}
107+
err := c.containerFollowRun("", true, utils)
108+
109+
require.Error(t, err)
110+
require.Contains(t, err.Error(), "missing parameter")
111+
})
112+
113+
}
114+
115+
func fixTestConn() net.Conn {
116+
srv, cl := net.Pipe()
117+
defer srv.Close()
118+
return cl
119+
}

internal/docker/pull.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import (
88
"github.com/docker/docker/api/types/container"
99
"github.com/docker/docker/api/types/image"
1010
"github.com/docker/docker/api/types/mount"
11+
"github.com/docker/docker/api/types/network"
1112
"github.com/docker/docker/pkg/jsonmessage"
1213
"github.com/docker/go-connections/nat"
1314
"github.com/kyma-project/cli.v3/internal/out"
15+
"github.com/opencontainers/image-spec/specs-go/v1"
1416
)
1517

16-
// ErrorMessage is used to parse error messages coming from Docker
1718
type ErrorMessage struct {
1819
Error string
1920
}
@@ -27,8 +28,28 @@ type ContainerRunOpts struct {
2728
Ports map[string]string
2829
}
2930

30-
// PullImageAndStartContainer creates, pulls and starts a container
31+
// Utils struct allows injecting dependencies for testing
32+
type utils struct {
33+
imagePull func(ctx context.Context, imageName string, opts image.PullOptions) (io.ReadCloser, error)
34+
containerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.CreateResponse, error)
35+
containerStart func(ctx context.Context, containerID string, opts container.StartOptions) error
36+
displayJSON func(reader io.Reader, outStream *streams.Out) error
37+
}
38+
39+
// PullImageAndStartContainer is the public function used in production
3140
func (c *Client) PullImageAndStartContainer(ctx context.Context, opts ContainerRunOpts) (string, error) {
41+
return pullImageAndStartContainer(ctx, opts, utils{
42+
imagePull: c.ImagePull,
43+
containerCreate: c.ContainerCreate,
44+
containerStart: c.ContainerStart,
45+
displayJSON: func(r io.Reader, outStream *streams.Out) error {
46+
return jsonmessage.DisplayJSONMessagesToStream(r, outStream, nil)
47+
},
48+
})
49+
}
50+
51+
// PullImageAndStartContainer is the private function that can be tested with mocks
52+
func pullImageAndStartContainer(ctx context.Context, opts ContainerRunOpts, u utils) (string, error) {
3253
config := &container.Config{
3354
Env: opts.Envs,
3455
ExposedPorts: portSet(opts.Ports),
@@ -41,8 +62,7 @@ func (c *Client) PullImageAndStartContainer(ctx context.Context, opts ContainerR
4162
NetworkMode: container.NetworkMode(opts.NetworkMode),
4263
}
4364

44-
var r io.ReadCloser
45-
r, err := c.ImagePull(ctx, config.Image, image.PullOptions{})
65+
r, err := u.imagePull(ctx, config.Image, image.PullOptions{})
4666
if err != nil {
4767
return "", err
4868
}
@@ -53,13 +73,12 @@ func (c *Client) PullImageAndStartContainer(ctx context.Context, opts ContainerR
5373
return "", err
5474
}
5575

56-
body, err := c.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.ContainerName)
76+
body, err := u.containerCreate(ctx, config, hostConfig, nil, nil, opts.ContainerName)
5777
if err != nil {
5878
return "", err
5979
}
6080

61-
err = c.ContainerStart(ctx, body.ID, container.StartOptions{})
62-
if err != nil {
81+
if err := u.containerStart(ctx, body.ID, container.StartOptions{}); err != nil {
6382
return "", err
6483
}
6584

internal/docker/pull_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package docker
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"io"
8+
"testing"
9+
10+
"github.com/docker/cli/cli/streams"
11+
"github.com/docker/docker/api/types/container"
12+
"github.com/docker/docker/api/types/image"
13+
"github.com/docker/docker/api/types/network"
14+
"github.com/docker/docker/pkg/jsonmessage"
15+
"github.com/opencontainers/image-spec/specs-go/v1"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestPullImageAndStartContainer(t *testing.T) {
20+
t.Run("successful pulling and container start", func(t *testing.T) {
21+
utils := utils{
22+
imagePull: func(ctx context.Context, imageName string, opts image.PullOptions) (io.ReadCloser, error) {
23+
return io.NopCloser(bytes.NewReader([]byte(""))), nil
24+
},
25+
containerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.CreateResponse, error) {
26+
return container.CreateResponse{ID: "test-container"}, nil
27+
},
28+
containerStart: func(ctx context.Context, containerID string, opts container.StartOptions) error {
29+
return nil
30+
},
31+
displayJSON: func(r io.Reader, outStream *streams.Out) error {
32+
return jsonmessage.DisplayJSONMessagesToStream(r, outStream, nil)
33+
},
34+
}
35+
36+
opts := ContainerRunOpts{
37+
ContainerName: "test",
38+
Image: "test-image",
39+
}
40+
41+
id, err := pullImageAndStartContainer(context.Background(), opts, utils)
42+
require.NoError(t, err)
43+
require.Equal(t, "test-container", id)
44+
})
45+
46+
t.Run("error while pulling image", func(t *testing.T) {
47+
utils := utils{
48+
imagePull: func(ctx context.Context, imageName string, opts image.PullOptions) (io.ReadCloser, error) {
49+
return nil, errors.New("pull error")
50+
},
51+
containerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.CreateResponse, error) {
52+
return container.CreateResponse{}, nil
53+
},
54+
containerStart: func(ctx context.Context, containerID string, opts container.StartOptions) error { return nil },
55+
displayJSON: func(r io.Reader, outStream *streams.Out) error { return nil },
56+
}
57+
58+
opts := ContainerRunOpts{ContainerName: "test", Image: "test-image"}
59+
60+
_, err := pullImageAndStartContainer(context.Background(), opts, utils)
61+
require.ErrorContains(t, err, "pull error")
62+
})
63+
64+
t.Run("error while creating container", func(t *testing.T) {
65+
utils := utils{
66+
imagePull: func(ctx context.Context, imageName string, opts image.PullOptions) (io.ReadCloser, error) {
67+
return io.NopCloser(bytes.NewReader([]byte(""))), nil
68+
},
69+
containerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.CreateResponse, error) {
70+
return container.CreateResponse{}, errors.New("create error")
71+
},
72+
containerStart: func(ctx context.Context, containerID string, opts container.StartOptions) error { return nil },
73+
displayJSON: func(r io.Reader, outStream *streams.Out) error { return nil },
74+
}
75+
76+
opts := ContainerRunOpts{ContainerName: "test", Image: "test-image"}
77+
78+
_, err := pullImageAndStartContainer(context.Background(), opts, utils)
79+
require.ErrorContains(t, err, "create error")
80+
})
81+
82+
t.Run("error while starting container", func(t *testing.T) {
83+
utils := utils{
84+
imagePull: func(ctx context.Context, imageName string, opts image.PullOptions) (io.ReadCloser, error) {
85+
return io.NopCloser(bytes.NewReader([]byte(""))), nil
86+
},
87+
containerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.CreateResponse, error) {
88+
return container.CreateResponse{ID: "test-container"}, nil
89+
},
90+
containerStart: func(ctx context.Context, containerID string, opts container.StartOptions) error {
91+
return errors.New("start error")
92+
},
93+
displayJSON: func(r io.Reader, outStream *streams.Out) error { return nil },
94+
}
95+
96+
opts := ContainerRunOpts{ContainerName: "test", Image: "test-image"}
97+
98+
_, err := pullImageAndStartContainer(context.Background(), opts, utils)
99+
require.ErrorContains(t, err, "start error")
100+
})
101+
}

0 commit comments

Comments
 (0)