diff options
-rw-r--r-- | cmd/podman/volumes/export.go | 96 | ||||
-rw-r--r-- | docs/source/markdown/podman-volume-export.1.md | 38 | ||||
-rw-r--r-- | docs/source/markdown/podman-volume.1.md | 1 | ||||
-rw-r--r-- | docs/source/volume.rst | 2 | ||||
-rw-r--r-- | libpod/volume.go | 11 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 1 | ||||
-rw-r--r-- | pkg/domain/infra/abi/volumes.go | 16 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/volumes.go | 6 | ||||
-rw-r--r-- | test/e2e/volume_create_test.go | 19 | ||||
-rw-r--r-- | utils/utils.go | 10 |
10 files changed, 200 insertions, 0 deletions
diff --git a/cmd/podman/volumes/export.go b/cmd/podman/volumes/export.go new file mode 100644 index 000000000..9e4fecdfa --- /dev/null +++ b/cmd/podman/volumes/export.go @@ -0,0 +1,96 @@ +package volumes + +import ( + "context" + "fmt" + + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v3/cmd/podman/common" + "github.com/containers/podman/v3/cmd/podman/inspect" + "github.com/containers/podman/v3/cmd/podman/registry" + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + volumeExportDescription = ` +podman volume export + +Allow content of volume to be exported into external tar.` + exportCommand = &cobra.Command{ + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + Use: "export [options] VOLUME", + Short: "Export volumes", + Args: cobra.ExactArgs(1), + Long: volumeExportDescription, + RunE: export, + ValidArgsFunction: common.AutocompleteVolumes, + } +) + +var ( + // Temporary struct to hold cli values. + cliExportOpts = struct { + Output string + }{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: exportCommand, + Parent: volumeCmd, + }) + flags := exportCommand.Flags() + + outputFlagName := "output" + flags.StringVarP(&cliExportOpts.Output, outputFlagName, "o", "/dev/stdout", "Write to a specified file (default: stdout, which must be redirected)") + _ = exportCommand.RegisterFlagCompletionFunc(outputFlagName, completion.AutocompleteDefault) +} + +func export(cmd *cobra.Command, args []string) error { + var inspectOpts entities.InspectOptions + containerEngine := registry.ContainerEngine() + ctx := context.Background() + + if cliExportOpts.Output == "" { + return errors.New("expects output path, use --output=[path]") + } + inspectOpts.Type = inspect.VolumeType + volumeData, _, err := containerEngine.VolumeInspect(ctx, args, inspectOpts) + if err != nil { + return err + } + if len(volumeData) < 1 { + return errors.New("no volume data found") + } + mountPoint := volumeData[0].VolumeConfigResponse.Mountpoint + driver := volumeData[0].VolumeConfigResponse.Driver + volumeOptions := volumeData[0].VolumeConfigResponse.Options + volumeMountStatus, err := containerEngine.VolumeMounted(ctx, args[0]) + if err != nil { + return err + } + if mountPoint == "" { + return errors.New("volume is not mounted anywhere on host") + } + // Check if volume is using external plugin and export only if volume is mounted + if driver != "" && driver != "local" { + if !volumeMountStatus.Value { + return fmt.Errorf("volume is using a driver %s and volume is not mounted on %s", driver, mountPoint) + } + } + // Check if volume is using `local` driver and has mount options type other than tmpfs + if driver == "local" { + if mountOptionType, ok := volumeOptions["type"]; ok { + if mountOptionType != "tmpfs" && !volumeMountStatus.Value { + return fmt.Errorf("volume is using a driver %s and volume is not mounted on %s", driver, mountPoint) + } + } + } + logrus.Debugf("Exporting volume data from %s to %s", mountPoint, cliExportOpts.Output) + err = utils.CreateTarFromSrc(mountPoint, cliExportOpts.Output) + return err +} diff --git a/docs/source/markdown/podman-volume-export.1.md b/docs/source/markdown/podman-volume-export.1.md new file mode 100644 index 000000000..caaa37652 --- /dev/null +++ b/docs/source/markdown/podman-volume-export.1.md @@ -0,0 +1,38 @@ +% podman-volume-export(1) + +## NAME +podman\-volume\-export - Exports volume to external tar + +## SYNOPSIS +**podman volume export** [*options*] *volume* + +## DESCRIPTION + +**podman volume export** exports the contents of a podman volume and saves it as a tarball +on the local machine. **podman volume export** writes to STDOUT by default and can be +redirected to a file using the `--output` flag. + +Note: Following command is not supported by podman-remote. + +**podman volume export [OPTIONS] VOLUME** + +## OPTIONS + +#### **--output**, **-o**=*file* + +Write to a file, default is STDOUT + +#### **--help** + +Print usage statement + + +## EXAMPLES + +``` +$ podman volume export myvol --output myvol.tar + +``` + +## SEE ALSO +podman-volume(1) diff --git a/docs/source/markdown/podman-volume.1.md b/docs/source/markdown/podman-volume.1.md index 5af5eb50e..20319ccf7 100644 --- a/docs/source/markdown/podman-volume.1.md +++ b/docs/source/markdown/podman-volume.1.md @@ -15,6 +15,7 @@ podman volume is a set of subcommands that manage volumes. | ------- | ------------------------------------------------------ | ------------------------------------------------------------------------------ | | create | [podman-volume-create(1)](podman-volume-create.1.md) | Create a new volume. | | exists | [podman-volume-exists(1)](podman-volume-exists.1.md) | Check if the given volume exists. | +| export | [podman-volume-export(1)](podman-volume-export.1.md) | Exports volume to external tar. | | inspect | [podman-volume-inspect(1)](podman-volume-inspect.1.md) | Get detailed information on one or more volumes. | | ls | [podman-volume-ls(1)](podman-volume-ls.1.md) | List all the available volumes. | | prune | [podman-volume-prune(1)](podman-volume-prune.1.md) | Remove all unused volumes. | diff --git a/docs/source/volume.rst b/docs/source/volume.rst index ce9ea2cbd..fb607cc2b 100644 --- a/docs/source/volume.rst +++ b/docs/source/volume.rst @@ -4,6 +4,8 @@ Volume :doc:`exists <markdown/podman-volume-exists.1>` Check if the given volume exists +:doc:`export <markdown/podman-volume-export.1>` Exports volume to external tar + :doc:`inspect <markdown/podman-volume-inspect.1>` Display detailed information on one or more volumes :doc:`ls <markdown/podman-volume-ls.1>` List volumes diff --git a/libpod/volume.go b/libpod/volume.go index 8f3dc4fcc..90b423f1d 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -139,6 +139,17 @@ func (v *Volume) MountPoint() (string, error) { return v.mountPoint(), nil } +// MountCount returns the volume's mountcount on the host from state +// Useful in determining if volume is using plugin or a filesystem mount and its mount +func (v *Volume) MountCount() (uint, error) { + v.lock.Lock() + defer v.lock.Unlock() + if err := v.update(); err != nil { + return 0, err + } + return v.state.MountCount, nil +} + // Internal-only helper for volume mountpoint func (v *Volume) mountPoint() string { if v.UsesVolumeDriver() { diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index d573e4704..5d3c9480e 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -92,6 +92,7 @@ type ContainerEngine interface { Version(ctx context.Context) (*SystemVersionReport, error) VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error) VolumeExists(ctx context.Context, namesOrID string) (*BoolReport, error) + VolumeMounted(ctx context.Context, namesOrID string) (*BoolReport, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts InspectOptions) ([]*VolumeInspectReport, []error, error) VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*reports.PruneReport, error) diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index e077b10ea..1610c0b48 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -162,3 +162,19 @@ func (ic *ContainerEngine) VolumeExists(ctx context.Context, nameOrID string) (* } return &entities.BoolReport{Value: exists}, nil } + +// Volumemounted check if a given volume using plugin or filesystem is mounted or not. +func (ic *ContainerEngine) VolumeMounted(ctx context.Context, nameOrID string) (*entities.BoolReport, error) { + vol, err := ic.Libpod.LookupVolume(nameOrID) + if err != nil { + return nil, err + } + mountCount, err := vol.MountCount() + if err != nil { + return &entities.BoolReport{Value: false}, nil + } + if mountCount > 0 { + return &entities.BoolReport{Value: true}, nil + } + return &entities.BoolReport{Value: false}, nil +} diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index 2d231bad6..2b2b2c2a1 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -91,3 +91,9 @@ func (ic *ContainerEngine) VolumeExists(ctx context.Context, nameOrID string) (* Value: exists, }, nil } + +// Volumemounted check if a given volume using plugin or filesystem is mounted or not. +// TODO: Not used and exposed to tunnel. Will be used by `export` command which is unavailable to `podman-remote` +func (ic *ContainerEngine) VolumeMounted(ctx context.Context, nameOrID string) (*entities.BoolReport, error) { + return nil, errors.New("not implemented") +} diff --git a/test/e2e/volume_create_test.go b/test/e2e/volume_create_test.go index 51005d177..d9c805f46 100644 --- a/test/e2e/volume_create_test.go +++ b/test/e2e/volume_create_test.go @@ -60,6 +60,25 @@ var _ = Describe("Podman volume create", func() { Expect(len(check.OutputToStringArray())).To(Equal(1)) }) + It("podman create and export volume", func() { + if podmanTest.RemoteTest { + Skip("Volume export check does not work with a remote client") + } + + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + volName := session.OutputToString() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"run", "--volume", volName + ":/data", ALPINE, "sh", "-c", "echo hello >> " + "/data/test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + check := podmanTest.Podman([]string{"volume", "export", volName}) + check.WaitWithDefaultTimeout() + Expect(check.OutputToString()).To(ContainSubstring("hello")) + }) + It("podman create volume with bad volume option", func() { session := podmanTest.Podman([]string{"volume", "create", "--opt", "badOpt=bad"}) session.WaitWithDefaultTimeout() diff --git a/utils/utils.go b/utils/utils.go index a2268a30b..2e415130e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -107,6 +107,16 @@ func UntarToFileSystem(dest string, tarball *os.File, options *archive.TarOption return archive.Untar(tarball, dest, options) } +// Creates a new tar file and wrties bytes from io.ReadCloser +func CreateTarFromSrc(source string, dest string) error { + file, err := os.Create(dest) + if err != nil { + return errors.Wrapf(err, "Could not create tarball file '%s'", dest) + } + defer file.Close() + return TarToFilesystem(source, file) +} + // TarToFilesystem creates a tarball from source and writes to an os.file // provided func TarToFilesystem(source string, tarball *os.File) error { |