volume: add support for non-volatile upperdir,workdir for overlay volumes

Often users want their overlayed volumes to be `non-volatile` in nature
that means that same `upper` dir can be re-used by one or more
containers but overall of nature of volumes still have to be `overlay`
so work done is still on a overlay not on the actual volume.

Following PR adds support for more advanced options i.e custom `workdir`
and `upperdir` for overlayed volumes. So that users can re-use `workdir`
and `upperdir` across new containers as well.

Usage
```console

$ podman run -it -v myvol:/data:O,upperdir=/path/persistant/upper,workdir=/path/persistant/work alpine sh

```

Signed-off-by: Aditya R <arajan@redhat.com>
This commit is contained in:
Aditya Rajan 2021-12-28 16:02:25 +05:30 committed by Aditya R
parent 5b01dab618
commit e64e6500d3
No known key found for this signature in database
GPG Key ID: 8E5A8A19DF7C8673
4 changed files with 109 additions and 3 deletions

View File

@ -1394,6 +1394,10 @@ directory will be the lower, and the container storage directory will be the
upper. Modifications to the mount point are destroyed when the container upper. Modifications to the mount point are destroyed when the container
finishes executing, similar to a tmpfs mount point being unmounted. finishes executing, similar to a tmpfs mount point being unmounted.
For advanced users overlay option also supports custom non-volatile `upperdir` and `workdir`
for the overlay mount. Custom `upperdir` and `workdir` can be fully managed by the users themselves
and `podman` will not remove it on lifecycle completion. Example `:O,upperdir=/some/upper,workdir=/some/work`
Subsequent executions of the container will see the original source directory Subsequent executions of the container will see the original source directory
content, any changes from previous container executions no longer exist. content, any changes from previous container executions no longer exist.

View File

@ -391,18 +391,52 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
} }
overlayFlag := false overlayFlag := false
upperDir := ""
workDir := ""
for _, o := range namedVol.Options { for _, o := range namedVol.Options {
if o == "O" { if o == "O" {
overlayFlag = true overlayFlag = true
} }
if overlayFlag && strings.Contains(o, "upperdir") {
splitOpt := strings.SplitN(o, "=", 2)
if len(splitOpt) > 1 {
upperDir = splitOpt[1]
if upperDir == "" {
return nil, errors.New("cannot accept empty value for upperdir")
}
}
}
if overlayFlag && strings.Contains(o, "workdir") {
splitOpt := strings.SplitN(o, "=", 2)
if len(splitOpt) > 1 {
workDir = splitOpt[1]
if workDir == "" {
return nil, errors.New("cannot accept empty value for workdir")
}
}
}
} }
if overlayFlag { if overlayFlag {
var overlayMount spec.Mount
var overlayOpts *overlay.Options
contentDir, err := overlay.TempDir(c.config.StaticDir, c.RootUID(), c.RootGID()) contentDir, err := overlay.TempDir(c.config.StaticDir, c.RootUID(), c.RootGID())
if err != nil { if err != nil {
return nil, err return nil, err
} }
overlayMount, err := overlay.Mount(contentDir, mountPoint, namedVol.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions())
if (upperDir != "" && workDir == "") || (upperDir == "" && workDir != "") {
return nil, errors.Wrapf(err, "must specify both upperdir and workdir")
}
overlayOpts = &overlay.Options{RootUID: c.RootUID(),
RootGID: c.RootGID(),
UpperDirOptionFragment: upperDir,
WorkDirOptionFragment: workDir,
GraphOpts: c.runtime.store.GraphOptions(),
}
overlayMount, err = overlay.MountWithOptions(contentDir, mountPoint, namedVol.Dest, overlayOpts)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "mounting overlay failed %q", mountPoint) return nil, errors.Wrapf(err, "mounting overlay failed %q", mountPoint)
} }

View File

@ -25,16 +25,30 @@ type defaultMountOptions struct {
// The sourcePath variable, if not empty, contains a bind mount source. // The sourcePath variable, if not empty, contains a bind mount source.
func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, error) { func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, error) {
var ( var (
foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU bool foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU, foundOverlay bool
) )
newOptions := make([]string, 0, len(options)) newOptions := make([]string, 0, len(options))
for _, opt := range options { for _, opt := range options {
// Some options have parameters - size, mode // Some options have parameters - size, mode
splitOpt := strings.SplitN(opt, "=", 2) splitOpt := strings.SplitN(opt, "=", 2)
// add advanced options such as upperdir=/path and workdir=/path, when overlay is specified
if foundOverlay {
if strings.Contains(opt, "upperdir") {
newOptions = append(newOptions, opt)
continue
}
if strings.Contains(opt, "workdir") {
newOptions = append(newOptions, opt)
continue
}
}
switch splitOpt[0] { switch splitOpt[0] {
case "idmap":
case "O": case "O":
foundOverlay = true
case "idmap":
if len(options) > 1 { if len(options) > 1 {
return nil, errors.Wrapf(ErrDupeMntOption, "'O' option can not be used with other options") return nil, errors.Wrapf(ErrDupeMntOption, "'O' option can not be used with other options")
} }

View File

@ -260,6 +260,60 @@ var _ = Describe("Podman run with volumes", func() {
}) })
It("podman support overlay on named volume with custom upperdir and workdir", func() {
SkipIfRemote("Overlay volumes only work locally")
if os.Getenv("container") != "" {
Skip("Overlay mounts not supported when running in a container")
}
if rootless.IsRootless() {
if _, err := exec.LookPath("fuse-overlayfs"); err != nil {
Skip("Fuse-Overlayfs required for rootless overlay mount test")
}
}
// create persistent upperdir on host
upperDir := filepath.Join(tempdir, "upper")
err := os.Mkdir(upperDir, 0755)
Expect(err).To(BeNil(), "mkdir "+upperDir)
// create persistent workdir on host
workDir := filepath.Join(tempdir, "work")
err = os.Mkdir(workDir, 0755)
Expect(err).To(BeNil(), "mkdir "+workDir)
overlayOpts := fmt.Sprintf("upperdir=%s,workdir=%s", upperDir, workDir)
session := podmanTest.Podman([]string{"volume", "create", "myvolume"})
session.WaitWithDefaultTimeout()
volName := session.OutputToString()
Expect(session).Should(Exit(0))
// create file on actual volume
session = podmanTest.Podman([]string{"run", "--volume", volName + ":/data", ALPINE, "sh", "-c", "echo hello >> " + "/data/test"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
// create file on overlay volume
session = podmanTest.Podman([]string{"run", "--volume", volName + ":/data:O," + overlayOpts, ALPINE, "sh", "-c", "echo hello >> " + "/data/overlay"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
session = podmanTest.Podman([]string{"run", "--volume", volName + ":/data:O," + overlayOpts, ALPINE, "sh", "-c", "ls /data"})
session.WaitWithDefaultTimeout()
// must contain `overlay` file since it should be persistent on specified upper and workdir
Expect(session.OutputToString()).To(ContainSubstring("overlay"))
// this should be there since `test` was written on actual volume not on any overlay
Expect(session.OutputToString()).To(ContainSubstring("test"))
session = podmanTest.Podman([]string{"run", "--volume", volName + ":/data:O", ALPINE, "sh", "-c", "ls /data"})
session.WaitWithDefaultTimeout()
// must not contain `overlay` file which was on custom upper and workdir since we have not specified any upper or workdir
Expect(session.OutputToString()).To(Not(ContainSubstring("overlay")))
// this should be there since `test` was written on actual volume not on any overlay
Expect(session.OutputToString()).To(ContainSubstring("test"))
})
It("podman run with noexec can't exec", func() { It("podman run with noexec can't exec", func() {
session := podmanTest.Podman([]string{"run", "--rm", "-v", "/bin:/hostbin:noexec", ALPINE, "/hostbin/ls", "/"}) session := podmanTest.Podman([]string{"run", "--rm", "-v", "/bin:/hostbin:noexec", ALPINE, "/hostbin/ls", "/"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()