mirror of https://github.com/containers/podman.git
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 <umohnani@redhat.com> Closes: #931 Approved by: mheon
This commit is contained in:
parent
c7424b6991
commit
4855998f1c
|
|
@ -302,6 +302,10 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = parseVolumesFrom(c.StringSlice("volumes-from")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tty := c.Bool("tty")
|
tty := c.Bool("tty")
|
||||||
|
|
||||||
pidMode := container.PidMode(c.String("pid"))
|
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"),
|
Volumes: c.StringSlice("volume"),
|
||||||
WorkDir: workDir,
|
WorkDir: workDir,
|
||||||
Rootfs: rootfs,
|
Rootfs: rootfs,
|
||||||
|
VolumesFrom: c.StringSlice("volumes-from"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.Privileged {
|
if !config.Privileged {
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,6 @@ func addWarning(warnings []string, msg string) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseVolumes(volumes []string) error {
|
func parseVolumes(volumes []string) error {
|
||||||
if len(volumes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, volume := range volumes {
|
for _, volume := range volumes {
|
||||||
arr := strings.SplitN(volume, ":", 3)
|
arr := strings.SplitN(volume, ":", 3)
|
||||||
if len(arr) < 2 {
|
if len(arr) < 2 {
|
||||||
|
|
@ -98,6 +95,21 @@ func parseVolumes(volumes []string) error {
|
||||||
return nil
|
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 {
|
func validateVolumeHostDir(hostDir string) error {
|
||||||
if !filepath.IsAbs(hostDir) {
|
if !filepath.IsAbs(hostDir) {
|
||||||
return errors.Errorf("invalid host path, must be an absolute path %q", 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 {
|
for _, opt := range options {
|
||||||
switch opt {
|
switch opt {
|
||||||
case "rw", "ro":
|
case "rw", "ro":
|
||||||
|
foundRWRO++
|
||||||
if foundRWRO > 1 {
|
if foundRWRO > 1 {
|
||||||
return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option)
|
return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option)
|
||||||
}
|
}
|
||||||
foundRWRO++
|
|
||||||
case "z", "Z":
|
case "z", "Z":
|
||||||
|
foundLabelChange++
|
||||||
if foundLabelChange > 1 {
|
if foundLabelChange > 1 {
|
||||||
return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option)
|
return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option)
|
||||||
}
|
}
|
||||||
foundLabelChange++
|
|
||||||
case "private", "rprivate", "shared", "rshared", "slave", "rslave":
|
case "private", "rprivate", "shared", "rshared", "slave", "rslave":
|
||||||
|
foundRootPropagation++
|
||||||
if foundRootPropagation > 1 {
|
if foundRootPropagation > 1 {
|
||||||
return errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private' or '[r]slave' option", option)
|
return errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private' or '[r]slave' option", option)
|
||||||
}
|
}
|
||||||
foundRootPropagation++
|
|
||||||
default:
|
default:
|
||||||
return errors.Errorf("invalid option type %q", option)
|
return errors.Errorf("invalid option type %q", option)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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**=""
|
**-w**, **--workdir**=""
|
||||||
|
|
||||||
Working directory inside the container
|
Working directory inside the container
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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**=""
|
**-w**, **--workdir**=""
|
||||||
|
|
||||||
Working directory inside the container
|
Working directory inside the container
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,9 @@ type ContainerConfig struct {
|
||||||
// ExitCommand is the container's exit command.
|
// ExitCommand is the container's exit command.
|
||||||
// This Command will be executed when the container exits
|
// This Command will be executed when the container exits
|
||||||
ExitCommand []string `json:"exitCommand,omitempty"`
|
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
|
// ContainerStatus returns a string representation for users
|
||||||
|
|
|
||||||
|
|
@ -1108,7 +1108,7 @@ func (c *Container) generateHosts() (string, error) {
|
||||||
return c.writeStringToRundir("hosts", hosts)
|
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
|
mountPoint := c.state.Mountpoint
|
||||||
if !c.state.Mounted {
|
if !c.state.Mounted {
|
||||||
return errors.Wrapf(ErrInternal, "container is not 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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
for k := range imageData.ContainerConfig.Volumes {
|
||||||
mount := spec.Mount{
|
mount := spec.Mount{
|
||||||
Destination: k,
|
Destination: k,
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
|
||||||
|
|
||||||
// Bind builtin image volumes
|
// Bind builtin image volumes
|
||||||
if c.config.Rootfs == "" && c.config.ImageVolumes {
|
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")
|
return nil, errors.Wrapf(err, "error mounting image volumes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// WithEntrypoint sets the entrypoint of the container.
|
||||||
// This is not used to change the container's spec, but will instead be used
|
// 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.
|
// during commit to populate the entrypoint of the new image.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package createconfig
|
package createconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -123,6 +124,7 @@ type CreateConfig struct {
|
||||||
User string //user
|
User string //user
|
||||||
UtsMode container.UTSMode //uts
|
UtsMode container.UTSMode //uts
|
||||||
Volumes []string //volume
|
Volumes []string //volume
|
||||||
|
VolumesFrom []string
|
||||||
WorkDir string //workdir
|
WorkDir string //workdir
|
||||||
MountLabel string //SecurityOpts
|
MountLabel string //SecurityOpts
|
||||||
ProcessLabel string //SecurityOpts
|
ProcessLabel string //SecurityOpts
|
||||||
|
|
@ -131,6 +133,7 @@ type CreateConfig struct {
|
||||||
SeccompProfilePath string //SecurityOpts
|
SeccompProfilePath string //SecurityOpts
|
||||||
SecurityOpts []string
|
SecurityOpts []string
|
||||||
Rootfs 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 }
|
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
|
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
|
//GetTmpfsMounts takes user provided input for Tmpfs mounts and creates Mount structs
|
||||||
func (c *CreateConfig) GetTmpfsMounts() []spec.Mount {
|
func (c *CreateConfig) GetTmpfsMounts() []spec.Mount {
|
||||||
var m []spec.Mount
|
var m []spec.Mount
|
||||||
|
|
@ -289,6 +338,10 @@ func (c *CreateConfig) GetContainerCreateOptions(runtime *libpod.Runtime) ([]lib
|
||||||
options = append(options, libpod.WithUserVolumes(volumes))
|
options = append(options, libpod.WithUserVolumes(volumes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.LocalVolumes) != 0 {
|
||||||
|
options = append(options, libpod.WithLocalVolumes(c.LocalVolumes))
|
||||||
|
}
|
||||||
|
|
||||||
if len(c.Command) != 0 {
|
if len(c.Command) != 0 {
|
||||||
options = append(options, libpod.WithCommand(c.Command))
|
options = append(options, libpod.WithCommand(c.Command))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,9 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint
|
||||||
}
|
}
|
||||||
|
|
||||||
// BIND MOUNTS
|
// 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)
|
mounts, 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")
|
||||||
|
|
|
||||||
|
|
@ -510,4 +510,41 @@ USER mail`
|
||||||
session.WaitWithDefaultTimeout()
|
session.WaitWithDefaultTimeout()
|
||||||
Expect(session.ExitCode()).To(Equal(0))
|
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"))
|
||||||
|
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue