[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
This commit is contained in:
Dmitry Ukov 2019-08-12 08:43:06 +00:00
parent 91bf9ab6e0
commit b37b6f3703
10 changed files with 1069 additions and 9 deletions

2
go.mod
View File

@ -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

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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}
}
}

View File

@ -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: <image_path>:<tag>. 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,
},
)
}

View File

@ -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)
}
}

View File

@ -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)
}

32
pkg/container/errors.go Normal file
View File

@ -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)
}

9
pkg/errors/common.go Normal file
View File

@ -0,0 +1,9 @@
package errors
// ErrNotImplemented returned for not implemented features
type ErrNotImplemented struct {
}
func (e ErrNotImplemented) Error() string {
return "Error. Not implemented"
}