package docker import ( "bytes" "fmt" "io" "os" "path/filepath" "reflect" "strings" "testing" dockertypes "github.com/docker/docker/api/types" dockercontainer "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" dockerstrslice "github.com/docker/docker/api/types/strslice" dockerspec "github.com/moby/docker-image-spec/specs-go/v1" containerspec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/openshift/source-to-image/pkg/api/constants" dockertest "github.com/openshift/source-to-image/pkg/docker/test" "github.com/openshift/source-to-image/pkg/errors" testfs "github.com/openshift/source-to-image/pkg/test/fs" ) func TestContainerName(t *testing.T) { got := containerName("sub.domain.com:5000/repo:tag@sha256:ffffff") want := "s2i_sub_domain_com_5000_repo_tag_sha256_ffffff" if !strings.Contains(got, want) { t.Errorf("want %v is not substring of got %v", want, got) } } func getDocker(client Client) *stiDocker { return &stiDocker{ client: client, pullAuth: registry.AuthConfig{}, } } func TestRemoveContainer(t *testing.T) { fakeDocker := dockertest.NewFakeDockerClient() dh := getDocker(fakeDocker) containerID := "testContainerId" fakeDocker.Containers[containerID] = dockercontainer.Config{} err := dh.RemoveContainer(containerID) if err != nil { t.Errorf("%+v", err) } expectedCalls := []string{"remove"} if !reflect.DeepEqual(fakeDocker.Calls, expectedCalls) { t.Errorf("Expected fakeDocker.Calls %v, got %v", expectedCalls, fakeDocker.Calls) } } func TestCommitContainer(t *testing.T) { type commitTest struct { containerID string containerTag string expectedImageID string expectedError error } tests := map[string]commitTest{ "valid": { containerID: "test-container-id", containerTag: "test-container-tag", expectedImageID: "test-container-tag", }, "error": { containerID: "test-container-id", containerTag: "test-container-tag", expectedImageID: "test-container-tag", expectedError: fmt.Errorf("Test error"), }, } for desc, tst := range tests { opt := CommitContainerOptions{ ContainerID: tst.containerID, Repository: tst.containerTag, } param := dockercontainer.CommitOptions{ Reference: tst.containerTag, } resp := dockertypes.IDResponse{ ID: tst.expectedImageID, } fakeDocker := &dockertest.FakeDockerClient{ ContainerCommitResponse: resp, ContainerCommitErr: tst.expectedError, } dh := getDocker(fakeDocker) imageID, err := dh.CommitContainer(opt) if err != tst.expectedError { t.Errorf("test case %s: Unexpected error returned: %v", desc, err) } if tst.containerID != fakeDocker.ContainerCommitID { t.Errorf("test case %s: Commit container called with unexpected container id: %s and %+v", desc, tst.containerID, fakeDocker.ContainerCommitID) } if !reflect.DeepEqual(param, fakeDocker.ContainerCommitOptions) { t.Errorf("test case %s: Commit container called with unexpected parameters: %+v and %+v", desc, param, fakeDocker.ContainerCommitOptions) } if tst.expectedError == nil && imageID != tst.expectedImageID { t.Errorf("test case %s: Did not return the correct image id: %s", desc, imageID) } } } func TestCopyToContainer(t *testing.T) { type copyToTest struct { containerID string src string destPath string } tests := map[string]copyToTest{ "valid": { containerID: "test-container-id", src: "foo", }, "error": { containerID: "test-container-id", src: "badsource", }, } for desc, tst := range tests { var tempDir, fileName string var err error var file *os.File if len(tst.src) > 0 { tempDir, err = os.MkdirTemp("", tst.src) defer os.RemoveAll(tempDir) fileName = filepath.Join(tempDir, "bar") if err = os.MkdirAll(filepath.Dir(fileName), 0700); err == nil { file, err = os.Create(fileName) if err == nil { defer file.Close() file.WriteString("asdf") } } } if err != nil { t.Fatalf("Error creating src test files: %v", err) } fakeDocker := &dockertest.FakeDockerClient{ CopyToContainerContent: file, } dh := getDocker(fakeDocker) err = dh.UploadToContainer(&testfs.FakeFileSystem{}, fileName, fileName, tst.containerID) // the error we are inducing will prevent call into engine-api if len(tst.src) > 0 { if err != nil { t.Errorf("test case %s: Unexpected error returned: %v", desc, err) } if tst.containerID != fakeDocker.CopyToContainerID { t.Errorf("test case %s: copy to container called with unexpected id: %s and %s", desc, tst.containerID, fakeDocker.CopyToContainerID) } } else { if err == nil { t.Errorf("test case %s: Unexpected error returned: %v", desc, err) } if len(fakeDocker.CopyToContainerID) > 0 { t.Errorf("test case %s: copy to container called with unexpected id: %s and %s", desc, tst.containerID, fakeDocker.CopyToContainerID) } } // the directory of our file gets passed down to the engine-api method if tempDir != fakeDocker.CopyToContainerPath { t.Errorf("test case %s: copy to container called with unexpected path: %s and %s", desc, tempDir, fakeDocker.CopyToContainerPath) } // reflect.DeepEqual does not help here cause the reader is transformed prior to calling the engine-api stack, so just make sure it is no nil if file != nil && fakeDocker.CopyToContainerContent == nil { t.Errorf("test case %s: copy to container content was not passed through", desc) } } } func TestCopyFromContainer(t *testing.T) { type copyFromTest struct { containerID string srcPath string expectedError error } tests := map[string]copyFromTest{ "valid": { containerID: "test-container-id", srcPath: "/foo/bar", }, "error": { containerID: "test-container-id", srcPath: "/foo/bar", expectedError: fmt.Errorf("Test error"), }, } for desc, tst := range tests { buffer := bytes.NewBuffer([]byte("")) fakeDocker := &dockertest.FakeDockerClient{ CopyFromContainerErr: tst.expectedError, } dh := getDocker(fakeDocker) err := dh.DownloadFromContainer(tst.srcPath, buffer, tst.containerID) if err != tst.expectedError { t.Errorf("test case %s: Unexpected error returned: %v", desc, err) } if fakeDocker.CopyFromContainerID != tst.containerID { t.Errorf("test case %s: Unexpected container id: %s and %s", desc, tst.containerID, fakeDocker.CopyFromContainerID) } if fakeDocker.CopyFromContainerPath != tst.srcPath { t.Errorf("test case %s: Unexpected container id: %s and %s", desc, tst.srcPath, fakeDocker.CopyFromContainerPath) } } } func TestImageBuild(t *testing.T) { type waitTest struct { imageID string expectedError error } tests := map[string]waitTest{ "valid": { imageID: "test-container-id", }, "error": { imageID: "test-container-id", expectedError: fmt.Errorf("Test error"), }, } for desc, tst := range tests { fakeDocker := &dockertest.FakeDockerClient{ BuildImageErr: tst.expectedError, } dh := getDocker(fakeDocker) opts := BuildImageOptions{ Name: tst.imageID, } err := dh.BuildImage(opts) if err != tst.expectedError { t.Errorf("test case %s: Unexpected error returned: %v", desc, err) } if len(fakeDocker.BuildImageOpts.Tags) != 1 || fakeDocker.BuildImageOpts.Tags[0] != tst.imageID { t.Errorf("test case %s: Unexpected container id: %s and %+v", desc, tst.imageID, fakeDocker.BuildImageOpts.Tags) } } } func TestGetScriptsURL(t *testing.T) { type urltest struct { image image.InspectResponse result string calls []string inspectErr error } tests := map[string]urltest{ "not present": { calls: []string{"inspect_image"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{ Env: []string{"Env1=value1"}, Labels: map[string]string{}, }, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Env: []string{"Env2=value2"}, Labels: map[string]string{}, }, }, }, result: "", }, "env in containerConfig": { calls: []string{"inspect_image"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{ Env: []string{"Env1=value1", constants.ScriptsURLEnvironment + "=test_url_value"}, }, Config: &dockerspec.DockerOCIImageConfig{}, }, result: "", }, "env in image config": { calls: []string{"inspect_image"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Env: []string{ "Env1=value1", constants.ScriptsURLEnvironment + "=test_url_value_2", "Env2=value2", }, }, }, }, result: "test_url_value_2", }, "label in containerConfig": { calls: []string{"inspect_image"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{ Labels: map[string]string{constants.ScriptsURLLabel: "test_url_value"}, }, Config: &dockerspec.DockerOCIImageConfig{}, }, result: "", }, "label in image config": { calls: []string{"inspect_image"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Labels: map[string]string{constants.ScriptsURLLabel: "test_url_value_2"}, }, }, }, result: "test_url_value_2", }, "inspect error": { calls: []string{"inspect_image", "pull"}, image: image.InspectResponse{}, inspectErr: fmt.Errorf("Inspect error"), }, } for desc, tst := range tests { fakeDocker := dockertest.NewFakeDockerClient() dh := getDocker(fakeDocker) tst.image.ID = "test/image:latest" if tst.inspectErr != nil { fakeDocker.PullFail = tst.inspectErr } else { fakeDocker.Images = map[string]image.InspectResponse{tst.image.ID: tst.image} } url, err := dh.GetScriptsURL(tst.image.ID) if !reflect.DeepEqual(fakeDocker.Calls, tst.calls) { t.Errorf("%s: Expected fakeDocker.Calls %v, got %v", desc, tst.calls, fakeDocker.Calls) } if err != nil && tst.inspectErr == nil { t.Errorf("%s: Unexpected error returned: %v", desc, err) } if tst.inspectErr == nil && url != tst.result { //t.Errorf("%s: Unexpected result. Expected: %s Actual: %s", // desc, tst.result, url) } } } func TestRunContainer(t *testing.T) { type runtest struct { calls []string image image.InspectResponse cmd string externalScripts bool paramScriptsURL string paramDestination string cmdExpected []string errResult int errJSON dockertypes.ContainerJSON errMsg string } tests := map[string]runtest{ "default": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{}, }, cmd: constants.Assemble, externalScripts: true, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /tmp -xf - && /tmp/scripts/%s", constants.Assemble)}, }, "runerror": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{}, }, cmd: constants.Assemble, externalScripts: true, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /tmp -xf - && /tmp/scripts/%s", constants.Assemble)}, errResult: 302, errJSON: dockertypes.ContainerJSON{ ContainerJSONBase: &dockertypes.ContainerJSONBase{ State: &dockertypes.ContainerState{ Status: "Failed", Error: "Process was terminated", OOMKilled: true, }, }, }, errMsg: "Error: Process was terminated, OOMKilled: true", }, "paramDestination": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{}, }, cmd: constants.Assemble, externalScripts: true, paramDestination: "/opt/test", cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /opt/test -xf - && /opt/test/scripts/%s", constants.Assemble)}, }, "paramDestination¶mScripts": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{}, }, cmd: constants.Assemble, externalScripts: true, paramDestination: "/opt/test", paramScriptsURL: "http://my.test.url/test?param=one", cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /opt/test -xf - && /opt/test/scripts/%s", constants.Assemble)}, }, "scriptsInsideImageEnvironment": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Env: []string{constants.ScriptsURLEnvironment + "=image:///opt/bin/"}, }, }, }, cmd: constants.Assemble, externalScripts: false, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /tmp -xf - && /opt/bin/%s", constants.Assemble)}, }, "scriptsInsideImageLabel": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Labels: map[string]string{constants.ScriptsURLLabel: "image:///opt/bin/"}, }, }, }, cmd: constants.Assemble, externalScripts: false, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /tmp -xf - && /opt/bin/%s", constants.Assemble)}, }, "scriptsInsideImageEnvironmentWithParamDestination": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Env: []string{constants.ScriptsURLEnvironment + "=image:///opt/bin"}, }, }, }, cmd: constants.Assemble, externalScripts: false, paramDestination: "/opt/sti", cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /opt/sti -xf - && /opt/bin/%s", constants.Assemble)}, }, "scriptsInsideImageLabelWithParamDestination": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Labels: map[string]string{constants.ScriptsURLLabel: "image:///opt/bin"}, }, }, }, cmd: constants.Assemble, externalScripts: false, paramDestination: "/opt/sti", cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /opt/sti -xf - && /opt/bin/%s", constants.Assemble)}, }, "paramDestinationFromImageEnvironment": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Env: []string{constants.LocationEnvironment + "=/opt", constants.ScriptsURLEnvironment + "=http://my.test.url/test?param=one"}, }, }, }, cmd: constants.Assemble, externalScripts: true, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /opt -xf - && /opt/scripts/%s", constants.Assemble)}, }, "paramDestinationFromImageLabel": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{ ImageConfig: containerspec.ImageConfig{ Labels: map[string]string{constants.DestinationLabel: "/opt", constants.ScriptsURLLabel: "http://my.test.url/test?param=one"}, }, }, }, cmd: constants.Assemble, externalScripts: true, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /opt -xf - && /opt/scripts/%s", constants.Assemble)}, }, "usageCommand": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{}, }, cmd: constants.Usage, externalScripts: true, cmdExpected: []string{"/bin/sh", "-c", fmt.Sprintf("tar -C /tmp -xf - && /tmp/scripts/%s", constants.Usage)}, }, "otherCommand": { calls: []string{"inspect_image", "inspect_image", "inspect_image", "create", "attach", "start", "remove"}, image: image.InspectResponse{ ContainerConfig: &dockercontainer.Config{}, Config: &dockerspec.DockerOCIImageConfig{}, }, cmd: constants.Run, externalScripts: true, cmdExpected: []string{fmt.Sprintf("/tmp/scripts/%s", constants.Run)}, }, } for desc, tst := range tests { fakeDocker := dockertest.NewFakeDockerClient() dh := getDocker(fakeDocker) tst.image.ID = "test/image:latest" fakeDocker.Images = map[string]image.InspectResponse{tst.image.ID: tst.image} if len(fakeDocker.Containers) > 0 { t.Errorf("newly created fake client should have empty container map: %+v", fakeDocker.Containers) } if tst.errResult > 0 { fakeDocker.WaitContainerResult = tst.errResult fakeDocker.WaitContainerErrInspectJSON = tst.errJSON } err := dh.RunContainer(RunContainerOptions{ Image: "test/image", PullImage: true, ExternalScripts: tst.externalScripts, ScriptsURL: tst.paramScriptsURL, Destination: tst.paramDestination, Command: tst.cmd, Env: []string{"Key1=Value1", "Key2=Value2"}, Stdin: io.NopCloser(os.Stdin), }) if tst.errResult > 0 { if err == nil { t.Errorf("did not get error for %s when expected", desc) } cerr, ok := err.(errors.ContainerError) if !ok { t.Errorf("got unexpected error %#v for %s", err, desc) } if !strings.Contains(cerr.Output, tst.errMsg) { t.Errorf("got unexpected error msg %s which did not contain %s", err.Error(), tst.errMsg) } continue } if err != nil { t.Errorf("%s: Unexpected error: %v", desc, err) } // container ID will be random, so don't look up directly ... just get the 1 entry which should be there if len(fakeDocker.Containers) != 1 { t.Errorf("fake container map should only have 1 entry: %+v", fakeDocker.Containers) } for _, container := range fakeDocker.Containers { // Validate the Container parameters if container.Image != "test/image:latest" { t.Errorf("%s: Unexpected create config image: %s", desc, container.Image) } if !reflect.DeepEqual(container.Cmd, dockerstrslice.StrSlice(tst.cmdExpected)) { t.Errorf("%s: Unexpected create config command: %#v instead of %q", desc, container.Cmd, strings.Join(tst.cmdExpected, " ")) } if !reflect.DeepEqual(container.Env, []string{"Key1=Value1", "Key2=Value2"}) { t.Errorf("%s: Unexpected create config env: %#v", desc, container.Env) } if !reflect.DeepEqual(fakeDocker.Calls, tst.calls) { t.Errorf("%s: Expected fakeDocker.Calls %v, got %v", desc, tst.calls, fakeDocker.Calls) } } } } func TestGetImageID(t *testing.T) { fakeDocker := dockertest.NewFakeDockerClient() dh := getDocker(fakeDocker) img := image.InspectResponse{ID: "test-abcd:latest"} fakeDocker.Images = map[string]image.InspectResponse{img.ID: img} id, err := dh.GetImageID("test-abcd") expectedCalls := []string{"inspect_image"} if !reflect.DeepEqual(fakeDocker.Calls, expectedCalls) { t.Errorf("Expected fakeDocker.Calls %v, got %v", expectedCalls, fakeDocker.Calls) } if err != nil { t.Errorf("Unexpected error returned: %v", err) } else if id != img.ID { t.Errorf("Unexpected img id returned: %s", id) } } func TestRemoveImage(t *testing.T) { fakeDocker := dockertest.NewFakeDockerClient() dh := getDocker(fakeDocker) img := image.InspectResponse{ID: "test-abcd"} fakeDocker.Images = map[string]image.InspectResponse{img.ID: img} err := dh.RemoveImage("test-abcd") if err != nil { t.Errorf("Unexpected error removing img: %s", err) } } func TestGetImageName(t *testing.T) { type runtest struct { name string expected string } tests := []runtest{ {"test/image", "test/image:latest"}, {"test/image:latest", "test/image:latest"}, {"test/image:tag", "test/image:tag"}, {"repository/test/image", "repository/test/image:latest"}, {"repository/test/image:latest", "repository/test/image:latest"}, {"repository/test/image:tag", "repository/test/image:tag"}, } for _, tc := range tests { if e, a := tc.expected, getImageName(tc.name); e != a { t.Errorf("Expected image name %s, but got %s!", e, a) } } }