Skip to content

Commit bfb8104

Browse files
committed
volumes: Implement subpath mount
`VolumeOptions` now has a `Subpath` field which allows to specify a path relative to the volume that should be mounted as a destination. Symlinks are supported, but they cannot escape the base volume directory. Signed-off-by: Paweł Gronowski <[email protected]>
1 parent f2e1105 commit bfb8104

20 files changed

Lines changed: 727 additions & 30 deletions

File tree

api/server/router/container/container_routes.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,14 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
628628
}
629629
}
630630

631+
if versions.LessThan(version, "1.45") {
632+
for _, m := range hostConfig.Mounts {
633+
if m.VolumeOptions != nil && m.VolumeOptions.Subpath != "" {
634+
return errdefs.InvalidParameter(errors.New("VolumeOptions.Subpath needs API v1.45 or newer"))
635+
}
636+
}
637+
}
638+
631639
var warnings []string
632640
if warn, err := handleMACAddressBC(config, hostConfig, networkingConfig, version); err != nil {
633641
return err

api/swagger.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@ definitions:
423423
type: "object"
424424
additionalProperties:
425425
type: "string"
426+
Subpath:
427+
description: "Source path inside the volume. Must be relative without any back traversals."
428+
type: "string"
429+
example: "dir-inside-volume/subdirectory"
426430
TmpfsOptions:
427431
description: "Optional configuration for the `tmpfs` type."
428432
type: "object"

api/types/mount/mount.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ type BindOptions struct {
9696
type VolumeOptions struct {
9797
NoCopy bool `json:",omitempty"`
9898
Labels map[string]string `json:",omitempty"`
99+
Subpath string `json:",omitempty"`
99100
DriverConfig *Driver `json:",omitempty"`
100101
}
101102

daemon/containerfs_linux.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai
6363
}
6464
}()
6565

66-
mounts, err := daemon.setupMounts(container)
66+
mounts, cleanup, err := daemon.setupMounts(container)
6767
if err != nil {
6868
return nil, err
6969
}
7070
defer func() {
71+
cleanup()
7172
if err != nil {
7273
_ = container.UnmountVolumes(daemon.LogVolumeEvent)
7374
}

daemon/start.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,12 @@ func (daemon *Daemon) containerStart(ctx context.Context, daemonCfg *configStore
164164
return err
165165
}
166166

167-
m, err := daemon.setupMounts(container)
167+
m, cleanup, err := daemon.setupMounts(container)
168168
if err != nil {
169169
return err
170170
}
171171
mnts = append(mnts, m...)
172+
defer cleanup()
172173

173174
spec, err := daemon.createSpec(ctx, daemonCfg, container, mnts)
174175
if err != nil {

daemon/volumes_unix.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,53 @@
33
package daemon // import "github.com/docker/docker/daemon"
44

55
import (
6+
"context"
67
"fmt"
78
"os"
89
"sort"
910
"strconv"
1011
"strings"
1112

13+
"github.com/containerd/log"
1214
"github.com/docker/docker/api/types/events"
1315
mounttypes "github.com/docker/docker/api/types/mount"
1416
"github.com/docker/docker/container"
17+
"github.com/docker/docker/internal/cleanups"
1518
volumemounts "github.com/docker/docker/volume/mounts"
1619
"github.com/pkg/errors"
1720
)
1821

1922
// setupMounts iterates through each of the mount points for a container and
2023
// calls Setup() on each. It also looks to see if is a network mount such as
2124
// /etc/resolv.conf, and if it is not, appends it to the array of mounts.
22-
func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, error) {
25+
//
26+
// The cleanup function should be called as soon as the container has been
27+
// started.
28+
func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, func() error, error) {
2329
var mounts []container.Mount
2430
// TODO: tmpfs mounts should be part of Mountpoints
2531
tmpfsMounts := make(map[string]bool)
2632
tmpfsMountInfo, err := c.TmpfsMounts()
2733
if err != nil {
28-
return nil, err
34+
return nil, nil, err
2935
}
3036
for _, m := range tmpfsMountInfo {
3137
tmpfsMounts[m.Destination] = true
3238
}
39+
40+
cleanups := cleanups.Composite{}
41+
defer func() {
42+
if err := cleanups.Call(); err != nil {
43+
log.G(context.TODO()).WithError(err).Warn("failed to cleanup temporary mounts created by MountPoint.Setup")
44+
}
45+
}()
46+
3347
for _, m := range c.MountPoints {
3448
if tmpfsMounts[m.Destination] {
3549
continue
3650
}
3751
if err := daemon.lazyInitializeVolume(c.ID, m); err != nil {
38-
return nil, err
52+
return nil, nil, err
3953
}
4054
// If the daemon is being shutdown, we should not let a container start if it is trying to
4155
// mount the socket the daemon is listening on. During daemon shutdown, the socket
@@ -48,10 +62,12 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
4862
return nil
4963
}
5064

51-
path, err := m.Setup(c.MountLabel, daemon.idMapping.RootPair(), checkfunc)
65+
path, clean, err := m.Setup(c.MountLabel, daemon.idMapping.RootPair(), checkfunc)
5266
if err != nil {
53-
return nil, err
67+
return nil, nil, err
5468
}
69+
cleanups.Add(clean)
70+
5571
if !c.TrySetNetworkMount(m.Destination, path) {
5672
mnt := container.Mount{
5773
Source: path,
@@ -61,13 +77,13 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
6177
}
6278
if m.Spec.Type == mounttypes.TypeBind && m.Spec.BindOptions != nil {
6379
if !m.Spec.ReadOnly && m.Spec.BindOptions.ReadOnlyNonRecursive {
64-
return nil, errors.New("mount options conflict: !ReadOnly && BindOptions.ReadOnlyNonRecursive")
80+
return nil, nil, errors.New("mount options conflict: !ReadOnly && BindOptions.ReadOnlyNonRecursive")
6581
}
6682
if !m.Spec.ReadOnly && m.Spec.BindOptions.ReadOnlyForceRecursive {
67-
return nil, errors.New("mount options conflict: !ReadOnly && BindOptions.ReadOnlyForceRecursive")
83+
return nil, nil, errors.New("mount options conflict: !ReadOnly && BindOptions.ReadOnlyForceRecursive")
6884
}
6985
if m.Spec.BindOptions.ReadOnlyNonRecursive && m.Spec.BindOptions.ReadOnlyForceRecursive {
70-
return nil, errors.New("mount options conflict: ReadOnlyNonRecursive && BindOptions.ReadOnlyForceRecursive")
86+
return nil, nil, errors.New("mount options conflict: ReadOnlyNonRecursive && BindOptions.ReadOnlyForceRecursive")
7187
}
7288
mnt.NonRecursive = m.Spec.BindOptions.NonRecursive
7389
mnt.ReadOnlyNonRecursive = m.Spec.BindOptions.ReadOnlyNonRecursive
@@ -98,11 +114,11 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
98114
// up to the user to make sure the file has proper ownership for userns
99115
if strings.Index(mnt.Source, daemon.repository) == 0 {
100116
if err := os.Chown(mnt.Source, rootIDs.UID, rootIDs.GID); err != nil {
101-
return nil, err
117+
return nil, nil, err
102118
}
103119
}
104120
}
105-
return append(mounts, netMounts...), nil
121+
return append(mounts, netMounts...), cleanups.Release(), nil
106122
}
107123

108124
// sortMounts sorts an array of mounts in lexicographic order. This ensure that

daemon/volumes_windows.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package daemon // import "github.com/docker/docker/daemon"
22

33
import (
4+
"context"
45
"sort"
56

7+
"github.com/containerd/log"
68
"github.com/docker/docker/api/types/mount"
79
"github.com/docker/docker/container"
10+
"github.com/docker/docker/internal/cleanups"
811
"github.com/docker/docker/pkg/idtools"
912
volumemounts "github.com/docker/docker/volume/mounts"
1013
)
@@ -13,21 +16,31 @@ import (
1316
// of the configured mounts on the container to the OCI mount structure
1417
// which will ultimately be passed into the oci runtime during container creation.
1518
// It also ensures each of the mounts are lexicographically sorted.
16-
19+
//
20+
// The cleanup function should be called as soon as the container has been
21+
// started.
22+
//
1723
// BUGBUG TODO Windows containerd. This would be much better if it returned
1824
// an array of runtime spec mounts, not container mounts. Then no need to
1925
// do multiple transitions.
26+
func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, func() error, error) {
27+
cleanups := cleanups.Composite{}
28+
defer func() {
29+
if err := cleanups.Call(); err != nil {
30+
log.G(context.TODO()).WithError(err).Warn("failed to cleanup temporary mounts created by MountPoint.Setup")
31+
}
32+
}()
2033

21-
func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, error) {
2234
var mnts []container.Mount
2335
for _, mount := range c.MountPoints { // type is volumemounts.MountPoint
2436
if err := daemon.lazyInitializeVolume(c.ID, mount); err != nil {
25-
return nil, err
37+
return nil, nil, err
2638
}
27-
s, err := mount.Setup(c.MountLabel, idtools.Identity{}, nil)
39+
s, c, err := mount.Setup(c.MountLabel, idtools.Identity{}, nil)
2840
if err != nil {
29-
return nil, err
41+
return nil, nil, err
3042
}
43+
cleanups.Add(c)
3144

3245
mnts = append(mnts, container.Mount{
3346
Source: s,
@@ -37,7 +50,7 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
3750
}
3851

3952
sort.Sort(mounts(mnts))
40-
return mnts, nil
53+
return mnts, cleanups.Release(), nil
4154
}
4255

4356
// setBindModeIfNull is platform specific processing which is a no-op on

docs/api/version-history.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ keywords: "API, Docker, rcli, REST, documentation"
1717

1818
[Docker Engine API v1.45](https://docs.docker.com/engine/api/v1.45/) documentation
1919

20+
* `POST /containers/create` now supports `VolumeOptions.Subpath` which allows a
21+
subpath of a named volume to be mounted.
22+
2023
## v1.44 API changes
2124

2225
[Docker Engine API v1.44](https://docs.docker.com/engine/api/v1.44/) documentation

integration/internal/container/container.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,28 @@ func Inspect(ctx context.Context, t *testing.T, apiClient client.APIClient, cont
170170

171171
return c
172172
}
173+
174+
type ContainerOutput struct {
175+
Stdout, Stderr string
176+
}
177+
178+
// Output waits for the container to end running and returns its output.
179+
func Output(ctx context.Context, client client.APIClient, id string) (ContainerOutput, error) {
180+
logs, err := client.ContainerLogs(ctx, id, container.LogsOptions{Follow: true, ShowStdout: true, ShowStderr: true})
181+
if err != nil {
182+
return ContainerOutput{}, err
183+
}
184+
185+
defer logs.Close()
186+
187+
var stdoutBuf, stderrBuf bytes.Buffer
188+
_, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, logs)
189+
if err != nil {
190+
return ContainerOutput{}, err
191+
}
192+
193+
return ContainerOutput{
194+
Stdout: stdoutBuf.String(),
195+
Stderr: stderrBuf.String(),
196+
}, nil
197+
}

0 commit comments

Comments
 (0)