/* 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) } }