From b37b6f37038ed0f2be55e770df800587c6dfb680 Mon Sep 17 00:00:00 2001 From: Dmitry Ukov Date: Mon, 12 Aug 2019 08:43:06 +0000 Subject: [PATCH] [AIR-137] Add logic to isogen subcommand * Add add default values for isogen subcommand keys * Introduce container interface * Implement docker driver * Add stdin support to container interface * Implement volume mount for container Change-Id: Ide0ecd474b1ccce358bdc9c85ef0006f230490b5 --- go.mod | 2 +- pkg/bootstrap/isogen/command.go | 50 ++- pkg/bootstrap/isogen/command_test.go | 98 +++++ pkg/bootstrap/isogen/config_test.go | 28 ++ pkg/container/container.go | 37 ++ pkg/container/container_docker.go | 300 +++++++++++++++ pkg/container/container_docker_test.go | 501 +++++++++++++++++++++++++ pkg/container/container_test.go | 21 ++ pkg/container/errors.go | 32 ++ pkg/errors/common.go | 9 + 10 files changed, 1069 insertions(+), 9 deletions(-) create mode 100644 pkg/bootstrap/isogen/command_test.go create mode 100644 pkg/bootstrap/isogen/config_test.go create mode 100644 pkg/container/container.go create mode 100644 pkg/container/container_docker.go create mode 100644 pkg/container/container_docker_test.go create mode 100644 pkg/container/container_test.go create mode 100644 pkg/container/errors.go create mode 100644 pkg/errors/common.go diff --git a/go.mod b/go.mod index 72ec9ea97..bd2390700 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/daviddengcn/go-colortext v0.0.0-20180409174941-186a3d44e920 // indirect github.com/docker/distribution v0.0.0-20170726174610-edc3ab29cdff // indirect - github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0 // indirect + github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0 github.com/docker/go-connections v0.3.0 // indirect github.com/docker/go-units v0.3.3 // indirect github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789 // indirect diff --git a/pkg/bootstrap/isogen/command.go b/pkg/bootstrap/isogen/command.go index 7cf1c4547..323c2787b 100644 --- a/pkg/bootstrap/isogen/command.go +++ b/pkg/bootstrap/isogen/command.go @@ -1,28 +1,62 @@ package isogen import ( - "errors" + "context" "fmt" "io" + "opendev.org/airship/airshipctl/pkg/container" + "opendev.org/airship/airshipctl/pkg/errors" "opendev.org/airship/airshipctl/pkg/util" ) -// ErrNotImplemented returned for not implemented features -var ErrNotImplemented = errors.New("Error. Not implemented") - // GenerateBootstrapIso will generate data for cloud init and start ISO builder container func GenerateBootstrapIso(settings *Settings, args []string, out io.Writer) error { if settings.IsogenConfigFile == "" { fmt.Fprintln(out, "Reading config file location from global settings is not supported") - return ErrNotImplemented + return errors.ErrNotImplemented{} } - cfg := Config{} + ctx := context.Background() + cfg := &Config{} if err := util.ReadYAMLFile(settings.IsogenConfigFile, &cfg); err != nil { return err } - fmt.Println("Under construction") - return nil + + fmt.Fprintln(out, "Creating ISO builder container") + builder, err := container.NewContainer( + &ctx, cfg.Container.ContainerRuntime, + cfg.Container.Image) + if err != nil { + return err + } + + return generateBootstrapIso(builder, cfg, out, settings.Debug) +} + +func generateBootstrapIso(builder container.Container, cfg *Config, out io.Writer, debug bool) error { + vols := []string{cfg.Container.Volume} + fmt.Fprintf(out, "Running default container command. Mounted dir: %s\n", vols) + if err := builder.RunCommand( + []string{}, + nil, + vols, + []string{}, + debug, + ); err != nil { + return err + } + + fmt.Fprintln(out, "ISO successfully built.") + if debug { + fmt.Fprintf( + out, + "Debug flag is set. Container %s stopped but not deleted.\n", + builder.GetId(), + ) + return nil + } + fmt.Fprintln(out, "Removing container.") + return builder.RmContainer() } diff --git a/pkg/bootstrap/isogen/command_test.go b/pkg/bootstrap/isogen/command_test.go new file mode 100644 index 000000000..df763358e --- /dev/null +++ b/pkg/bootstrap/isogen/command_test.go @@ -0,0 +1,98 @@ +package isogen + +import ( + "bytes" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +type mockContainer struct { + imagePull func() error + runCommand func() error + runCommandOutput func() (io.ReadCloser, error) + rmContainer func() error + getId func() string +} + +func (mc *mockContainer) ImagePull() error { + return mc.imagePull() +} + +func (mc *mockContainer) RunCommand([]string, io.Reader, []string, []string, bool) error { + return mc.runCommand() +} + +func (mc *mockContainer) RunCommandOutput([]string, io.Reader, []string, []string) (io.ReadCloser, error) { + return mc.runCommandOutput() +} + +func (mc *mockContainer) RmContainer() error { + return mc.rmContainer() +} + +func (mc *mockContainer) GetId() string { + return mc.getId() +} + +func TestBootstrapIso(t *testing.T) { + testErr := fmt.Errorf("TestErr") + expOut := []string{ + "Running default container command. Mounted dir: []\n", + "ISO successfully built.\n", + "Debug flag is set. Container TESTID stopped but not deleted.\n", + "Removing container.\n", + } + + tests := []struct { + builder *mockContainer + cfg *Config + debug bool + expectedOut string + expectdErr error + }{ + { + builder: &mockContainer{ + runCommand: func() error { return testErr }, + }, + cfg: &Config{}, + debug: false, + expectedOut: expOut[0], + expectdErr: testErr, + }, + { + builder: &mockContainer{ + runCommand: func() error { return nil }, + getId: func() string { return "TESTID" }, + }, + cfg: &Config{}, + debug: true, + expectedOut: expOut[0] + expOut[1] + expOut[2], + expectdErr: nil, + }, + { + builder: &mockContainer{ + runCommand: func() error { return nil }, + getId: func() string { return "TESTID" }, + rmContainer: func() error { return testErr }, + }, + cfg: &Config{}, + debug: false, + expectedOut: expOut[0] + expOut[1] + expOut[3], + expectdErr: testErr, + }, + } + + for _, tt := range tests { + actualOut := bytes.NewBufferString("") + actualErr := generateBootstrapIso(tt.builder, tt.cfg, actualOut, tt.debug) + + errS := fmt.Sprintf("generateBootstrapIso should have return error %s, got %s", tt.expectdErr, actualErr) + assert.Equal(t, actualErr, tt.expectdErr, errS) + + errS = fmt.Sprintf("generateBootstrapIso should have print %s, got %s", tt.expectedOut, actualOut) + assert.Equal(t, actualOut.String(), tt.expectedOut, errS) + } +} diff --git a/pkg/bootstrap/isogen/config_test.go b/pkg/bootstrap/isogen/config_test.go new file mode 100644 index 000000000..7f652a34e --- /dev/null +++ b/pkg/bootstrap/isogen/config_test.go @@ -0,0 +1,28 @@ +package isogen + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToYaml(t *testing.T) { + expectedBytes := []byte(`builder: {} +container: + containerRuntime: docker +`) + cnf := &Config{ + Container: Container{ + ContainerRuntime: "docker", + }, + } + + actualBytes, _ := cnf.ToYAML() + errS := fmt.Sprintf( + "Call ToYAML should have returned %s, got %s", + expectedBytes, + actualBytes, + ) + assert.Equal(t, actualBytes, expectedBytes, errS) +} diff --git a/pkg/container/container.go b/pkg/container/container.go new file mode 100644 index 000000000..8e6c91d4e --- /dev/null +++ b/pkg/container/container.go @@ -0,0 +1,37 @@ +package container + +import ( + "context" + "io" +) + +// Container interface abstracion for continer. +// Particular implementation depends on container runtime environment (CRE). Interface +// defines methods that must be implemented for CRE (e.g. docker, containerd or CRI-O) +type Container interface { + ImagePull() error + RunCommand([]string, io.Reader, []string, []string, bool) error + RunCommandOutput([]string, io.Reader, []string, []string) (io.ReadCloser, error) + RmContainer() error + GetId() string +} + +// NewContainer returns instance of Container interface implemented by particular driver +// Returned instance type (i.e. implementation) depends on driver sceified via function +// arguments (e.g. "docker"). +// Supported drivers: +// * docker +func NewContainer(ctx *context.Context, driver string, url string) (Container, error) { + switch driver { + case "docker": + cli, err := NewDockerClient(ctx) + if err != nil { + return nil, err + } + return NewDockerContainer(ctx, url, cli) + default: + + return nil, ErrContainerDrvNotSupported{Driver: driver} + } + +} diff --git a/pkg/container/container_docker.go b/pkg/container/container_docker.go new file mode 100644 index 000000000..01cf7ff97 --- /dev/null +++ b/pkg/container/container_docker.go @@ -0,0 +1,300 @@ +package container + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" +) + +// DockerClient interface that represents abstract Docker client object +// Interface used as a wrapper for upstream docker client object +type DockerClient interface { + // ImageInspectWithRaw returns the image information and its raw + // representation. + ImageInspectWithRaw( + context.Context, + string, + ) (types.ImageInspect, []byte, error) + // ImageList returns a list of images in the docker host. + ImageList( + context.Context, + types.ImageListOptions, + ) ([]types.ImageSummary, error) + // ImagePull requests the docker host to pull an image from a remote registry. + ImagePull( + context.Context, + string, + types.ImagePullOptions, + ) (io.ReadCloser, error) + // ContainerCreate creates a new container based in the given configuration. + ContainerCreate( + context.Context, + *container.Config, + *container.HostConfig, + *network.NetworkingConfig, + string, + ) (container.ContainerCreateCreatedBody, error) + // ContainerAttach attaches a connection to a container in the server. + ContainerAttach( + context.Context, + string, + types.ContainerAttachOptions, + ) (types.HijackedResponse, error) + //ContainerStart sends a request to the docker daemon to start a container. + ContainerStart(context.Context, string, types.ContainerStartOptions) error + // ContainerWait waits until the specified container is in a certain state + // indicated by the given condition, either "not-running" (default), + // "next-exit", or "removed". + ContainerWait( + context.Context, + string, + container.WaitCondition, + ) (<-chan container.ContainerWaitOKBody, <-chan error) + // ContainerLogs returns the logs generated by a continer in an + // io.ReadCloser. + ContainerLogs( + context.Context, + string, + types.ContainerLogsOptions, + ) (io.ReadCloser, error) + // ContainerRemove kills and removes a container from the docker host. + ContainerRemove( + context.Context, + string, + types.ContainerRemoveOptions, + ) error +} + +// DockerContainer docker container object wrapper +type DockerContainer struct { + tag string + imageUrl string + id string + dockerClient DockerClient + ctx *context.Context +} + +// NewDockerClient returns instance of DockerClient. +// Function essentially returns new Docker API client with default values +func NewDockerClient(ctx *context.Context) (DockerClient, error) { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return nil, err + } + cli.NegotiateAPIVersion(*ctx) + return cli, nil +} + +// NewDockerContainer returns instance of DockerContainer object wrapper. +// Function gets container image url, pointer to execution context and +// DockerClient instance. +// +// url format: :. If tag is not specified "latest" is used +// as default value +func NewDockerContainer(ctx *context.Context, url string, cli DockerClient) (*DockerContainer, error) { + t := "latest" + nameTag := strings.Split(url, ":") + if len(nameTag) == 2 { + t = nameTag[1] + } + + cnt := &DockerContainer{ + tag: t, + imageUrl: url, + id: "", + dockerClient: cli, + ctx: ctx, + } + if err := cnt.ImagePull(); err != nil { + return nil, err + } + return cnt, nil +} + +// getCmd identifies container command. Accepts list of strings each element +// represents command part (e.g "sample cmd --key" should be transformed to +// []string{"sample", "command", "--key"}) +// +// If input parameter is NOT empty list method returns input parameter +// immediately +// +// If input parameter is empty list method identifies container image and +// tries to extract Cmd option from this image description (i.e. tries to +// identify default command specified in Dockerfile) +func (c *DockerContainer) getCmd(cmd []string) ([]string, error) { + if len(cmd) > 0 { + return cmd, nil + } + + id, err := c.getImageId(c.imageUrl) + if err != nil { + return nil, err + } + + insp, _, err := c.dockerClient.ImageInspectWithRaw(*c.ctx, id) + if err != nil { + return nil, err + } + config := *insp.Config + return config.Cmd, nil +} + +// getConfig creates configuration structures for Docker API client. +func (c *DockerContainer) getConfig( + cmd []string, + volumeMounts []string, + envVars []string, +) (container.Config, container.HostConfig) { + cCfg := container.Config{ + Image: c.imageUrl, + Cmd: cmd, + AttachStdin: true, + OpenStdin: true, + Env: envVars, + } + hCfg := container.HostConfig{ + Binds: volumeMounts, + } + return cCfg, hCfg +} + +// getImageId return ID of continer image specified by URL. Method executes +// ImageList function supplied with "reference" filter +func (c *DockerContainer) getImageId(url string) (string, error) { + kv := filters.KeyValuePair{ + Key: "reference", + Value: url, + } + filter := filters.NewArgs(kv) + opts := types.ImageListOptions{ + All: false, + Filters: filter, + } + img, err := c.dockerClient.ImageList(*c.ctx, opts) + if err != nil { + return "", err + } + + if len(img) == 0 { + return "", ErrEmptyImageList{} + } + + return img[0].ID, nil +} + +func (c *DockerContainer) GetId() string { + return c.id +} + +// ImagePull downloads image for container +func (c *DockerContainer) ImagePull() error { + // TODO (D. Ukov) add logic for searching among local images + // to avoid image download on each execution + resp, err := c.dockerClient.ImagePull(*c.ctx, c.imageUrl, types.ImagePullOptions{}) + if err != nil { + return err + } + // Wait for image is downloaded + if _, err := ioutil.ReadAll(resp); err != nil { + return err + } + + return nil +} + +// RunCommand executes specified command in Docker container. Method handles +// container STDIN and volume binds +func (c *DockerContainer) RunCommand( + cmd []string, + containerInput io.Reader, + volumeMounts []string, + envVars []string, + // TODO (D. Ukov) add debug logic + debug bool, +) error { + realCmd, err := c.getCmd(cmd) + if err != nil { + return err + } + + containerConfig, hostConfig := c.getConfig(realCmd, volumeMounts, envVars) + resp, err := c.dockerClient.ContainerCreate( + *c.ctx, + &containerConfig, + &hostConfig, + nil, + "", + ) + if err != nil { + return err + } + + c.id = resp.ID + + if containerInput != nil { + conn, attachErr := c.dockerClient.ContainerAttach(*c.ctx, c.id, types.ContainerAttachOptions{ + Stream: true, + Stdin: true, + }) + if attachErr != nil { + return attachErr + } + if _, err = io.Copy(conn.Conn, containerInput); err != nil { + return err + } + } + + if err = c.dockerClient.ContainerStart(*c.ctx, c.id, types.ContainerStartOptions{}); err != nil { + return err + } + + statusCh, errCh := c.dockerClient.ContainerWait(*c.ctx, c.id, container.WaitConditionNotRunning) + select { + case err = <-errCh: + if err != nil { + return err + } + case retCode := <-statusCh: + if retCode.StatusCode != 0 { + logsCmd := fmt.Sprintf("docker logs %s", c.id) + return ErrRunContainerCommand{Cmd: logsCmd} + } + } + + return nil +} + +// RunCommandOutput executes specified command in Docker container and +// returns command output as ReadCloser object. RunCommand debug option is +// set to false explicitly +func (c *DockerContainer) RunCommandOutput( + cmd []string, + continerInput io.Reader, + volumeMounts []string, + envVars []string, +) (io.ReadCloser, error) { + if err := c.RunCommand(cmd, continerInput, volumeMounts, envVars, false); err != nil { + return nil, err + } + + return c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true}) +} + +// RmContainer kills and removes a container from the docker host. +func (c *DockerContainer) RmContainer() error { + return c.dockerClient.ContainerRemove( + *c.ctx, + c.id, + types.ContainerRemoveOptions{ + Force: true, + }, + ) +} diff --git a/pkg/container/container_docker_test.go b/pkg/container/container_docker_test.go new file mode 100644 index 000000000..785f5268a --- /dev/null +++ b/pkg/container/container_docker_test.go @@ -0,0 +1,501 @@ +package container + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" +) + +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(t time.Time) error { return nil } +func (mc mockConn) SetReadDeadline(t time.Time) error { return nil } +func (mc mockConn) SetWriteDeadline(t 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) +} + +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, + 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 getDockerContainerMock(mdc mockDockerClient) *DockerContainer { + ctx := context.Background() + cnt := &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: 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 Error") + tests := []struct { + mockDockerClient mockDockerClient + expectedErr error + }{ + { + mockDockerClient: mockDockerClient{ + imagePull: func() (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("test")), nil + }, + }, + expectedErr: nil, + }, + { + mockDockerClient: mockDockerClient{ + imagePull: func() (io.ReadCloser, error) { + return 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{}) + cnt.RunCommand([]string{"testCmd"}, nil, nil, []string{}, false) + actialId := cnt.GetId() + + assert.Equal(t, "testID", actialId) +} + +func TestRunCommand(t *testing.T) { + imageListeError := 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 + debug bool + mockDockerClient mockDockerClient + expectedErr error + assertF func(*testing.T) + }{ + { + cmd: []string{"testCmd"}, + containerInput: nil, + volumeMounts: nil, + debug: false, + mockDockerClient: mockDockerClient{}, + expectedErr: nil, + assertF: func(t *testing.T) {}, + }, + { + cmd: []string{}, + containerInput: nil, + volumeMounts: nil, + debug: false, + mockDockerClient: mockDockerClient{ + imageList: func() ([]types.ImageSummary, error) { + return nil, imageListeError + }, + }, + expectedErr: imageListeError, + 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 + }, + }, + expectedErr: 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 + }, + }, + expectedErr: attachError, + assertF: func(t *testing.T) {}, + }, + { + cmd: []string{"testCmd"}, + containerInput: nil, + volumeMounts: nil, + debug: false, + mockDockerClient: mockDockerClient{ + containerStart: func() error { + return containerStartError + }, + }, + expectedErr: containerStartError, + 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 + }, + }, + expectedErr: containerWaitError, + assertF: func(t *testing.T) {}, + }, + { + cmd: []string{"testCmd"}, + containerInput: nil, + volumeMounts: nil, + debug: false, + mockDockerClient: mockDockerClient{ + containerWait: func() (<-chan container.ContainerWaitOKBody, <-chan error) { + + resC := make(chan container.ContainerWaitOKBody) + go func() { + resC <- container.ContainerWaitOKBody{StatusCode: 1} + }() + return resC, nil + }, + }, + expectedErr: ErrRunContainerCommand{Cmd: "docker logs testID"}, + assertF: func(t *testing.T) {}, + }, + } + for _, tt := range tests { + cnt := getDockerContainerMock(tt.mockDockerClient) + actualErr := cnt.RunCommand(tt.cmd, tt.containerInput, tt.volumeMounts, []string{}, tt.debug) + + assert.Equal(t, tt.expectedErr, 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) + actualRes, actualErr := cnt.RunCommandOutput(tt.cmd, tt.containerInput, tt.volumeMounts, []string{}) + + assert.Equal(t, tt.expectedErr, actualErr) + + var actualResBytes []byte + if actualRes != nil { + actualResBytes, _ = ioutil.ReadAll(actualRes) + } 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 + }, + }, + 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 + }, + }, + expectedErr: testError, + expectedResult: resultStruct{}, + }, + } + for _, tt := range tests { + actualRes, actualErr := NewDockerContainer(&(tt.ctx), tt.url, &(tt.cli)) + + assert.Equal(t, tt.expectedErr, actualErr) + + var actualResStuct resultStruct + if actualRes == nil { + actualResStuct = resultStruct{} + } else { + actualResStuct = resultStruct{ + tag: actualRes.tag, + imageUrl: actualRes.imageUrl, + id: actualRes.id, + } + } + assert.Equal(t, tt.expectedResult, actualResStuct) + } +} + +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) + } +} diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go new file mode 100644 index 000000000..250d10a11 --- /dev/null +++ b/pkg/container/container_test.go @@ -0,0 +1,21 @@ +package container + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewContainer(t *testing.T) { + ctx := context.Background() + _, actualErr := NewContainer(&ctx, "test_drv", "") + expectedErr := ErrContainerDrvNotSupported{Driver: "test_drv"} + errS := fmt.Sprintf( + "Call NewContainer should have returned error %s, got %s", + expectedErr, + actualErr, + ) + assert.Equal(t, actualErr, expectedErr, errS) +} diff --git a/pkg/container/errors.go b/pkg/container/errors.go new file mode 100644 index 000000000..a7b74e268 --- /dev/null +++ b/pkg/container/errors.go @@ -0,0 +1,32 @@ +package container + +import ( + "fmt" +) + +// ErrEmptyImageList returned if no image defined in filter found +type ErrEmptyImageList struct { +} + +func (e ErrEmptyImageList) Error() string { + return "Image List Error. No images filetered" +} + +// ErrRunContainerCommand returned if container command +// exited with non-zero code +type ErrRunContainerCommand struct { + Cmd string +} + +func (e ErrRunContainerCommand) Error() string { + return fmt.Sprintf("Command error. run '%s' for details", e.Cmd) +} + +// ErrContainerDrvNotSupported returned if desired CRI is not supported +type ErrContainerDrvNotSupported struct { + Driver string +} + +func (e ErrContainerDrvNotSupported) Error() string { + return fmt.Sprintf("Driver %s is not supported", e.Driver) +} diff --git a/pkg/errors/common.go b/pkg/errors/common.go new file mode 100644 index 000000000..085be7b1d --- /dev/null +++ b/pkg/errors/common.go @@ -0,0 +1,9 @@ +package errors + +// ErrNotImplemented returned for not implemented features +type ErrNotImplemented struct { +} + +func (e ErrNotImplemented) Error() string { + return "Error. Not implemented" +}