From 4855998f1cf533b27e48b2ded5541841fe6a3ea6 Mon Sep 17 00:00:00 2001 From: umohnani8 Date: Mon, 11 Jun 2018 15:27:42 -0400 Subject: Add --volumes-from flag to podman run and create podman now supports --volumes-from flag, which allows users to add all the volumes an existing container has to a new one. Signed-off-by: umohnani8 Closes: #931 Approved by: mheon --- cmd/podman/create.go | 5 +++ cmd/podman/create_cli.go | 24 ++++++++++---- docs/podman-create.1.md | 34 ++++++++++++++++++++ docs/podman-run.1.md | 34 ++++++++++++++++++++ libpod/container.go | 3 ++ libpod/container_internal.go | 13 +++++++- libpod/container_internal_linux.go | 2 +- libpod/options.go | 18 +++++++++++ pkg/spec/createconfig.go | 65 ++++++++++++++++++++++++++++++++++---- pkg/spec/spec.go | 3 ++ test/e2e/run_test.go | 37 ++++++++++++++++++++++ 11 files changed, 224 insertions(+), 14 deletions(-) diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 19e87c306..d61f85442 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -302,6 +302,10 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim return nil, err } + if err = parseVolumesFrom(c.StringSlice("volumes-from")); err != nil { + return nil, err + } + tty := c.Bool("tty") pidMode := container.PidMode(c.String("pid")) @@ -596,6 +600,7 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim Volumes: c.StringSlice("volume"), WorkDir: workDir, Rootfs: rootfs, + VolumesFrom: c.StringSlice("volumes-from"), } if !config.Privileged { diff --git a/cmd/podman/create_cli.go b/cmd/podman/create_cli.go index 8ae4b6c64..d0ad28595 100644 --- a/cmd/podman/create_cli.go +++ b/cmd/podman/create_cli.go @@ -75,9 +75,6 @@ func addWarning(warnings []string, msg string) []string { } func parseVolumes(volumes []string) error { - if len(volumes) == 0 { - return nil - } for _, volume := range volumes { arr := strings.SplitN(volume, ":", 3) if len(arr) < 2 { @@ -98,6 +95,21 @@ func parseVolumes(volumes []string) error { return nil } +func parseVolumesFrom(volumesFrom []string) error { + for _, vol := range volumesFrom { + arr := strings.SplitN(vol, ":", 2) + if len(arr) == 2 { + if strings.Contains(arr[1], "Z") || strings.Contains(arr[1], "private") || strings.Contains(arr[1], "slave") || strings.Contains(arr[1], "shared") { + return errors.Errorf("invalid options %q, can only specify 'ro', 'rw', and 'z", arr[1]) + } + if err := validateVolumeOpts(arr[1]); err != nil { + return err + } + } + } + return nil +} + func validateVolumeHostDir(hostDir string) error { if !filepath.IsAbs(hostDir) { return errors.Errorf("invalid host path, must be an absolute path %q", hostDir) @@ -121,20 +133,20 @@ func validateVolumeOpts(option string) error { for _, opt := range options { switch opt { case "rw", "ro": + foundRWRO++ if foundRWRO > 1 { return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option) } - foundRWRO++ case "z", "Z": + foundLabelChange++ if foundLabelChange > 1 { return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option) } - foundLabelChange++ case "private", "rprivate", "shared", "rshared", "slave", "rslave": + foundRootPropagation++ if foundRootPropagation > 1 { return errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private' or '[r]slave' option", option) } - foundRootPropagation++ default: return errors.Errorf("invalid option type %q", option) } diff --git a/docs/podman-create.1.md b/docs/podman-create.1.md index 9631655c1..ed20c6f8d 100644 --- a/docs/podman-create.1.md +++ b/docs/podman-create.1.md @@ -654,6 +654,40 @@ change propagation properties of source mount. Say `/` is source mount for To disable automatic copying of data from the container path to the volume, use the `nocopy` flag. The `nocopy` flag can be set on bind mounts and named volumes. +**--volumes-from**[=*CONTAINER*[:*OPTIONS*]] + +Mount volumes from the specified container(s). +*OPTIONS* is a comma delimited list with the following available elements: + +* [rw|ro] +* z + +Mounts already mounted volumes from a source container onto another +container. You must supply the source's container-id or container-name. +To share a volume, use the --volumes-from option when running +the target container. You can share volumes even if the source container +is not running. + +By default, podman mounts the volumes in the same mode (read-write or +read-only) as it is mounted in the source container. Optionally, you +can change this by suffixing the container-id with either the `ro` or +`rw` keyword. + +Labeling systems like SELinux require that proper labels are placed on volume +content mounted into a container. Without a label, the security system might +prevent the processes running inside the container from using the content. By +default, podman does not change the labels set by the OS. + +To change a label in the container context, you can add `z` to the volume mount. +This suffix tells podman to relabel file objects on the shared volumes. The `z` +option tells podman that two containers share the volume content. As a result, +podman labels the content with a shared content label. Shared volume labels allow +all containers to read/write content. + +If the location of the volume from the source container overlaps with +data residing on a target container, then the volume hides +that data on the target. + **-w**, **--workdir**="" Working directory inside the container diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md index 10b5d9e16..a049a33aa 100644 --- a/docs/podman-run.1.md +++ b/docs/podman-run.1.md @@ -686,6 +686,40 @@ change propagation properties of source mount. Say `/` is source mount for To disable automatic copying of data from the container path to the volume, use the `nocopy` flag. The `nocopy` flag can be set on bind mounts and named volumes. +**--volumes-from**[=*CONTAINER*[:*OPTIONS*]] + +Mount volumes from the specified container(s). +*OPTIONS* is a comma delimited list with the following available elements: + +* [rw|ro] +* z + +Mounts already mounted volumes from a source container onto another +container. You must supply the source's container-id or container-name. +To share a volume, use the --volumes-from option when running +the target container. You can share volumes even if the source container +is not running. + +By default, podman mounts the volumes in the same mode (read-write or +read-only) as it is mounted in the source container. Optionally, you +can change this by suffixing the container-id with either the `ro` or +`rw` keyword. + +Labeling systems like SELinux require that proper labels are placed on volume +content mounted into a container. Without a label, the security system might +prevent the processes running inside the container from using the content. By +default, podman does not change the labels set by the OS. + +To change a label in the container context, you can add `z` to the volume mount. +This suffix tells podman to relabel file objects on the shared volumes. The `z` +option tells podman that two containers share the volume content. As a result, +podman labels the content with a shared content label. Shared volume labels allow +all containers to read/write content. + +If the location of the volume from the source container overlaps with +data residing on a target container, then the volume hides +that data on the target. + **-w**, **--workdir**="" Working directory inside the container diff --git a/libpod/container.go b/libpod/container.go index 24a5f93af..9486986ab 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -313,6 +313,9 @@ type ContainerConfig struct { // ExitCommand is the container's exit command. // This Command will be executed when the container exits ExitCommand []string `json:"exitCommand,omitempty"` + // LocalVolumes are the built-in volumes we get from the --volumes-from flag + // It picks up the built-in volumes of the container used by --volumes-from + LocalVolumes []string } // ContainerStatus returns a string representation for users diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 94cd74c09..eb9b39a02 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -1108,7 +1108,7 @@ func (c *Container) generateHosts() (string, error) { return c.writeStringToRundir("hosts", hosts) } -func (c *Container) addImageVolumes(ctx context.Context, g *generate.Generator) error { +func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator) error { mountPoint := c.state.Mountpoint if !c.state.Mounted { return errors.Wrapf(ErrInternal, "container is not mounted") @@ -1121,6 +1121,17 @@ func (c *Container) addImageVolumes(ctx context.Context, g *generate.Generator) if err != nil { return err } + // Add the built-in volumes of the container passed in to --volumes-from + for _, vol := range c.config.LocalVolumes { + if imageData.ContainerConfig.Volumes == nil { + imageData.ContainerConfig.Volumes = map[string]struct{}{ + vol: {}, + } + } else { + imageData.ContainerConfig.Volumes[vol] = struct{}{} + } + } + for k := range imageData.ContainerConfig.Volumes { mount := spec.Mount{ Destination: k, diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 0dc02d117..9ad825458 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -127,7 +127,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // Bind builtin image volumes if c.config.Rootfs == "" && c.config.ImageVolumes { - if err := c.addImageVolumes(ctx, &g); err != nil { + if err := c.addLocalVolumes(ctx, &g); err != nil { return nil, errors.Wrapf(err, "error mounting image volumes") } } diff --git a/libpod/options.go b/libpod/options.go index 9f07db7ed..c02bd4336 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -885,6 +885,24 @@ func WithUserVolumes(volumes []string) CtrCreateOption { } } +// WithLocalVolumes sets the built-in volumes of the container retrieved +// from a container passed in to the --volumes-from flag. +// This stores the built-in volume information in the ContainerConfig so we can +// add them when creating the container. +func WithLocalVolumes(volumes []string) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return ErrCtrFinalized + } + + if volumes != nil { + ctr.config.LocalVolumes = append(ctr.config.LocalVolumes, volumes...) + } + + return nil + } +} + // WithEntrypoint sets the entrypoint of the container. // This is not used to change the container's spec, but will instead be used // during commit to populate the entrypoint of the new image. diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 48343c3a4..a39f4875c 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -1,6 +1,7 @@ package createconfig import ( + "encoding/json" "os" "strconv" "strings" @@ -123,14 +124,16 @@ type CreateConfig struct { User string //user UtsMode container.UTSMode //uts Volumes []string //volume - WorkDir string //workdir - MountLabel string //SecurityOpts - ProcessLabel string //SecurityOpts - NoNewPrivs bool //SecurityOpts - ApparmorProfile string //SecurityOpts - SeccompProfilePath string //SecurityOpts + VolumesFrom []string + WorkDir string //workdir + MountLabel string //SecurityOpts + ProcessLabel string //SecurityOpts + NoNewPrivs bool //SecurityOpts + ApparmorProfile string //SecurityOpts + SeccompProfilePath string //SecurityOpts SecurityOpts []string Rootfs string + LocalVolumes []string //Keeps track of the built-in volumes of container used in the --volumes-from flag } func u32Ptr(i int64) *uint32 { u := uint32(i); return &u } @@ -215,6 +218,52 @@ func (c *CreateConfig) GetVolumeMounts(specMounts []spec.Mount) ([]spec.Mount, e return m, nil } +// GetVolumesFrom reads the create-config artifact of the container to get volumes from +// and adds it to c.Volumes of the curent container. +func (c *CreateConfig) GetVolumesFrom() error { + var options string + for _, vol := range c.VolumesFrom { + splitVol := strings.SplitN(vol, ":", 2) + if len(splitVol) == 2 { + options = splitVol[1] + } + ctr, err := c.Runtime.LookupContainer(splitVol[0]) + if err != nil { + return errors.Wrapf(err, "error looking up container %q", splitVol[0]) + } + var createArtifact CreateConfig + artifact, err := ctr.GetArtifact("create-config") + if err != nil { + return errors.Wrapf(err, "error getting create-config artifact for %q", splitVol[0]) + } + if err := json.Unmarshal(artifact, &createArtifact); err != nil { + return err + } + + for key := range createArtifact.BuiltinImgVolumes { + c.LocalVolumes = append(c.LocalVolumes, key) + } + + for _, i := range createArtifact.Volumes { + // Volumes format is host-dir:ctr-dir[:options], so get the host and ctr dir + // and add on the options given by the user to the flag. + spliti := strings.SplitN(i, ":", 3) + // Throw error if mounting volume from container with Z option (private label) + // Override this by adding 'z' to options. + if len(spliti) > 2 && strings.Contains(spliti[2], "Z") && !strings.Contains(options, "z") { + return errors.Errorf("volume mounted with private option 'Z' in %q. Use option 'z' to mount in current container", ctr.ID()) + } + if options == "" { + // Mount the volumes with the default options + c.Volumes = append(c.Volumes, createArtifact.Volumes...) + } else { + c.Volumes = append(c.Volumes, spliti[0]+":"+spliti[1]+":"+options) + } + } + } + return nil +} + //GetTmpfsMounts takes user provided input for Tmpfs mounts and creates Mount structs func (c *CreateConfig) GetTmpfsMounts() []spec.Mount { var m []spec.Mount @@ -289,6 +338,10 @@ func (c *CreateConfig) GetContainerCreateOptions(runtime *libpod.Runtime) ([]lib options = append(options, libpod.WithUserVolumes(volumes)) } + if len(c.LocalVolumes) != 0 { + options = append(options, libpod.WithLocalVolumes(c.LocalVolumes)) + } + if len(c.Command) != 0 { options = append(options, libpod.WithCommand(c.Command)) } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index dc23c129c..ffa242675 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -248,6 +248,9 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint } // BIND MOUNTS + if err := config.GetVolumesFrom(); err != nil { + return nil, errors.Wrap(err, "error getting volume mounts from --volumes-from flag") + } mounts, err := config.GetVolumeMounts(configSpec.Mounts) if err != nil { return nil, errors.Wrapf(err, "error getting volume mounts") diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index dc087f700..32206c4f5 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -510,4 +510,41 @@ USER mail` session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) }) + + It("podman run --volumes-from flag", func() { + vol := filepath.Join(podmanTest.TempDir, "vol-test") + err := os.MkdirAll(vol, 0755) + Expect(err).To(BeNil()) + + volFile := filepath.Join(vol, "test.txt") + data := "Testing --volumes-from!!!" + err = ioutil.WriteFile(volFile, []byte(data), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"create", "--volume", vol + ":/myvol", "docker.io/library/redis:alpine", "sh"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ctrID := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "--volumes-from", ctrID, ALPINE, "echo", "'testing read-write!' >> myvol/test.txt"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "--volumes-from", ctrID + ":z", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("podman run --volumes-from flag with built-in volumes", func() { + session := podmanTest.Podman([]string{"create", "docker.io/library/redis:alpine", "sh"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ctrID := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "--volumes-from", ctrID, ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("data")) + + }) }) -- cgit v1.2.3-54-g00ecf