From 52c1365f32398b8ba0321c159e739a5416cd9ab2 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Fri, 21 Sep 2018 06:29:18 -0400 Subject: Add --mount option for `create` & `run` command Signed-off-by: Kunal Kushwaha Signed-off-by: Daniel J Walsh Closes: #1524 Approved by: mheon --- cmd/podman/common.go | 4 ++ cmd/podman/create.go | 6 +++ cmd/podman/create_cli.go | 90 ++++++++++++++++++++++++++++++++++++++ completions/bash/podman | 1 + docs/podman-create.1.md | 30 +++++++++++++ docs/podman-run.1.md | 38 ++++++++++++++++ libpod/container_internal.go | 9 ++++ libpod/container_internal_linux.go | 7 +++ pkg/spec/createconfig.go | 76 +++++++++++++++++--------------- pkg/spec/spec.go | 64 ++++++++++++++++----------- test/e2e/run_test.go | 39 +++++++++++++++++ 11 files changed, 303 insertions(+), 61 deletions(-) diff --git a/cmd/podman/common.go b/cmd/podman/common.go index 8d20081f6..9ab0e57e5 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -417,6 +417,10 @@ var createFlags = []cli.Flag{ Name: "uts", Usage: "UTS namespace to use", }, + cli.StringSliceFlag{ + Name: "mount", + Usage: "Attach a filesystem mount to the container (default [])", + }, cli.StringSliceFlag{ Name: "volume, v", Usage: "Bind mount a volume into the container (default [])", diff --git a/cmd/podman/create.go b/cmd/podman/create.go index ff912560b..fc0c71536 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -24,6 +24,7 @@ import ( "github.com/docker/docker/pkg/signal" "github.com/docker/go-connections/nat" "github.com/docker/go-units" + spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -459,6 +460,10 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim } blkioWeight = uint16(u) } + var mountList []spec.Mount + if mountList, err = parseMounts(c.StringSlice("mount")); err != nil { + return nil, err + } if err = parseVolumes(c.StringSlice("volume")); err != nil { return nil, err @@ -772,6 +777,7 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim Tty: tty, User: user, UsernsMode: usernsMode, + Mounts: mountList, Volumes: c.StringSlice("volume"), WorkDir: workDir, Rootfs: rootfs, diff --git a/cmd/podman/create_cli.go b/cmd/podman/create_cli.go index 812b62058..218e9b806 100644 --- a/cmd/podman/create_cli.go +++ b/cmd/podman/create_cli.go @@ -8,6 +8,8 @@ import ( cc "github.com/containers/libpod/pkg/spec" "github.com/docker/docker/pkg/sysinfo" + "github.com/docker/go-units" + spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -74,6 +76,94 @@ func addWarning(warnings []string, msg string) []string { return append(warnings, msg) } +// Format supported. +// podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ... +// podman run --mount type=tmpfs,target=/dev/shm .. +func parseMounts(mounts []string) ([]spec.Mount, error) { + var mountList []spec.Mount + errInvalidSyntax := errors.Errorf("incorrect mount format : should be --mount type=,[src=,]target=,[options]") + for _, mount := range mounts { + var tokenCount int + var mountInfo spec.Mount + + arr := strings.SplitN(mount, ",", 2) + if len(arr) < 2 { + return nil, errInvalidSyntax + } + kv := strings.Split(arr[0], "=") + if kv[0] != "type" { + return nil, errInvalidSyntax + } + switch kv[1] { + case "bind": + mountInfo.Type = string(cc.TypeBind) + case "tmpfs": + mountInfo.Type = string(cc.TypeTmpfs) + mountInfo.Source = string(cc.TypeTmpfs) + mountInfo.Options = append(mountInfo.Options, []string{"rprivate", "noexec", "nosuid", "nodev", "size=65536k"}...) + + default: + return nil, errors.Errorf("invalid filesystem type %q", kv[1]) + } + + tokens := strings.Split(arr[1], ",") + for i, val := range tokens { + if i == (tokenCount - 1) { + //Parse tokens before options. + break + } + kv := strings.Split(val, "=") + switch kv[0] { + case "ro", "nosuid", "nodev", "noexec": + mountInfo.Options = append(mountInfo.Options, kv[0]) + case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z": + if mountInfo.Type != "bind" { + return nil, errors.Errorf("%s can only be used with bind mounts", kv[0]) + } + mountInfo.Options = append(mountInfo.Options, kv[0]) + case "tmpfs-mode": + if mountInfo.Type != "tmpfs" { + return nil, errors.Errorf("%s can only be used with tmpfs mounts", kv[0]) + } + mountInfo.Options = append(mountInfo.Options, fmt.Sprintf("mode=%s", kv[1])) + case "tmpfs-size": + if mountInfo.Type != "tmpfs" { + return nil, errors.Errorf("%s can only be used with tmpfs mounts", kv[0]) + } + shmSize, err := units.FromHumanSize(kv[1]) + if err != nil { + return nil, errors.Wrapf(err, "unable to translate tmpfs-size") + } + + mountInfo.Options = append(mountInfo.Options, fmt.Sprintf("size=%d", shmSize)) + + case "bind-propagation": + if mountInfo.Type != "bind" { + return nil, errors.Errorf("%s can only be used with bind mounts", kv[0]) + } + mountInfo.Options = append(mountInfo.Options, kv[1]) + case "src", "source": + if mountInfo.Type == "tmpfs" { + return nil, errors.Errorf("can not use src= on a tmpfs file system") + } + if err := validateVolumeHostDir(kv[1]); err != nil { + return nil, err + } + mountInfo.Source = kv[1] + case "target", "dst", "destination": + if err := validateVolumeCtrDir(kv[1]); err != nil { + return nil, err + } + mountInfo.Destination = kv[1] + default: + return nil, errors.Errorf("incorrect mount option : %s", kv[0]) + } + } + mountList = append(mountList, mountInfo) + } + return mountList, nil +} + func parseVolumes(volumes []string) error { for _, volume := range volumes { arr := strings.SplitN(volume, ":", 3) diff --git a/completions/bash/podman b/completions/bash/podman index de535512f..b97c4b0d5 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -945,6 +945,7 @@ _podman_build() { --userns-uid-map-user --userns-gid-map-group --uts + --mount --volume -v " diff --git a/docs/podman-create.1.md b/docs/podman-create.1.md index 01e072005..c42671b76 100644 --- a/docs/podman-create.1.md +++ b/docs/podman-create.1.md @@ -372,6 +372,36 @@ unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. +**--mount**=*type=TYPE,TYPE-SPECIFIC-OPTION[,...]* + +Attach a filesystem mount to the container + +Current supported mount TYPES are bind, and tmpfs. + + e.g. + + type=bind,source=/path/on/host,destination=/path/in/container + + type=tmpfs,tmpfs-size=512M,destination=/path/in/container + + Common Options: + + · src, source: mount source spec for bind and volume. Mandatory for bind. + + · dst, destination, target: mount destination spec. + + · ro, read-only: true or false (default). + + Options specific to bind: + + · bind-propagation: shared, slave, private, rshared, rslave, or rprivate(default). See also mount(2). + + Options specific to tmpfs: + + · tmpfs-size: Size of the tmpfs mount in bytes. Unlimited by default in Linux. + + · tmpfs-mode: File mode of the tmpfs in octal. (e.g. 700 or 0700.) Defaults to 1777 in Linux. + **--name**="" Assign a name to the container diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md index a4c47f5de..fccebb7f7 100644 --- a/docs/podman-run.1.md +++ b/docs/podman-run.1.md @@ -655,6 +655,36 @@ Set the UTS mode for the container **NOTE**: the host mode gives the container access to changing the host's hostname and is therefore considered insecure. +**--mount**=*type=TYPE,TYPE-SPECIFIC-OPTION[,...]* + +Attach a filesystem mount to the container + +Current supported mount TYPES are bind, and tmpfs. + + e.g. + + type=bind,source=/path/on/host,destination=/path/in/container + + type=tmpfs,tmpfs-size=512M,destination=/path/in/container + + Common Options: + + · src, source: mount source spec for bind and volume. Mandatory for bind. + + · dst, destination, target: mount destination spec. + + · ro, read-only: true or false (default). + + Options specific to bind: + + · bind-propagation: Z, z, shared, slave, private, rshared, rslave, or rprivate(default). See also mount(2). + + Options specific to tmpfs: + + · tmpfs-size: Size of the tmpfs mount in bytes. Unlimited by default in Linux. + + · tmpfs-mode: File mode of the tmpfs in octal. (e.g. 700 or 0700.) Defaults to 1777 in Linux. + **-v**|**--volume**[=*[HOST-DIR:CONTAINER-DIR[:OPTIONS]]*] Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, podman @@ -931,6 +961,12 @@ colon: $ podman run -v /var/db:/data1 -i -t fedora bash ``` +Using --mount flags, To mount a host directory as a container folder, specify +the absolute path to the directory and the absolute path for the container +directory: + +$ podman run --mount type=bind,src=/var/db,target=/data1 busybox sh + When using SELinux, be aware that the host has no knowledge of container SELinux policy. Therefore, in the above example, if SELinux policy is enforced, the `/var/db` directory is not writable to the container. A "Permission Denied" @@ -1030,6 +1066,8 @@ $ podman run --uidmap 0:30000:7000 --gidmap 0:30000:7000 fedora echo hello subgid(5), subuid(5), libpod.conf(5) ## HISTORY +September 2018, updated by Kunal Kushwaha + October 2017, converted from Docker documentation to podman by Dan Walsh for podman November 2015, updated by Sally O'Malley diff --git a/libpod/container_internal.go b/libpod/container_internal.go index e5e871d6f..c88794212 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -926,6 +926,9 @@ func (c *Container) makeBindMounts() error { if err != nil { return errors.Wrapf(err, "error creating resolv.conf for container %s", c.ID()) } + if err = label.Relabel(newResolv, c.config.MountLabel, false); err != nil { + return errors.Wrapf(err, "error relabeling %q for container %q", newResolv, c.ID) + } c.state.BindMounts["/etc/resolv.conf"] = newResolv // Make /etc/hosts @@ -937,6 +940,9 @@ func (c *Container) makeBindMounts() error { if err != nil { return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) } + if err = label.Relabel(newHosts, c.config.MountLabel, false); err != nil { + return errors.Wrapf(err, "error relabeling %q for container %q", newHosts, c.ID) + } c.state.BindMounts["/etc/hosts"] = newHosts // Make /etc/hostname @@ -946,6 +952,9 @@ func (c *Container) makeBindMounts() error { if err != nil { return errors.Wrapf(err, "error creating hostname file for container %s", c.ID()) } + if err = label.Relabel(hostnamePath, c.config.MountLabel, false); err != nil { + return errors.Wrapf(err, "error relabeling %q for container %q", hostnamePath, c.ID) + } c.state.BindMounts["/etc/hostname"] = hostnamePath } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b77beaf64..553a612b3 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -283,6 +283,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { mounts := sortMounts(g.Mounts()) g.ClearMounts() for _, m := range mounts { + switch m.Type { + case "tmpfs", "devpts": + o := label.FormatMountLabel("", c.config.MountLabel) + if o != "" { + m.Options = append(m.Options, o) + } + } g.AddMount(m) } return g.Config, nil diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index a9c7d2967..887ef8e95 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -122,6 +122,7 @@ type CreateConfig struct { UsernsMode namespaces.UsernsMode //userns User string //user UtsMode namespaces.UTSMode //uts + Mounts []spec.Mount //mounts Volumes []string //volume VolumesFrom []string WorkDir string //workdir @@ -142,54 +143,59 @@ func (c *CreateConfig) CreateBlockIO() (*spec.LinuxBlockIO, error) { return c.createBlockIO() } +func processOptions(options []string) []string { + var ( + foundrw, foundro bool + rootProp string + ) + options = append(options, "rbind") + for _, opt := range options { + switch opt { + case "rw": + foundrw = true + case "ro": + foundro = true + case "private", "rprivate", "slave", "rslave", "shared", "rshared": + rootProp = opt + } + } + if !foundrw && !foundro { + options = append(options, "rw") + } + if rootProp == "" { + options = append(options, "rprivate") + } + return options +} + +func (c *CreateConfig) initFSMounts() []spec.Mount { + var mounts []spec.Mount + for _, m := range c.Mounts { + m.Options = processOptions(m.Options) + if m.Type == "tmpfs" { + m.Options = append(m.Options, "tmpcopyup") + } else { + mounts = append(mounts, m) + } + } + return mounts +} + //GetVolumeMounts takes user provided input for bind mounts and creates Mount structs func (c *CreateConfig) GetVolumeMounts(specMounts []spec.Mount) ([]spec.Mount, error) { var m []spec.Mount for _, i := range c.Volumes { - var ( - options []string - foundrw, foundro, foundz, foundZ bool - rootProp string - ) - - // We need to handle SELinux options better here, specifically :Z + var options []string spliti := strings.Split(i, ":") if len(spliti) > 2 { options = strings.Split(spliti[2], ",") } - options = append(options, "rbind") - for _, opt := range options { - switch opt { - case "rw": - foundrw = true - case "ro": - foundro = true - case "z": - foundz = true - case "Z": - foundZ = true - case "private", "rprivate", "slave", "rslave", "shared", "rshared": - rootProp = opt - } - } - if !foundrw && !foundro { - options = append(options, "rw") - } - if foundz { - options = append(options, "z") - } - if foundZ { - options = append(options, "Z") - } - if rootProp == "" { - options = append(options, "rprivate") - } m = append(m, spec.Mount{ Destination: spliti[1], Type: string(TypeBind), Source: spliti[0], - Options: options, + Options: processOptions(options), }) logrus.Debugf("User mount %s:%s options %v", spliti[0], spliti[1], options) diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index e115bba7f..ad14ea65d 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -18,6 +18,34 @@ import ( const cpuPeriod = 100000 +func supercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.Mount { + if len(mounts) > 0 { + // If we have overlappings mounts, remove them from the spec in favor of + // the user-added volume mounts + destinations := make(map[string]bool) + for _, mount := range mounts { + destinations[path.Clean(mount.Destination)] = true + } + // Copy all mounts from spec to defaultMounts, except for + // - mounts overridden by a user supplied mount; + // - all mounts under /dev if a user supplied /dev is present; + mountDev := destinations["/dev"] + for _, mount := range configMount { + if _, ok := destinations[path.Clean(mount.Destination)]; !ok { + if mountDev && strings.HasPrefix(mount.Destination, "/dev/") { + // filter out everything under /dev if /dev is user-mounted + continue + } + + logrus.Debugf("Adding mount %s", mount.Destination) + mounts = append(mounts, mount) + } + } + return mounts + } + return configMount +} + // CreateConfigToOCISpec parses information needed to create a container into an OCI runtime spec func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint cgroupPerm := "ro" @@ -246,6 +274,12 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint g.AddMount(tmpfsMnt) } + for _, m := range config.Mounts { + if m.Type == "tmpfs" { + g.AddMount(m) + } + } + for name, val := range config.Env { g.AddProcessEnv(name, val) } @@ -305,36 +339,14 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint return nil, errors.Wrap(err, "error getting volume mounts from --volumes-from flag") } - mounts, err := config.GetVolumeMounts(configSpec.Mounts) + volumeMounts, err := config.GetVolumeMounts(configSpec.Mounts) if err != nil { return nil, errors.Wrapf(err, "error getting volume mounts") } - if len(mounts) > 0 { - // If we have overlappings mounts, remove them from the spec in favor of - // the user-added volume mounts - destinations := make(map[string]bool) - for _, mount := range mounts { - destinations[path.Clean(mount.Destination)] = true - } - - // Copy all mounts from spec to defaultMounts, except for - // - mounts overridden by a user supplied mount; - // - all mounts under /dev if a user supplied /dev is present; - mountDev := destinations["/dev"] - for _, mount := range configSpec.Mounts { - if _, ok := destinations[path.Clean(mount.Destination)]; !ok { - if mountDev && strings.HasPrefix(mount.Destination, "/dev/") { - // filter out everything under /dev if /dev is user-mounted - continue - } - - logrus.Debugf("Adding mount %s", mount.Destination) - mounts = append(mounts, mount) - } - } - configSpec.Mounts = mounts - } + configSpec.Mounts = supercedeUserMounts(volumeMounts, configSpec.Mounts) + //--mount + configSpec.Mounts = supercedeUserMounts(config.initFSMounts(), configSpec.Mounts) if canAddResources { // BLOCK IO blkio, err := config.CreateBlockIO() diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 3d487db66..baaca6333 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -234,6 +234,32 @@ var _ = Describe("Podman run", func() { Expect(session.OutputToString()).To(ContainSubstring("/run/test rw,relatime, shared")) }) + It("podman run with mount flag", func() { + mountPath := filepath.Join(podmanTest.TempDir, "secrets") + os.Mkdir(mountPath, 0755) + session := podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=bind,src=%s,target=/run/test", mountPath), ALPINE, "grep", "/run/test", "/proc/self/mountinfo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("/run/test rw")) + + session = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=bind,src=%s,target=/run/test,ro", mountPath), ALPINE, "grep", "/run/test", "/proc/self/mountinfo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("/run/test ro")) + + session = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=bind,src=%s,target=/run/test,shared", mountPath), ALPINE, "grep", "/run/test", "/proc/self/mountinfo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("/run/test rw,relatime shared")) + + mountPath = filepath.Join(podmanTest.TempDir, "scratchpad") + os.Mkdir(mountPath, 0755) + session = podmanTest.Podman([]string{"run", "--rm", "--mount", "type=tmpfs,target=/run/test", ALPINE, "grep", "/run/test", "/proc/self/mountinfo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("/run/test rw,nosuid,nodev,noexec,relatime - tmpfs")) + }) + It("podman run with cidfile", func() { session := podmanTest.Podman([]string{"run", "--cidfile", tempdir + "cidfile", ALPINE, "ls"}) session.WaitWithDefaultTimeout() @@ -565,6 +591,19 @@ USER mail` Expect(session.ExitCode()).To(Equal(0)) }) + It("podman run --mount flag with multiple mounts", func() { + vol1 := filepath.Join(podmanTest.TempDir, "vol-test1") + err := os.MkdirAll(vol1, 0755) + Expect(err).To(BeNil()) + vol2 := filepath.Join(podmanTest.TempDir, "vol-test2") + err = os.MkdirAll(vol2, 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"run", "--mount", "type=bind,src=" + vol1 + ",target=/myvol1,z", "--mount", "type=bind,src=" + vol2 + ",target=/myvol2,z", ALPINE, "touch", "/myvol2/foo.txt"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + It("podman run findmnt nothing shared", func() { vol1 := filepath.Join(podmanTest.TempDir, "vol-test1") err := os.MkdirAll(vol1, 0755) -- cgit v1.2.3-54-g00ecf