Add --mount option for create & run command

Signed-off-by: Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>

Closes: #1524
Approved by: mheon
This commit is contained in:
Daniel J Walsh
2018-09-21 06:29:18 -04:00
committed by Atomic Bot
parent 9e81f9daa4
commit 52c1365f32
11 changed files with 303 additions and 61 deletions

View File

@ -417,6 +417,10 @@ var createFlags = []cli.Flag{
Name: "uts", Name: "uts",
Usage: "UTS namespace to use", Usage: "UTS namespace to use",
}, },
cli.StringSliceFlag{
Name: "mount",
Usage: "Attach a filesystem mount to the container (default [])",
},
cli.StringSliceFlag{ cli.StringSliceFlag{
Name: "volume, v", Name: "volume, v",
Usage: "Bind mount a volume into the container (default [])", Usage: "Bind mount a volume into the container (default [])",

View File

@ -24,6 +24,7 @@ import (
"github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/signal"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/docker/go-units" "github.com/docker/go-units"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/selinux/go-selinux/label" "github.com/opencontainers/selinux/go-selinux/label"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -459,6 +460,10 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim
} }
blkioWeight = uint16(u) 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 { if err = parseVolumes(c.StringSlice("volume")); err != nil {
return nil, err return nil, err
@ -772,6 +777,7 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim
Tty: tty, Tty: tty,
User: user, User: user,
UsernsMode: usernsMode, UsernsMode: usernsMode,
Mounts: mountList,
Volumes: c.StringSlice("volume"), Volumes: c.StringSlice("volume"),
WorkDir: workDir, WorkDir: workDir,
Rootfs: rootfs, Rootfs: rootfs,

View File

@ -8,6 +8,8 @@ import (
cc "github.com/containers/libpod/pkg/spec" cc "github.com/containers/libpod/pkg/spec"
"github.com/docker/docker/pkg/sysinfo" "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/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -74,6 +76,94 @@ func addWarning(warnings []string, msg string) []string {
return append(warnings, msg) 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=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>,[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 { func parseVolumes(volumes []string) error {
for _, volume := range volumes { for _, volume := range volumes {
arr := strings.SplitN(volume, ":", 3) arr := strings.SplitN(volume, ":", 3)

View File

@ -945,6 +945,7 @@ _podman_build() {
--userns-uid-map-user --userns-uid-map-user
--userns-gid-map-group --userns-gid-map-group
--uts --uts
--mount
--volume --volume
-v -v
" "

View File

@ -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. 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**="" **--name**=""
Assign a name to the container Assign a name to the container

View File

@ -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. **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]]*] **-v**|**--volume**[=*[HOST-DIR:CONTAINER-DIR[:OPTIONS]]*]
Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, podman 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 $ 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 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 policy. Therefore, in the above example, if SELinux policy is enforced, the
`/var/db` directory is not writable to the container. A "Permission Denied" `/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) subgid(5), subuid(5), libpod.conf(5)
## HISTORY ## HISTORY
September 2018, updated by Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
October 2017, converted from Docker documentation to podman by Dan Walsh for podman <dwalsh@redhat.com> October 2017, converted from Docker documentation to podman by Dan Walsh for podman <dwalsh@redhat.com>
November 2015, updated by Sally O'Malley <somalley@redhat.com> November 2015, updated by Sally O'Malley <somalley@redhat.com>

View File

@ -926,6 +926,9 @@ func (c *Container) makeBindMounts() error {
if err != nil { if err != nil {
return errors.Wrapf(err, "error creating resolv.conf for container %s", c.ID()) 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 c.state.BindMounts["/etc/resolv.conf"] = newResolv
// Make /etc/hosts // Make /etc/hosts
@ -937,6 +940,9 @@ func (c *Container) makeBindMounts() error {
if err != nil { if err != nil {
return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) 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 c.state.BindMounts["/etc/hosts"] = newHosts
// Make /etc/hostname // Make /etc/hostname
@ -946,6 +952,9 @@ func (c *Container) makeBindMounts() error {
if err != nil { if err != nil {
return errors.Wrapf(err, "error creating hostname file for container %s", c.ID()) 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 c.state.BindMounts["/etc/hostname"] = hostnamePath
} }

View File

@ -283,6 +283,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
mounts := sortMounts(g.Mounts()) mounts := sortMounts(g.Mounts())
g.ClearMounts() g.ClearMounts()
for _, m := range mounts { 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) g.AddMount(m)
} }
return g.Config, nil return g.Config, nil

View File

@ -122,6 +122,7 @@ type CreateConfig struct {
UsernsMode namespaces.UsernsMode //userns UsernsMode namespaces.UsernsMode //userns
User string //user User string //user
UtsMode namespaces.UTSMode //uts UtsMode namespaces.UTSMode //uts
Mounts []spec.Mount //mounts
Volumes []string //volume Volumes []string //volume
VolumesFrom []string VolumesFrom []string
WorkDir string //workdir WorkDir string //workdir
@ -142,54 +143,59 @@ func (c *CreateConfig) CreateBlockIO() (*spec.LinuxBlockIO, error) {
return c.createBlockIO() 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 //GetVolumeMounts takes user provided input for bind mounts and creates Mount structs
func (c *CreateConfig) GetVolumeMounts(specMounts []spec.Mount) ([]spec.Mount, error) { func (c *CreateConfig) GetVolumeMounts(specMounts []spec.Mount) ([]spec.Mount, error) {
var m []spec.Mount var m []spec.Mount
for _, i := range c.Volumes { for _, i := range c.Volumes {
var ( var options []string
options []string
foundrw, foundro, foundz, foundZ bool
rootProp string
)
// We need to handle SELinux options better here, specifically :Z
spliti := strings.Split(i, ":") spliti := strings.Split(i, ":")
if len(spliti) > 2 { if len(spliti) > 2 {
options = strings.Split(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{ m = append(m, spec.Mount{
Destination: spliti[1], Destination: spliti[1],
Type: string(TypeBind), Type: string(TypeBind),
Source: spliti[0], Source: spliti[0],
Options: options, Options: processOptions(options),
}) })
logrus.Debugf("User mount %s:%s options %v", spliti[0], spliti[1], options) logrus.Debugf("User mount %s:%s options %v", spliti[0], spliti[1], options)

View File

@ -18,6 +18,34 @@ import (
const cpuPeriod = 100000 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 // CreateConfigToOCISpec parses information needed to create a container into an OCI runtime spec
func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint
cgroupPerm := "ro" cgroupPerm := "ro"
@ -246,6 +274,12 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint
g.AddMount(tmpfsMnt) g.AddMount(tmpfsMnt)
} }
for _, m := range config.Mounts {
if m.Type == "tmpfs" {
g.AddMount(m)
}
}
for name, val := range config.Env { for name, val := range config.Env {
g.AddProcessEnv(name, val) 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") 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 { if err != nil {
return nil, errors.Wrapf(err, "error getting volume mounts") 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 { if canAddResources {
// BLOCK IO // BLOCK IO
blkio, err := config.CreateBlockIO() blkio, err := config.CreateBlockIO()

View File

@ -234,6 +234,32 @@ var _ = Describe("Podman run", func() {
Expect(session.OutputToString()).To(ContainSubstring("/run/test rw,relatime, shared")) 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() { It("podman run with cidfile", func() {
session := podmanTest.Podman([]string{"run", "--cidfile", tempdir + "cidfile", ALPINE, "ls"}) session := podmanTest.Podman([]string{"run", "--cidfile", tempdir + "cidfile", ALPINE, "ls"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
@ -565,6 +591,19 @@ USER mail`
Expect(session.ExitCode()).To(Equal(0)) 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() { It("podman run findmnt nothing shared", func() {
vol1 := filepath.Join(podmanTest.TempDir, "vol-test1") vol1 := filepath.Join(podmanTest.TempDir, "vol-test1")
err := os.MkdirAll(vol1, 0755) err := os.MkdirAll(vol1, 0755)