diff --git a/container/container.go b/container/container.go index 92161e708d..b86bff6ec6 100644 --- a/container/container.go +++ b/container/container.go @@ -948,3 +948,21 @@ func (container *Container) InitializeStdio(iop libcontainerd.IOPipe) error { return nil } + +// SecretMountPath returns the path of the secret mount for the container +func (container *Container) SecretMountPath() string { + return filepath.Join(container.Root, "secrets") +} + +// SecretFilePath returns the path to the location of a secret on the host. +func (container *Container) SecretFilePath(secretRef swarmtypes.SecretReference) string { + return filepath.Join(container.SecretMountPath(), secretRef.SecretID) +} + +func getSecretTargetPath(r *swarmtypes.SecretReference) string { + if filepath.IsAbs(r.File.Name) { + return r.File.Name + } + + return filepath.Join(containerSecretMountPath, r.File.Name) +} diff --git a/container/container_notlinux.go b/container/container_notlinux.go index f65653e992..768c762d2f 100644 --- a/container/container_notlinux.go +++ b/container/container_notlinux.go @@ -12,8 +12,8 @@ func detachMounted(path string) error { return unix.Unmount(path, 0) } -// SecretMount returns the mount for the secret path -func (container *Container) SecretMount() *Mount { +// SecretMounts returns the mounts for the secret path +func (container *Container) SecretMounts() []Mount { return nil } diff --git a/container/container_unit_test.go b/container/container_unit_test.go index f301f25bbe..01d06e4eb8 100644 --- a/container/container_unit_test.go +++ b/container/container_unit_test.go @@ -1,9 +1,11 @@ package container import ( + "path/filepath" "testing" "github.com/docker/docker/api/types/container" + swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/signal" ) @@ -58,3 +60,17 @@ func TestContainerStopTimeout(t *testing.T) { t.Fatalf("Expected 15, got %v", s) } } + +func TestContainerSecretReferenceDestTarget(t *testing.T) { + ref := &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "app", + }, + } + + d := getSecretTargetPath(ref) + expected := filepath.Join(containerSecretMountPath, "app") + if d != expected { + t.Fatalf("expected secret dest %q; received %q", expected, d) + } +} diff --git a/container/container_unix.go b/container/container_unix.go index 898a1bdceb..265bea84d2 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -163,11 +163,6 @@ func (container *Container) NetworkMounts() []Mount { return mounts } -// SecretMountPath returns the path of the secret mount for the container -func (container *Container) SecretMountPath() string { - return filepath.Join(container.Root, "secrets") -} - // CopyImagePathContent copies files in destination to the volume. func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error { rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS) @@ -253,17 +248,21 @@ func (container *Container) IpcMounts() []Mount { return mounts } -// SecretMount returns the mount for the secret path -func (container *Container) SecretMount() *Mount { - if len(container.SecretReferences) > 0 { - return &Mount{ - Source: container.SecretMountPath(), - Destination: containerSecretMountPath, - Writable: false, +// SecretMounts returns the mounts for the secret path. +func (container *Container) SecretMounts() []Mount { + var mounts []Mount + for _, r := range container.SecretReferences { + if r.File == nil { + continue } + mounts = append(mounts, Mount{ + Source: container.SecretFilePath(*r), + Destination: getSecretTargetPath(r), + Writable: false, + }) } - return nil + return mounts } // UnmountSecrets unmounts the local tmpfs for secrets diff --git a/container/container_windows.go b/container/container_windows.go index ab56b61033..50e202f8c4 100644 --- a/container/container_windows.go +++ b/container/container_windows.go @@ -10,6 +10,10 @@ import ( containertypes "github.com/docker/docker/api/types/container" ) +const ( + containerSecretMountPath = `C:\ProgramData\Docker\secrets` +) + // Container holds fields specific to the Windows implementation. See // CommonContainer for standard fields common to all containers. type Container struct { @@ -43,8 +47,8 @@ func (container *Container) IpcMounts() []Mount { return nil } -// SecretMount returns the mount for the secret path -func (container *Container) SecretMount() *Mount { +// SecretMounts returns the mounts for the secret path +func (container *Container) SecretMounts() []Mount { return nil } diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index 0b602ef20c..21eebaf178 100644 --- a/daemon/container_operations_unix.go +++ b/daemon/container_operations_unix.go @@ -177,13 +177,9 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { return fmt.Errorf("secret target type is not a file target") } - targetPath := filepath.Clean(s.File.Name) - // ensure that the target is a filename only; no paths allowed - if targetPath != filepath.Base(targetPath) { - return fmt.Errorf("error creating secret: secret must not be a path") - } - - fPath := filepath.Join(localMountPath, targetPath) + // secrets are created in the SecretMountPath on the host, at a + // single level + fPath := c.SecretFilePath(*s) if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil { return errors.Wrap(err, "error creating secret mount path") } diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index a469a63b53..f04188b171 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -750,8 +750,8 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { } ms = append(ms, tmpfsMounts...) - if m := c.SecretMount(); m != nil { - ms = append(ms, *m) + if m := c.SecretMounts(); m != nil { + ms = append(ms, m...) } sort.Sort(mounts(ms)) diff --git a/hack/dockerfile/binaries-commits b/hack/dockerfile/binaries-commits index a0c4a73d61..b57e2b32e9 100644 --- a/hack/dockerfile/binaries-commits +++ b/hack/dockerfile/binaries-commits @@ -11,5 +11,5 @@ VNDR_COMMIT=c56e082291115e369f77601f9c071dd0b87c7120 BINDATA_COMMIT=a0ff2567cfb70903282db057e799fd826784d41d # CLI -DOCKERCLI_REPO=https://github.com/docker/cli -DOCKERCLI_COMMIT=c3648a9c9400d45524cc71b8fca4085b192c626f +DOCKERCLI_REPO=https://github.com/aaronlehmann/cli +DOCKERCLI_COMMIT=fd5a5910409fe5ed862b29de45a66f2bf8056894 diff --git a/integration-cli/docker_cli_service_create_test.go b/integration-cli/docker_cli_service_create_test.go index 1ff9b482cf..0ff7feb594 100644 --- a/integration-cli/docker_cli_service_create_test.go +++ b/integration-cli/docker_cli_service_create_test.go @@ -5,6 +5,7 @@ package main import ( "encoding/json" "fmt" + "path/filepath" "strings" "github.com/docker/docker/api/types" @@ -90,23 +91,41 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretSimple(c *check.C) { c.Assert(refs[0].File.Name, checker.Equals, testName) c.Assert(refs[0].File.UID, checker.Equals, "0") c.Assert(refs[0].File.GID, checker.Equals, "0") + + out, err = d.Cmd("service", "rm", serviceName) + c.Assert(err, checker.IsNil, check.Commentf(out)) + d.DeleteSecret(c, testName) } -func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) { +func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTargetPaths(c *check.C) { d := s.AddDaemon(c, true, true) - serviceName := "test-service-secret" - testName := "test_secret" - id := d.CreateSecret(c, swarm.SecretSpec{ - Annotations: swarm.Annotations{ - Name: testName, - }, - Data: []byte("TESTINGDATA"), - }) - c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) - testTarget := "testing" + testPaths := map[string]string{ + "app": "/etc/secret", + "test_secret": "test_secret", + "relative_secret": "relative/secret", + "escapes_in_container": "../secret", + } - out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget), "busybox", "top") + var secretFlags []string + + for testName, testTarget := range testPaths { + id := d.CreateSecret(c, swarm.SecretSpec{ + Annotations: swarm.Annotations{ + Name: testName, + }, + Data: []byte("TESTINGDATA " + testName + " " + testTarget), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + secretFlags = append(secretFlags, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget)) + } + + serviceName := "svc" + serviceCmd := []string{"service", "create", "--name", serviceName} + serviceCmd = append(serviceCmd, secretFlags...) + serviceCmd = append(serviceCmd, "busybox", "top") + out, err := d.Cmd(serviceCmd...) c.Assert(err, checker.IsNil, check.Commentf(out)) out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) @@ -114,11 +133,82 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) { var refs []swarm.SecretReference c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil) - c.Assert(refs, checker.HasLen, 1) + c.Assert(refs, checker.HasLen, len(testPaths)) - c.Assert(refs[0].SecretName, checker.Equals, testName) - c.Assert(refs[0].File, checker.Not(checker.IsNil)) - c.Assert(refs[0].File.Name, checker.Equals, testTarget) + var tasks []swarm.Task + waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) { + tasks = d.GetServiceTasks(c, serviceName) + return len(tasks) > 0, nil + }, checker.Equals, true) + + task := tasks[0] + waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) { + if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" { + task = d.GetTask(c, task.ID) + } + return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil + }, checker.Equals, true) + + for testName, testTarget := range testPaths { + path := testTarget + if !filepath.IsAbs(path) { + path = filepath.Join("/run/secrets", path) + } + out, err := d.Cmd("exec", task.Status.ContainerStatus.ContainerID, "cat", path) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Equals, "TESTINGDATA "+testName+" "+testTarget) + } + + out, err = d.Cmd("service", "rm", serviceName) + c.Assert(err, checker.IsNil, check.Commentf(out)) +} + +func (s *DockerSwarmSuite) TestServiceCreateWithSecretReferencedTwice(c *check.C) { + d := s.AddDaemon(c, true, true) + + id := d.CreateSecret(c, swarm.SecretSpec{ + Annotations: swarm.Annotations{ + Name: "mysecret", + }, + Data: []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + serviceName := "svc" + out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", "source=mysecret,target=target1", "--secret", "source=mysecret,target=target2", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) + c.Assert(err, checker.IsNil) + + var refs []swarm.SecretReference + c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil) + c.Assert(refs, checker.HasLen, 2) + + var tasks []swarm.Task + waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) { + tasks = d.GetServiceTasks(c, serviceName) + return len(tasks) > 0, nil + }, checker.Equals, true) + + task := tasks[0] + waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) { + if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" { + task = d.GetTask(c, task.ID) + } + return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil + }, checker.Equals, true) + + for _, target := range []string{"target1", "target2"} { + c.Assert(err, checker.IsNil, check.Commentf(out)) + path := filepath.Join("/run/secrets", target) + out, err := d.Cmd("exec", task.Status.ContainerStatus.ContainerID, "cat", path) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Equals, "TESTINGDATA") + } + + out, err = d.Cmd("service", "rm", serviceName) + c.Assert(err, checker.IsNil, check.Commentf(out)) } func (s *DockerSwarmSuite) TestServiceCreateMountTmpfs(c *check.C) { diff --git a/opts/secret.go b/opts/secret.go index 56ed29eb5b..a1fde54d91 100644 --- a/opts/secret.go +++ b/opts/secret.go @@ -4,7 +4,6 @@ import ( "encoding/csv" "fmt" "os" - "path/filepath" "strconv" "strings" @@ -53,10 +52,6 @@ func (o *SecretOpt) Set(value string) error { case "source", "src": options.SecretName = value case "target": - tDir, _ := filepath.Split(value) - if tDir != "" { - return fmt.Errorf("target must not be a path") - } options.File.Name = value case "uid": options.File.UID = value