airshipctl/pkg/container/container_docker_test.go
Ruslan Aliev d1abe6e1ea Decouple implementations and unit tests in container module
Unit test should follow black-box approach.

Change-Id: I98c46c613a73b539f79d8dfe99cd73592792536d
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Closes: #561
2021-07-16 21:02:03 -05:00

710 lines
20 KiB
Go

/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container_test
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
aircontainer "opendev.org/airship/airshipctl/pkg/container"
)
type mockConn struct {
WData []byte
}
func (mc mockConn) Read(b []byte) (n int, err error) { return len(b), nil }
func (mc mockConn) Write(b []byte) (n int, err error) {
copy(mc.WData, b)
return len(b), nil
}
func (mc mockConn) Close() error { return nil }
func (mc mockConn) LocalAddr() net.Addr { return nil }
func (mc mockConn) RemoteAddr() net.Addr { return nil }
func (mc mockConn) SetDeadline(time.Time) error { return nil }
func (mc mockConn) SetReadDeadline(time.Time) error { return nil }
func (mc mockConn) SetWriteDeadline(time.Time) error { return nil }
type mockDockerClient struct {
imageInspectWithRaw func() (types.ImageInspect, []byte, error)
imageList func() ([]types.ImageSummary, error)
containerAttach func() (types.HijackedResponse, error)
imagePull func() (io.ReadCloser, error)
containerStart func() error
containerWait func() (<-chan container.ContainerWaitOKBody, <-chan error)
containerLogs func() (io.ReadCloser, error)
containerInspect func() (types.ContainerJSON, error)
}
func (mdc *mockDockerClient) ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error) {
return mdc.imageInspectWithRaw()
}
func (mdc *mockDockerClient) ImageList(context.Context, types.ImageListOptions) ([]types.ImageSummary, error) {
return mdc.imageList()
}
func (mdc *mockDockerClient) ImagePull(
context.Context,
string,
types.ImagePullOptions,
) (io.ReadCloser, error) {
return mdc.imagePull()
}
func (mdc *mockDockerClient) ContainerCreate(
context.Context,
*container.Config,
*container.HostConfig,
*network.NetworkingConfig,
*specs.Platform,
string,
) (container.ContainerCreateCreatedBody, error) {
return container.ContainerCreateCreatedBody{ID: "testID"}, nil
}
func (mdc *mockDockerClient) ContainerAttach(
context.Context,
string,
types.ContainerAttachOptions,
) (types.HijackedResponse, error) {
return mdc.containerAttach()
}
func (mdc *mockDockerClient) ContainerStart(context.Context, string, types.ContainerStartOptions) error {
if mdc.containerStart != nil {
return mdc.containerStart()
}
return nil
}
func (mdc *mockDockerClient) ContainerWait(
context.Context,
string,
container.WaitCondition,
) (<-chan container.ContainerWaitOKBody, <-chan error) {
if mdc.containerWait != nil {
return mdc.containerWait()
}
resC := make(chan container.ContainerWaitOKBody)
go func() {
resC <- container.ContainerWaitOKBody{StatusCode: 0}
}()
return resC, nil
}
func (mdc *mockDockerClient) ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error) {
if mdc.containerLogs != nil {
return mdc.containerLogs()
}
return ioutil.NopCloser(strings.NewReader("")), nil
}
func (mdc *mockDockerClient) ContainerRemove(context.Context, string, types.ContainerRemoveOptions) error {
return nil
}
func (mdc *mockDockerClient) ContainerInspect(context.Context, string) (types.ContainerJSON, error) {
if mdc.containerInspect != nil {
return mdc.containerInspect()
}
return types.ContainerJSON{}, nil
}
func getDockerContainerMock(mdc mockDockerClient) *aircontainer.DockerContainer {
ctx := context.Background()
cnt := &aircontainer.DockerContainer{
DockerClient: &mdc,
Ctx: ctx,
}
return cnt
}
func TestGetCmd(t *testing.T) {
testError := fmt.Errorf("inspect error")
tests := []struct {
cmd []string
mockDockerClient mockDockerClient
expectedResult []string
expectedErr error
}{
{
cmd: []string{"testCmd"},
mockDockerClient: mockDockerClient{},
expectedResult: []string{"testCmd"},
expectedErr: nil,
},
{
cmd: []string{},
mockDockerClient: mockDockerClient{
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{{ID: "imgid"}}, nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{
Config: &container.Config{
Cmd: []string{"testCmd"},
},
}, nil, nil
},
},
expectedResult: []string{"testCmd"},
expectedErr: nil,
},
{
cmd: []string{},
mockDockerClient: mockDockerClient{
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{{ID: "imgid"}}, nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, testError
},
},
expectedResult: nil,
expectedErr: testError,
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualRes, actualErr := cnt.GetCmd(tt.cmd)
assert.Equal(t, tt.expectedErr, actualErr)
assert.Equal(t, tt.expectedResult, actualRes)
}
}
func TestGetImageId(t *testing.T) {
testError := fmt.Errorf("img list error")
tests := []struct {
url string
mockDockerClient mockDockerClient
expectedResult string
expectedErr error
}{
{
url: "test:latest",
mockDockerClient: mockDockerClient{
imageList: func() ([]types.ImageSummary, error) {
return nil, testError
},
},
expectedResult: "",
expectedErr: testError,
},
{
url: "test:latest",
mockDockerClient: mockDockerClient{
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{}, nil
},
},
expectedResult: "",
expectedErr: aircontainer.ErrEmptyImageList{},
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualRes, actualErr := cnt.GetImageID(tt.url)
assert.Equal(t, tt.expectedErr, actualErr)
assert.Equal(t, tt.expectedResult, actualRes)
}
}
func TestImagePull(t *testing.T) {
testError := fmt.Errorf("image pull rror")
tests := []struct {
mockDockerClient mockDockerClient
expectedErr error
}{
{
mockDockerClient: mockDockerClient{
imagePull: func() (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("test")), nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, testError
},
},
expectedErr: nil,
},
{
mockDockerClient: mockDockerClient{
imagePull: func() (io.ReadCloser, error) {
return nil, testError
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, testError
},
},
expectedErr: testError,
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualErr := cnt.ImagePull()
assert.Equal(t, tt.expectedErr, actualErr)
}
}
func TestGetId(t *testing.T) {
cnt := getDockerContainerMock(mockDockerClient{})
err := cnt.RunCommand(aircontainer.RunCommandOptions{
Cmd: []string{"testCmd"},
})
require.NoError(t, err)
actualID := cnt.GetID()
assert.Equal(t, "testID", actualID)
}
func TestRunCommand(t *testing.T) {
imageListError := fmt.Errorf("image list error")
attachError := fmt.Errorf("attach error")
containerStartError := fmt.Errorf("container start error")
containerWaitError := fmt.Errorf("container wait error")
tests := []struct {
cmd []string
containerInput io.Reader
volumeMounts []string
mounts []aircontainer.Mount
debug bool
mockDockerClient mockDockerClient
expectedRunErr error
expectedWaitErr error
assertF func(*testing.T)
}{
{
cmd: []string{"testCmd"},
containerInput: nil,
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{},
expectedRunErr: nil,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
cmd: []string{},
containerInput: nil,
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{
imageList: func() ([]types.ImageSummary, error) {
return nil, imageListError
},
},
expectedRunErr: imageListError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
cmd: []string{"testCmd"},
containerInput: strings.NewReader("testInput"),
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{
containerAttach: func() (types.HijackedResponse, error) {
conn := types.HijackedResponse{
Conn: mockConn{WData: make([]byte, len([]byte("testInput")))},
}
return conn, nil
},
},
expectedRunErr: nil,
mounts: []aircontainer.Mount{
{
ReadOnly: true,
Type: "bind",
Dst: "/dev/vda0",
Src: "/dev/vd3",
},
},
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
cmd: []string{"testCmd"},
containerInput: strings.NewReader("testInput"),
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{
containerAttach: func() (types.HijackedResponse, error) {
return types.HijackedResponse{}, attachError
},
},
expectedRunErr: attachError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
cmd: []string{"testCmd"},
containerInput: nil,
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{
containerStart: func() error {
return containerStartError
},
},
expectedRunErr: containerStartError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
// pass empty buffer to make sure we cover error when input isn't nil
containerInput: bytes.NewBuffer([]byte{}),
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{
containerStart: func() error {
return containerStartError
},
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{{ID: "imgid"}}, nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{
Config: &container.Config{},
}, nil, nil
},
containerAttach: func() (types.HijackedResponse, error) {
conn := types.HijackedResponse{
Conn: mockConn{WData: make([]byte, len([]byte("testInput")))},
}
return conn, nil
},
},
expectedRunErr: containerStartError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
cmd: []string{"testCmd"},
containerInput: nil,
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{
containerWait: func() (<-chan container.ContainerWaitOKBody, <-chan error) {
errC := make(chan error)
go func() {
errC <- containerWaitError
}()
return nil, errC
},
},
expectedRunErr: nil,
expectedWaitErr: containerWaitError,
assertF: func(t *testing.T) {},
},
{
cmd: []string{"testCmd"},
containerInput: nil,
volumeMounts: nil,
debug: true,
mockDockerClient: mockDockerClient{
containerWait: func() (<-chan container.ContainerWaitOKBody, <-chan error) {
resC := make(chan container.ContainerWaitOKBody)
go func() {
resC <- container.ContainerWaitOKBody{StatusCode: 1}
}()
return resC, nil
},
},
expectedRunErr: nil,
expectedWaitErr: aircontainer.ErrRunContainerCommand{Cmd: "docker logs testID"},
assertF: func(t *testing.T) {},
},
{
cmd: []string{"testCmd"},
containerInput: nil,
volumeMounts: nil,
debug: true,
mockDockerClient: mockDockerClient{
containerWait: func() (<-chan container.ContainerWaitOKBody, <-chan error) {
resC := make(chan container.ContainerWaitOKBody)
go func() {
resC <- container.ContainerWaitOKBody{StatusCode: 1}
}()
return resC, nil
},
containerLogs: func() (io.ReadCloser, error) {
return nil, fmt.Errorf("logs error")
},
},
expectedRunErr: nil,
expectedWaitErr: aircontainer.ErrRunContainerCommand{Cmd: "docker logs testID"},
assertF: func(t *testing.T) {},
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualErr := cnt.RunCommand(aircontainer.RunCommandOptions{
Input: tt.containerInput,
Cmd: tt.cmd,
Binds: tt.volumeMounts,
Mounts: tt.mounts,
})
assert.Equal(t, tt.expectedRunErr, actualErr)
actualErr = cnt.WaitUntilFinished()
assert.Equal(t, tt.expectedWaitErr, actualErr)
tt.assertF(t)
}
}
func TestRunCommandOutput(t *testing.T) {
testError := fmt.Errorf("img list error")
tests := []struct {
cmd []string
containerInput io.Reader
volumeMounts []string
mockDockerClient mockDockerClient
expectedResult string
expectedErr error
}{
{
cmd: []string{"testCmd"},
containerInput: nil,
volumeMounts: nil,
mockDockerClient: mockDockerClient{},
expectedResult: "",
expectedErr: nil,
},
{
cmd: []string{},
containerInput: nil,
volumeMounts: nil,
mockDockerClient: mockDockerClient{
imageList: func() ([]types.ImageSummary, error) {
return nil, testError
},
},
expectedResult: "",
expectedErr: testError,
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualErr := cnt.RunCommand(aircontainer.RunCommandOptions{
Input: tt.containerInput,
Cmd: tt.cmd,
Binds: tt.volumeMounts,
})
assert.Equal(t, tt.expectedErr, actualErr)
actualRes, actualErr := cnt.GetContainerLogs(aircontainer.GetLogOptions{Stdout: true, Follow: true})
require.NoError(t, actualErr)
var actualResBytes []byte
if actualRes != nil {
var err error
actualResBytes, err = ioutil.ReadAll(actualRes)
require.NoError(t, err)
} else {
actualResBytes = []byte{}
}
assert.Equal(t, tt.expectedResult, string(actualResBytes))
}
}
func TestNewDockerContainer(t *testing.T) {
testError := fmt.Errorf("image pull error")
type resultStruct struct {
tag string
imageURL string
id string
}
tests := []struct {
url string
ctx context.Context
cli mockDockerClient
expectedErr error
expectedResult resultStruct
}{
{
url: "testPrefix/testImage:testTag",
ctx: context.Background(),
cli: mockDockerClient{
imagePull: func() (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("test")), nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, testError
},
},
expectedErr: nil,
expectedResult: resultStruct{
tag: "testTag",
imageURL: "testPrefix/testImage:testTag",
id: "",
},
},
{
url: "testPrefix/testImage:testTag",
ctx: context.Background(),
cli: mockDockerClient{
imagePull: func() (io.ReadCloser, error) {
return nil, testError
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, testError
},
},
expectedErr: testError,
expectedResult: resultStruct{},
},
}
for _, tt := range tests {
actualRes, actualErr := aircontainer.NewDockerContainer(tt.ctx, tt.url, &(tt.cli))
assert.Equal(t, tt.expectedErr, actualErr)
var actualResStruct resultStruct
if actualRes == nil {
actualResStruct = resultStruct{}
} else {
actualResStruct = resultStruct{
tag: actualRes.Tag,
imageURL: actualRes.ImageURL,
id: actualRes.ID,
}
}
assert.Equal(t, tt.expectedResult, actualResStruct)
}
}
func TestRmContainer(t *testing.T) {
tests := []struct {
mockDockerClient mockDockerClient
expectedErr error
}{
{
mockDockerClient: mockDockerClient{},
expectedErr: nil,
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualErr := cnt.RmContainer()
assert.Equal(t, tt.expectedErr, actualErr)
}
}
func TestInspectContainer(t *testing.T) {
tests := []struct {
cli mockDockerClient
expectedState aircontainer.State
expectedErr error
}{
{
// Status: String representation of the container state.
// Testing Status == CreatedContainerStatus and ExitCode == 0
cli: mockDockerClient{
containerInspect: func() (types.ContainerJSON, error) {
json := types.ContainerJSON{}
json.ContainerJSONBase = &types.ContainerJSONBase{}
json.ContainerJSONBase.State = &types.ContainerState{}
json.ContainerJSONBase.State.ExitCode = 0
json.ContainerJSONBase.State.Status = aircontainer.CreatedContainerStatus
return json, nil
},
},
expectedState: aircontainer.State{
ExitCode: 0,
Status: aircontainer.CreatedContainerStatus,
},
expectedErr: nil,
},
{
// Status: String representation of the container state.
// Testing Status == RunningContainerStatus and ExitCode == 0
cli: mockDockerClient{
containerInspect: func() (types.ContainerJSON, error) {
json := types.ContainerJSON{}
json.ContainerJSONBase = &types.ContainerJSONBase{}
json.ContainerJSONBase.State = &types.ContainerState{}
json.ContainerJSONBase.State.ExitCode = 0
json.ContainerJSONBase.State.Status = aircontainer.RunningContainerStatus
return json, nil
},
},
expectedState: aircontainer.State{
ExitCode: 0,
Status: aircontainer.RunningContainerStatus,
},
expectedErr: nil,
},
{
// Status: String representation of the container state.
// Testing Status == ExitedContainerStatus and ExitCode == 0
cli: mockDockerClient{
containerInspect: func() (types.ContainerJSON, error) {
json := types.ContainerJSON{}
json.ContainerJSONBase = &types.ContainerJSONBase{}
json.ContainerJSONBase.State = &types.ContainerState{}
json.ContainerJSONBase.State.ExitCode = 0
json.ContainerJSONBase.State.Status = aircontainer.ExitedContainerStatus
return json, nil
},
},
expectedState: aircontainer.State{
ExitCode: 0,
Status: aircontainer.ExitedContainerStatus,
},
expectedErr: nil,
},
{
// Status: String representation of the container state.
// Testing Status == ExitedContainerStatus and ExitCode == 1
cli: mockDockerClient{
containerInspect: func() (types.ContainerJSON, error) {
json := types.ContainerJSON{}
json.ContainerJSONBase = &types.ContainerJSONBase{}
json.ContainerJSONBase.State = &types.ContainerState{}
json.ContainerJSONBase.State.ExitCode = 1
json.ContainerJSONBase.State.Status = aircontainer.ExitedContainerStatus
return json, nil
},
},
expectedState: aircontainer.State{
ExitCode: 1,
Status: aircontainer.ExitedContainerStatus,
},
expectedErr: nil,
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.cli)
actualState, actualErr := cnt.InspectContainer()
assert.Equal(t, tt.expectedState, actualState)
assert.Equal(t, tt.expectedErr, actualErr)
}
}