mirror of https://github.com/containers/podman.git
				
				
				
			Merge pull request #11290 from flouthoc/volume-export
volumes: Add support for `volume export` which allows exporting content to external path.
This commit is contained in:
		
						commit
						90cf78b199
					
				|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  | @ -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.                                                           | | | 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.                                              | | | 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.                               | | | 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.                                                | | | 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.                                                     | | | prune   | [podman-volume-prune(1)](podman-volume-prune.1.md)     | Remove all unused volumes.                                                     | | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ Volume | ||||||
| 
 | 
 | ||||||
| :doc:`exists <markdown/podman-volume-exists.1>` Check if the given volume exists | :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:`inspect <markdown/podman-volume-inspect.1>` Display detailed information on one or more volumes | ||||||
| 
 | 
 | ||||||
| :doc:`ls <markdown/podman-volume-ls.1>` List volumes | :doc:`ls <markdown/podman-volume-ls.1>` List volumes | ||||||
|  |  | ||||||
|  | @ -139,6 +139,17 @@ func (v *Volume) MountPoint() (string, error) { | ||||||
| 	return v.mountPoint(), nil | 	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
 | // Internal-only helper for volume mountpoint
 | ||||||
| func (v *Volume) mountPoint() string { | func (v *Volume) mountPoint() string { | ||||||
| 	if v.UsesVolumeDriver() { | 	if v.UsesVolumeDriver() { | ||||||
|  |  | ||||||
|  | @ -92,6 +92,7 @@ type ContainerEngine interface { | ||||||
| 	Version(ctx context.Context) (*SystemVersionReport, error) | 	Version(ctx context.Context) (*SystemVersionReport, error) | ||||||
| 	VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error) | 	VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error) | ||||||
| 	VolumeExists(ctx context.Context, namesOrID string) (*BoolReport, 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) | 	VolumeInspect(ctx context.Context, namesOrIds []string, opts InspectOptions) ([]*VolumeInspectReport, []error, error) | ||||||
| 	VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) | 	VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) | ||||||
| 	VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*reports.PruneReport, error) | 	VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*reports.PruneReport, error) | ||||||
|  |  | ||||||
|  | @ -162,3 +162,19 @@ func (ic *ContainerEngine) VolumeExists(ctx context.Context, nameOrID string) (* | ||||||
| 	} | 	} | ||||||
| 	return &entities.BoolReport{Value: exists}, nil | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -91,3 +91,9 @@ func (ic *ContainerEngine) VolumeExists(ctx context.Context, nameOrID string) (* | ||||||
| 		Value: exists, | 		Value: exists, | ||||||
| 	}, nil | 	}, 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") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -60,6 +60,25 @@ var _ = Describe("Podman volume create", func() { | ||||||
| 		Expect(len(check.OutputToStringArray())).To(Equal(1)) | 		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() { | 	It("podman create volume with bad volume option", func() { | ||||||
| 		session := podmanTest.Podman([]string{"volume", "create", "--opt", "badOpt=bad"}) | 		session := podmanTest.Podman([]string{"volume", "create", "--opt", "badOpt=bad"}) | ||||||
| 		session.WaitWithDefaultTimeout() | 		session.WaitWithDefaultTimeout() | ||||||
|  |  | ||||||
|  | @ -107,6 +107,16 @@ func UntarToFileSystem(dest string, tarball *os.File, options *archive.TarOption | ||||||
| 	return archive.Untar(tarball, dest, options) | 	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
 | // TarToFilesystem creates a tarball from source and writes to an os.file
 | ||||||
| // provided
 | // provided
 | ||||||
| func TarToFilesystem(source string, tarball *os.File) error { | func TarToFilesystem(source string, tarball *os.File) error { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue