Skip to content

Commit

Permalink
volumes: Add support for exporting volumes to external tar
Browse files Browse the repository at this point in the history
Adds support for transferring data between systems and backing up systems.
Use cases: recover from disasters or move data between machines.

Signed-off-by: flouthoc <[email protected]>
  • Loading branch information
flouthoc committed Aug 23, 2021
1 parent 30b036c commit edddfe8
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 0 deletions.
96 changes: 96 additions & 0 deletions cmd/podman/volumes/export.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions docs/source/markdown/podman-volume-export.1.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions docs/source/markdown/podman-volume.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 2 additions & 0 deletions docs/source/volume.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions libpod/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions pkg/domain/entities/engine_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions pkg/domain/infra/abi/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions pkg/domain/infra/tunnel/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
19 changes: 19 additions & 0 deletions test/e2e/volume_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit edddfe8

Please sign in to comment.