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",
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 [])",

View File

@ -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,

View File

@ -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=<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 {
for _, volume := range volumes {
arr := strings.SplitN(volume, ":", 3)

View File

@ -945,6 +945,7 @@ _podman_build() {
--userns-uid-map-user
--userns-gid-map-group
--uts
--mount
--volume
-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.
**--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

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.
**--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 <kushwaha_kunal_v7@lab.ntt.co.jp>
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>

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)