Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/markdown/podman-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
content, any changes from previous container executions no longer exist.

Expand Down
36 changes: 35 additions & 1 deletion libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,18 +391,52 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
}

overlayFlag := false
upperDir := ""
workDir := ""
for _, o := range namedVol.Options {
if o == "O" {
overlayFlag = true
}
if overlayFlag && strings.Contains(o, "upperdir") {
splitOpt := strings.SplitN(o, "=", 2)
if len(splitOpt) > 1 {
upperDir = splitOpt[1]
Copy link
Member

@rhatdan rhatdan Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you check for duplicates?

if upperDir != "" {
        return ERROR
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ignore in underlying library if upperdir is empty. But I agree user should get early error so added here.

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]
flouthoc marked this conversation as resolved.
Show resolved Hide resolved
if workDir == "" {
return nil, errors.New("cannot accept empty value for workdir")
}
}
}
}

if overlayFlag {
var overlayMount spec.Mount
var overlayOpts *overlay.Options
contentDir, err := overlay.TempDir(c.config.StaticDir, c.RootUID(), c.RootGID())
if err != nil {
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 {
return nil, errors.Wrapf(err, "mounting overlay failed %q", mountPoint)
}
Expand Down
18 changes: 16 additions & 2 deletions pkg/util/mountOpts.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,30 @@ type defaultMountOptions struct {
// The sourcePath variable, if not empty, contains a bind mount source.
func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, error) {
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))
for _, opt := range options {
// Some options have parameters - size, mode
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] {
case "idmap":
case "O":
foundOverlay = true
case "idmap":
if len(options) > 1 {
return nil, errors.Wrapf(ErrDupeMntOption, "'O' option can not be used with other options")
}
Expand Down
54 changes: 54 additions & 0 deletions test/e2e/run_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test two volume mounts, one without overlayOpts and one without, and make sure the one without is not perment

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes added another mount with just overlay and no custom upper/workdir which tests that content is not persisted.

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() {
session := podmanTest.Podman([]string{"run", "--rm", "-v", "/bin:/hostbin:noexec", ALPINE, "/hostbin/ls", "/"})
session.WaitWithDefaultTimeout()
Expand Down