package dockerfile import ( "fmt" "os" "path/filepath" "runtime" "testing" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/idtools" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestEmptyDockerfile(t *testing.T) { contextDir, cleanup := createTestTempDir(t, "", "builder-dockerfile-test") defer cleanup() createTestTempFile(t, contextDir, builder.DefaultDockerfileName, "", 0777) readAndCheckDockerfile(t, "emptyDockerfile", contextDir, "", "the Dockerfile (Dockerfile) cannot be empty") } func TestSymlinkDockerfile(t *testing.T) { contextDir, cleanup := createTestTempDir(t, "", "builder-dockerfile-test") defer cleanup() createTestSymlink(t, contextDir, builder.DefaultDockerfileName, "/etc/passwd") // The reason the error is "Cannot locate specified Dockerfile" is because // in the builder, the symlink is resolved within the context, therefore // Dockerfile -> /etc/passwd becomes etc/passwd from the context which is // a nonexistent file. expectedError := fmt.Sprintf("Cannot locate specified Dockerfile: %s", builder.DefaultDockerfileName) readAndCheckDockerfile(t, "symlinkDockerfile", contextDir, builder.DefaultDockerfileName, expectedError) } func TestDockerfileOutsideTheBuildContext(t *testing.T) { contextDir, cleanup := createTestTempDir(t, "", "builder-dockerfile-test") defer cleanup() expectedError := "Forbidden path outside the build context: ../../Dockerfile ()" readAndCheckDockerfile(t, "DockerfileOutsideTheBuildContext", contextDir, "../../Dockerfile", expectedError) } func TestNonExistingDockerfile(t *testing.T) { contextDir, cleanup := createTestTempDir(t, "", "builder-dockerfile-test") defer cleanup() expectedError := "Cannot locate specified Dockerfile: Dockerfile" readAndCheckDockerfile(t, "NonExistingDockerfile", contextDir, "Dockerfile", expectedError) } func readAndCheckDockerfile(t *testing.T, testName, contextDir, dockerfilePath, expectedError string) { tarStream, err := archive.Tar(contextDir, archive.Uncompressed) require.NoError(t, err) defer func() { if err = tarStream.Close(); err != nil { t.Fatalf("Error when closing tar stream: %s", err) } }() if dockerfilePath == "" { // handled in BuildWithContext dockerfilePath = builder.DefaultDockerfileName } config := backend.BuildConfig{ Options: &types.ImageBuildOptions{Dockerfile: dockerfilePath}, Source: tarStream, } _, _, err = remotecontext.Detect(config) assert.EqualError(t, err, expectedError) } func TestCopyRunConfig(t *testing.T) { defaultEnv := []string{"foo=1"} defaultCmd := []string{"old"} var testcases = []struct { doc string modifiers []runConfigModifier expected *container.Config }{ { doc: "Set the command", modifiers: []runConfigModifier{withCmd([]string{"new"})}, expected: &container.Config{ Cmd: []string{"new"}, Env: defaultEnv, }, }, { doc: "Set the command to a comment", modifiers: []runConfigModifier{withCmdComment("comment", runtime.GOOS)}, expected: &container.Config{ Cmd: append(defaultShellForOS(runtime.GOOS), "#(nop) ", "comment"), Env: defaultEnv, }, }, { doc: "Set the command and env", modifiers: []runConfigModifier{ withCmd([]string{"new"}), withEnv([]string{"one", "two"}), }, expected: &container.Config{ Cmd: []string{"new"}, Env: []string{"one", "two"}, }, }, } for _, testcase := range testcases { runConfig := &container.Config{ Cmd: defaultCmd, Env: defaultEnv, } runConfigCopy := copyRunConfig(runConfig, testcase.modifiers...) assert.Equal(t, testcase.expected, runConfigCopy, testcase.doc) // Assert the original was not modified assert.NotEqual(t, runConfig, runConfigCopy, testcase.doc) } } func fullMutableRunConfig() *container.Config { return &container.Config{ Cmd: []string{"command", "arg1"}, Env: []string{"env1=foo", "env2=bar"}, ExposedPorts: nat.PortSet{ "1000/tcp": {}, "1001/tcp": {}, }, Volumes: map[string]struct{}{ "one": {}, "two": {}, }, Entrypoint: []string{"entry", "arg1"}, OnBuild: []string{"first", "next"}, Labels: map[string]string{ "label1": "value1", "label2": "value2", }, Shell: []string{"shell", "-c"}, } } func TestDeepCopyRunConfig(t *testing.T) { runConfig := fullMutableRunConfig() copy := copyRunConfig(runConfig) assert.Equal(t, fullMutableRunConfig(), copy) copy.Cmd[1] = "arg2" copy.Env[1] = "env2=new" copy.ExposedPorts["10002"] = struct{}{} copy.Volumes["three"] = struct{}{} copy.Entrypoint[1] = "arg2" copy.OnBuild[0] = "start" copy.Labels["label3"] = "value3" copy.Shell[0] = "sh" assert.Equal(t, fullMutableRunConfig(), runConfig) } func TestChownFlagParsing(t *testing.T) { testFiles := map[string]string{ "passwd": `root:x:0:0::/bin:/bin/false bin:x:1:1::/bin:/bin/false wwwwww:x:21:33::/bin:/bin/false unicorn:x:1001:1002::/bin:/bin/false `, "group": `root:x:0: bin:x:1: wwwwww:x:33: unicorn:x:1002: somegrp:x:5555: othergrp:x:6666: `, } // test mappings for validating use of maps idMaps := []idtools.IDMap{ { ContainerID: 0, HostID: 100000, Size: 65536, }, } remapped := idtools.NewIDMappingsFromMaps(idMaps, idMaps) unmapped := &idtools.IDMappings{} contextDir, cleanup := createTestTempDir(t, "", "builder-chown-parse-test") defer cleanup() if err := os.Mkdir(filepath.Join(contextDir, "etc"), 0755); err != nil { t.Fatalf("error creating test directory: %v", err) } for filename, content := range testFiles { createTestTempFile(t, filepath.Join(contextDir, "etc"), filename, content, 0644) } // positive tests for _, testcase := range []struct { name string chownStr string idMapping *idtools.IDMappings expected idtools.IDPair }{ { name: "UIDNoMap", chownStr: "1", idMapping: unmapped, expected: idtools.IDPair{UID: 1, GID: 1}, }, { name: "UIDGIDNoMap", chownStr: "0:1", idMapping: unmapped, expected: idtools.IDPair{UID: 0, GID: 1}, }, { name: "UIDWithMap", chownStr: "0", idMapping: remapped, expected: idtools.IDPair{UID: 100000, GID: 100000}, }, { name: "UIDGIDWithMap", chownStr: "1:33", idMapping: remapped, expected: idtools.IDPair{UID: 100001, GID: 100033}, }, { name: "UserNoMap", chownStr: "bin:5555", idMapping: unmapped, expected: idtools.IDPair{UID: 1, GID: 5555}, }, { name: "GroupWithMap", chownStr: "0:unicorn", idMapping: remapped, expected: idtools.IDPair{UID: 100000, GID: 101002}, }, { name: "UserOnlyWithMap", chownStr: "unicorn", idMapping: remapped, expected: idtools.IDPair{UID: 101001, GID: 101002}, }, } { t.Run(testcase.name, func(t *testing.T) { idPair, err := parseChownFlag(testcase.chownStr, contextDir, testcase.idMapping) require.NoError(t, err, "Failed to parse chown flag: %q", testcase.chownStr) assert.Equal(t, testcase.expected, idPair, "chown flag mapping failure") }) } // error tests for _, testcase := range []struct { name string chownStr string idMapping *idtools.IDMappings descr string }{ { name: "BadChownFlagFormat", chownStr: "bob:1:555", idMapping: unmapped, descr: "invalid chown string format: bob:1:555", }, { name: "UserNoExist", chownStr: "bob", idMapping: unmapped, descr: "can't find uid for user bob: no such user: bob", }, { name: "GroupNoExist", chownStr: "root:bob", idMapping: unmapped, descr: "can't find gid for group bob: no such group: bob", }, } { t.Run(testcase.name, func(t *testing.T) { _, err := parseChownFlag(testcase.chownStr, contextDir, testcase.idMapping) assert.EqualError(t, err, testcase.descr, "Expected error string doesn't match") }) } }