mirror of
https://github.com/containers/podman.git
synced 2025-07-03 09:17:15 +08:00
Refactor podman export
to work with the remote client
Previously, our approach was to inspect the volume, grab its mountpoint, and tar that up, all in the CLI code. There's no reason why that has to be in the CLI - if we move it into Libpod, and add a REST endpoint to stream the tar, we can enable it for the remote client as well. As a bonus, previously, we could not properly handle volumes that needed to be mounted. Now, we can mount the volume if necessary, and as such export works with more types of volumes, including volume drivers. Signed-off-by: Matt Heon <mheon@redhat.com>
This commit is contained in:
@ -3,15 +3,11 @@ package volumes
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/common/pkg/completion"
|
||||
"github.com/containers/podman/v5/cmd/podman/common"
|
||||
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||
"github.com/containers/podman/v5/pkg/errorhandling"
|
||||
"github.com/containers/podman/v5/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -21,7 +17,6 @@ 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),
|
||||
@ -32,10 +27,7 @@ Allow content of volume to be exported into external tar.`
|
||||
)
|
||||
|
||||
var (
|
||||
// Temporary struct to hold cli values.
|
||||
cliExportOpts = struct {
|
||||
Output string
|
||||
}{}
|
||||
cliExportOpts entities.VolumeExportOptions
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -46,54 +38,20 @@ func init() {
|
||||
flags := exportCommand.Flags()
|
||||
|
||||
outputFlagName := "output"
|
||||
flags.StringVarP(&cliExportOpts.Output, outputFlagName, "o", "/dev/stdout", "Write to a specified file (default: stdout, which must be redirected)")
|
||||
flags.StringVarP(&cliExportOpts.OutputPath, 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 == "" {
|
||||
if cliExportOpts.OutputPath == "" {
|
||||
return errors.New("expects output path, use --output=[path]")
|
||||
}
|
||||
inspectOpts.Type = common.VolumeType
|
||||
volumeData, errs, err := containerEngine.VolumeInspect(ctx, args, inspectOpts)
|
||||
if err != nil {
|
||||
|
||||
if err := containerEngine.VolumeExport(ctx, args[0], cliExportOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errorhandling.JoinErrors(errs)
|
||||
}
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
@ -3,12 +3,16 @@
|
||||
package libpod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
"github.com/containers/podman/v5/libpod/lock"
|
||||
"github.com/containers/podman/v5/libpod/plugin"
|
||||
"github.com/containers/podman/v5/utils"
|
||||
"github.com/containers/storage/pkg/directory"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Volume is a libpod named volume.
|
||||
@ -294,3 +298,28 @@ func (v *Volume) Unmount() error {
|
||||
func (v *Volume) NeedsMount() bool {
|
||||
return v.needsMount()
|
||||
}
|
||||
|
||||
// Returns a ReadCloser which points to a tar of all the volume's contents.
|
||||
func (v *Volume) ExportVolume() (io.ReadCloser, error) {
|
||||
v.lock.Lock()
|
||||
err := v.mount()
|
||||
v.lock.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
v.lock.Lock()
|
||||
defer v.lock.Unlock()
|
||||
|
||||
if err := v.unmount(false); err != nil {
|
||||
logrus.Errorf("Error unmounting volume %s: %v", v.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
||||
volContents, err := utils.TarWithChroot(v.mountPoint())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating tar of volume %s contents: %w", v.Name(), err)
|
||||
}
|
||||
|
||||
return volContents, nil
|
||||
}
|
||||
|
@ -215,3 +215,22 @@ func ExistsVolume(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
utils.WriteResponse(w, http.StatusNoContent, "")
|
||||
}
|
||||
|
||||
// ExportVolume exports a volume
|
||||
func ExportVolume(w http.ResponseWriter, r *http.Request) {
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
name := utils.GetName(r)
|
||||
|
||||
vol, err := runtime.GetVolume(name)
|
||||
if err != nil {
|
||||
utils.Error(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
contents, err := vol.ExportVolume()
|
||||
if err != nil {
|
||||
utils.Error(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
utils.WriteResponse(w, http.StatusOK, contents)
|
||||
}
|
||||
|
@ -148,6 +148,31 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// $ref: "#/responses/internalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/{name}"), s.APIHandler(libpod.RemoveVolume)).Methods(http.MethodDelete)
|
||||
|
||||
// swagger:operation GET /libpod/volumes/{name}/export libpod VolumeExportLibpod
|
||||
// ---
|
||||
// tags:
|
||||
// - volumes
|
||||
// summary: Export a volume
|
||||
// parameters:
|
||||
// - in: path
|
||||
// name: name
|
||||
// type: string
|
||||
// required: true
|
||||
// description: the name or ID of the volume
|
||||
// produces:
|
||||
// - application/x-tar
|
||||
// responses:
|
||||
// 200:
|
||||
// description: no error
|
||||
// schema:
|
||||
// type: string
|
||||
// format: binary
|
||||
// 404:
|
||||
// $ref: "#/responses/volumeNotFound"
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/{name}/export"), s.APIHandler(libpod.ExportVolume)).Methods(http.MethodGet)
|
||||
|
||||
/*
|
||||
* Docker compatibility endpoints
|
||||
*/
|
||||
|
@ -2,10 +2,15 @@ package volumes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/podman/v5/pkg/bindings"
|
||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||
"github.com/containers/podman/v5/pkg/domain/entities/reports"
|
||||
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@ -139,3 +144,33 @@ func Exists(ctx context.Context, nameOrID string, options *ExistsOptions) (bool,
|
||||
|
||||
return response.IsSuccess(), nil
|
||||
}
|
||||
|
||||
// Export exports a volume to the given path
|
||||
func Export(ctx context.Context, nameOrID string, options entities.VolumeExportOptions) error {
|
||||
if options.OutputPath == "" {
|
||||
return errors.New("must provide valid path for file to write to")
|
||||
}
|
||||
|
||||
targetFile, err := os.Create(options.OutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create target file path %q: %w", options.OutputPath, err)
|
||||
}
|
||||
defer targetFile.Close()
|
||||
|
||||
conn, err := bindings.GetClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/volumes/%s/export", nil, nil, nameOrID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.IsSuccess() || response.IsRedirection() {
|
||||
if _, err := io.Copy(targetFile, response.Body); err != nil {
|
||||
return fmt.Errorf("writing volume %s contents to file: %w", nameOrID, err)
|
||||
}
|
||||
}
|
||||
return response.Process(nil)
|
||||
}
|
||||
|
@ -116,4 +116,5 @@ type ContainerEngine interface { //nolint:interfacebloat
|
||||
VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error)
|
||||
VolumeUnmount(ctx context.Context, namesOrIds []string) ([]*VolumeUnmountReport, error)
|
||||
VolumeReload(ctx context.Context) (*VolumeReloadReport, error)
|
||||
VolumeExport(ctx context.Context, nameOrID string, options VolumeExportOptions) error
|
||||
}
|
||||
|
@ -37,12 +37,13 @@ type VolumeListReport = types.VolumeListReport
|
||||
// VolumeReloadReport describes the response from reload volume plugins
|
||||
type VolumeReloadReport = types.VolumeReloadReport
|
||||
|
||||
/*
|
||||
* Docker API compatibility types
|
||||
*/
|
||||
|
||||
// VolumeMountReport describes the response from volume mount
|
||||
type VolumeMountReport = types.VolumeMountReport
|
||||
|
||||
// VolumeUnmountReport describes the response from umounting a volume
|
||||
type VolumeUnmountReport = types.VolumeUnmountReport
|
||||
|
||||
// VolumeExportOptions describes the options required to export a volume.
|
||||
type VolumeExportOptions struct {
|
||||
OutputPath string
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/containers/podman/v5/libpod"
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
@ -239,3 +241,32 @@ func (ic *ContainerEngine) VolumeReload(ctx context.Context) (*entities.VolumeRe
|
||||
report := ic.Libpod.UpdateVolumePlugins(ctx)
|
||||
return &entities.VolumeReloadReport{VolumeReload: *report}, nil
|
||||
}
|
||||
|
||||
func (ic *ContainerEngine) VolumeExport(ctx context.Context, nameOrID string, options entities.VolumeExportOptions) error {
|
||||
if options.OutputPath == "" {
|
||||
return errors.New("must provide valid path for file to write to")
|
||||
}
|
||||
|
||||
targetFile, err := os.Create(options.OutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create target file path %q: %w", options.OutputPath, err)
|
||||
}
|
||||
defer targetFile.Close()
|
||||
|
||||
vol, err := ic.Libpod.GetVolume(nameOrID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contents, err := vol.ExportVolume()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer contents.Close()
|
||||
|
||||
if _, err := io.Copy(targetFile, contents); err != nil {
|
||||
return fmt.Errorf("writing volume %s to file: %w", vol.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -113,3 +113,7 @@ func (ic *ContainerEngine) VolumeUnmount(ctx context.Context, nameOrIDs []string
|
||||
func (ic *ContainerEngine) VolumeReload(ctx context.Context) (*entities.VolumeReloadReport, error) {
|
||||
return nil, errors.New("volume reload is not supported for remote clients")
|
||||
}
|
||||
|
||||
func (ic *ContainerEngine) VolumeExport(ctx context.Context, nameOrID string, options entities.VolumeExportOptions) error {
|
||||
return volumes.Export(ic.ClientCtx, nameOrID, options)
|
||||
}
|
||||
|
@ -65,10 +65,6 @@ var _ = Describe("Podman volume create", func() {
|
||||
})
|
||||
|
||||
It("podman create and export volume", func() {
|
||||
if podmanTest.RemoteTest {
|
||||
Skip("Volume export check does not work with a remote client")
|
||||
}
|
||||
|
||||
volName := "my_vol_" + RandomString(10)
|
||||
session := podmanTest.Podman([]string{"volume", "create", volName})
|
||||
session.WaitWithDefaultTimeout()
|
||||
|
@ -260,8 +260,6 @@ EOF
|
||||
|
||||
# stdout with NULs is easier to test here than in ginkgo
|
||||
@test "podman volume export to stdout" {
|
||||
skip_if_remote "N/A on podman-remote"
|
||||
|
||||
local volname="myvol_$(random_string 10)"
|
||||
local mountpoint="/data$(random_string 8)"
|
||||
|
||||
|
Reference in New Issue
Block a user