Skip to content

Commit

Permalink
Add U volume flag to chown source volumes
Browse files Browse the repository at this point in the history
Signed-off-by: Eduardo Vega <edvegavalerio@gmail.com>
  • Loading branch information
EduardoVega committed Feb 23, 2021
1 parent 96fc9d9 commit 874f232
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 8 deletions.
9 changes: 9 additions & 0 deletions docs/source/markdown/podman-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ The _options_ is a comma delimited list and can be:
* [**no**]**dev**
* [**no**]**suid**
* [**O**]
* [**U**]

The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The volume
will be mounted into the container at this directory.
Expand Down Expand Up @@ -1065,6 +1066,14 @@ You can add `:ro` or `:rw` suffix to a volume to mount it read-only or
read-write mode, respectively. By default, the volumes are mounted read-write.
See examples.

`Chowning Volume Mounts`

By default, Podman does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host.

The `:U` suffix tells Podman to use the correct host UID and GID based on the UID and GID within the container, to change recursively the owner and group of the source volume.

**Warning** use with caution since this will modify the host filesystem.

`Labeling Volume Mounts`

Labeling systems like SELinux require that proper labels are placed on volume
Expand Down
11 changes: 11 additions & 0 deletions docs/source/markdown/podman-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ The _options_ is a comma delimited list and can be: <sup>[[1]](#Footnote1)</sup>
* [**no**]**dev**
* [**no**]**suid**
* [**O**]
* [**U**]

The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The volume
will be mounted into the container at this directory.
Expand Down Expand Up @@ -1139,6 +1140,14 @@ container.
You can add **:ro** or **:rw** option to mount a volume in read-only or
read-write mode, respectively. By default, the volumes are mounted read-write.

`Chowning Volume Mounts`

By default, Podman does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host.

The `:U` suffix tells Podman to use the correct host UID and GID based on the UID and GID within the container, to change recursively the owner and group of the source volume.

**Warning** use with caution since this will modify the host filesystem.

`Labeling Volume Mounts`

Labeling systems like SELinux require that proper labels are placed on volume
Expand Down Expand Up @@ -1450,6 +1459,8 @@ $ podman run -v /var/db:/data1 -i -t fedora bash
$ podman run -v data:/data2 -i -t fedora bash
$ podman run -v /var/cache/dnf:/var/cache/dnf:O -ti fedora dnf -y update
$ podman run -d -e MYSQL_ROOT_PASSWORD=root --user mysql --userns=keep-id -v ~/data:/var/lib/mysql:z,U mariadb
```

Using **--mount** flags to mount a host directory as a container folder, specify
Expand Down
2 changes: 2 additions & 0 deletions libpod/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ type ContainerOverlayVolume struct {
Dest string `json:"dest"`
// Source specifies the source path of the mount.
Source string `json:"source,omitempty"`
// Options holds overlay volume options.
Options []string `json:"options,omitempty"`
}

// ContainerImageVolume is a volume based on a container image. The container
Expand Down
36 changes: 34 additions & 2 deletions libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/pkg/overlay"
butil "github.com/containers/buildah/util"
"github.com/containers/common/pkg/apparmor"
"github.com/containers/common/pkg/chown"
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/subscriptions"
"github.com/containers/common/pkg/umask"
Expand Down Expand Up @@ -356,13 +358,28 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
return nil, err
}

// Check if the spec file mounts contain the label Relabel flags z or Z.
// If they do, relabel the source directory and then remove the option.
// Get host UID and GID based on the container process UID and GID.
hostUID, hostGID, err := butil.GetHostIDs(util.IDtoolsToRuntimeSpec(c.config.IDMappings.UIDMap), util.IDtoolsToRuntimeSpec(c.config.IDMappings.GIDMap), uint32(execUser.Uid), uint32(execUser.Gid))
if err != nil {
return nil, err
}

// Check if the spec file mounts contain the options z, Z or U.
// If they have z or Z, relabel the source directory and then remove the option.
// If they have U, chown the source directory and them remove the option.
for i := range g.Config.Mounts {
m := &g.Config.Mounts[i]
var options []string
for _, o := range m.Options {
switch o {
case "U":
if m.Type == "tmpfs" {
options = append(options, []string{fmt.Sprintf("uid=%d", execUser.Uid), fmt.Sprintf("gid=%d", execUser.Gid)}...)
} else {
if err := chown.ChangeHostPathOwnership(m.Source, true, int(hostUID), int(hostGID)); err != nil {
return nil, err
}
}
case "z":
fallthrough
case "Z":
Expand Down Expand Up @@ -427,6 +444,21 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
if err != nil {
return nil, errors.Wrapf(err, "mounting overlay failed %q", overlayVol.Source)
}

// Check overlay volume options
for _, o := range overlayVol.Options {
switch o {
case "U":
if err := chown.ChangeHostPathOwnership(overlayVol.Source, true, int(hostUID), int(hostGID)); err != nil {
return nil, err
}

if err := chown.ChangeHostPathOwnership(contentDir, true, int(hostUID), int(hostGID)); err != nil {
return nil, err
}
}
}

g.AddMount(overlayMount)
}

Expand Down
5 changes: 3 additions & 2 deletions libpod/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -1429,8 +1429,9 @@ func WithOverlayVolumes(volumes []*ContainerOverlayVolume) CtrCreateOption {

for _, vol := range volumes {
ctr.config.OverlayVolumes = append(ctr.config.OverlayVolumes, &ContainerOverlayVolume{
Dest: vol.Dest,
Source: vol.Source,
Dest: vol.Dest,
Source: vol.Source,
Options: vol.Options,
})
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/specgen/generate/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,9 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
var vols []*libpod.ContainerOverlayVolume
for _, v := range overlays {
vols = append(vols, &libpod.ContainerOverlayVolume{
Dest: v.Destination,
Source: v.Source,
Dest: v.Destination,
Source: v.Source,
Options: v.Options,
})
}
options = append(options, libpod.WithOverlayVolumes(vols))
Expand Down
13 changes: 12 additions & 1 deletion pkg/specgen/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type OverlayVolume struct {
Destination string `json:"destination"`
// Source specifies the source path of the mount.
Source string `json:"source,omitempty"`
// Options holds overlay volume options.
Options []string `json:"options,omitempty"`
}

// ImageVolume is a volume based on a container image. The container image is
Expand Down Expand Up @@ -100,10 +102,17 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na
if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") {
// This is not a named volume
overlayFlag := false
chownFlag := false
for _, o := range options {
if o == "O" {
overlayFlag = true
if len(options) > 1 {

joinedOpts := strings.Join(options, "")
if strings.Contains(joinedOpts, "U") {
chownFlag = true
}

if len(options) > 2 || (len(options) == 2 && !chownFlag) {
return nil, nil, nil, errors.New("can't use 'O' with other options")
}
}
Expand All @@ -113,6 +122,8 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na
newOverlayVol := new(OverlayVolume)
newOverlayVol.Destination = cleanDest
newOverlayVol.Source = src
newOverlayVol.Options = options

if _, ok := overlayVolumes[newOverlayVol.Destination]; ok {
return nil, nil, nil, errors.Wrapf(errDuplicateDest, newOverlayVol.Destination)
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/util/mountOpts.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ 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 bool
foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU bool
)

newOptions := make([]string, 0, len(options))
Expand Down Expand Up @@ -116,6 +116,11 @@ func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string
return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'z' and 'Z' can be used")
}
foundZ = true
case "U":
if foundU {
return nil, errors.Wrapf(ErrDupeMntOption, "the 'U' option can only be set once")
}
foundU = true
default:
return nil, errors.Wrapf(ErrBadMntOption, "unknown mount option %q", opt)
}
Expand Down
14 changes: 14 additions & 0 deletions pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/containers/storage"
"github.com/containers/storage/pkg/idtools"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
Expand Down Expand Up @@ -692,3 +693,16 @@ func CoresToPeriodAndQuota(cores float64) (uint64, int64) {
func PeriodAndQuotaToCores(period uint64, quota int64) float64 {
return float64(quota) / float64(period)
}

// IDtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
func IDtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
for _, idmap := range idMaps {
tempIDMap := specs.LinuxIDMapping{
ContainerID: uint32(idmap.ContainerID),
HostID: uint32(idmap.HostID),
Size: uint32(idmap.Size),
}
convertedIDMap = append(convertedIDMap, tempIDMap)
}
return convertedIDMap
}
53 changes: 53 additions & 0 deletions test/e2e/run_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package integration

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"

Expand Down Expand Up @@ -590,4 +592,55 @@ VOLUME /test/`
Expect(session.ExitCode()).To(Equal(0))
Expect(len(session.OutputToStringArray())).To(Equal(2))
})

It("podman run with U volume flag", func() {
SkipIfRemote("Overlay volumes only work locally")

u, err := user.Current()
Expect(err).To(BeNil())
name := u.Username
if name == "root" {
name = "containers"
}

content, err := ioutil.ReadFile("/etc/subuid")
if err != nil {
Skip("cannot read /etc/subuid")
}
if !strings.Contains(string(content), name) {
Skip("cannot find mappings for the current user")
}

if os.Getenv("container") != "" {
Skip("Overlay mounts not supported when running in a container")
}
if rootless.IsRootless() {
if _, err := exec.LookPath("fuse_overlay"); err != nil {
Skip("Fuse-Overlayfs required for rootless overlay mount test")
}
}

mountPath := filepath.Join(podmanTest.TempDir, "secrets")
os.Mkdir(mountPath, 0755)
vol := mountPath + ":" + dest + ":U"

session := podmanTest.Podman([]string{"run", "--rm", "--user", "888:888", "-v", vol, ALPINE, "stat", "-c", "%u:%g", dest})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
found, _ := session.GrepString("888:888")
Expect(found).Should(BeTrue())

session = podmanTest.Podman([]string{"run", "--rm", "--user", "888:888", "--userns", "auto", "-v", vol, ALPINE, "stat", "-c", "%u:%g", dest})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
found, _ = session.GrepString("888:888")
Expect(found).Should(BeTrue())

vol = vol + ",O"
session = podmanTest.Podman([]string{"run", "--rm", "--user", "888:888", "--userns", "keep-id", "-v", vol, ALPINE, "stat", "-c", "%u:%g", dest})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
found, _ = session.GrepString("888:888")
Expect(found).Should(BeTrue())
})
})
Loading

0 comments on commit 874f232

Please sign in to comment.