mirror of
https://github.com/containers/podman.git
synced 2025-10-19 04:03:23 +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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/containers/common/pkg/completion"
|
"github.com/containers/common/pkg/completion"
|
||||||
"github.com/containers/podman/v5/cmd/podman/common"
|
"github.com/containers/podman/v5/cmd/podman/common"
|
||||||
"github.com/containers/podman/v5/cmd/podman/registry"
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
"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"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,7 +17,6 @@ podman volume export
|
|||||||
|
|
||||||
Allow content of volume to be exported into external tar.`
|
Allow content of volume to be exported into external tar.`
|
||||||
exportCommand = &cobra.Command{
|
exportCommand = &cobra.Command{
|
||||||
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
|
|
||||||
Use: "export [options] VOLUME",
|
Use: "export [options] VOLUME",
|
||||||
Short: "Export volumes",
|
Short: "Export volumes",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
@ -32,10 +27,7 @@ Allow content of volume to be exported into external tar.`
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Temporary struct to hold cli values.
|
cliExportOpts entities.VolumeExportOptions
|
||||||
cliExportOpts = struct {
|
|
||||||
Output string
|
|
||||||
}{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -46,54 +38,20 @@ func init() {
|
|||||||
flags := exportCommand.Flags()
|
flags := exportCommand.Flags()
|
||||||
|
|
||||||
outputFlagName := "output"
|
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)
|
_ = exportCommand.RegisterFlagCompletionFunc(outputFlagName, completion.AutocompleteDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func export(cmd *cobra.Command, args []string) error {
|
func export(cmd *cobra.Command, args []string) error {
|
||||||
var inspectOpts entities.InspectOptions
|
|
||||||
containerEngine := registry.ContainerEngine()
|
containerEngine := registry.ContainerEngine()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
if cliExportOpts.Output == "" {
|
if cliExportOpts.OutputPath == "" {
|
||||||
return errors.New("expects output path, use --output=[path]")
|
return errors.New("expects output path, use --output=[path]")
|
||||||
}
|
}
|
||||||
inspectOpts.Type = common.VolumeType
|
|
||||||
volumeData, errs, err := containerEngine.VolumeInspect(ctx, args, inspectOpts)
|
if err := containerEngine.VolumeExport(ctx, args[0], cliExportOpts); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(errs) > 0 {
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,16 @@
|
|||||||
package libpod
|
package libpod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containers/podman/v5/libpod/define"
|
"github.com/containers/podman/v5/libpod/define"
|
||||||
"github.com/containers/podman/v5/libpod/lock"
|
"github.com/containers/podman/v5/libpod/lock"
|
||||||
"github.com/containers/podman/v5/libpod/plugin"
|
"github.com/containers/podman/v5/libpod/plugin"
|
||||||
|
"github.com/containers/podman/v5/utils"
|
||||||
"github.com/containers/storage/pkg/directory"
|
"github.com/containers/storage/pkg/directory"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Volume is a libpod named volume.
|
// Volume is a libpod named volume.
|
||||||
@ -294,3 +298,28 @@ func (v *Volume) Unmount() error {
|
|||||||
func (v *Volume) NeedsMount() bool {
|
func (v *Volume) NeedsMount() bool {
|
||||||
return v.needsMount()
|
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, "")
|
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"
|
// $ref: "#/responses/internalError"
|
||||||
r.Handle(VersionedPath("/libpod/volumes/{name}"), s.APIHandler(libpod.RemoveVolume)).Methods(http.MethodDelete)
|
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
|
* Docker compatibility endpoints
|
||||||
*/
|
*/
|
||||||
|
@ -2,10 +2,15 @@ package volumes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/podman/v5/pkg/bindings"
|
"github.com/containers/podman/v5/pkg/bindings"
|
||||||
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
"github.com/containers/podman/v5/pkg/domain/entities/reports"
|
"github.com/containers/podman/v5/pkg/domain/entities/reports"
|
||||||
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
|
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
@ -139,3 +144,33 @@ func Exists(ctx context.Context, nameOrID string, options *ExistsOptions) (bool,
|
|||||||
|
|
||||||
return response.IsSuccess(), nil
|
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)
|
VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error)
|
||||||
VolumeUnmount(ctx context.Context, namesOrIds []string) ([]*VolumeUnmountReport, error)
|
VolumeUnmount(ctx context.Context, namesOrIds []string) ([]*VolumeUnmountReport, error)
|
||||||
VolumeReload(ctx context.Context) (*VolumeReloadReport, 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
|
// VolumeReloadReport describes the response from reload volume plugins
|
||||||
type VolumeReloadReport = types.VolumeReloadReport
|
type VolumeReloadReport = types.VolumeReloadReport
|
||||||
|
|
||||||
/*
|
|
||||||
* Docker API compatibility types
|
|
||||||
*/
|
|
||||||
|
|
||||||
// VolumeMountReport describes the response from volume mount
|
// VolumeMountReport describes the response from volume mount
|
||||||
type VolumeMountReport = types.VolumeMountReport
|
type VolumeMountReport = types.VolumeMountReport
|
||||||
|
|
||||||
// VolumeUnmountReport describes the response from umounting a volume
|
// VolumeUnmountReport describes the response from umounting a volume
|
||||||
type VolumeUnmountReport = types.VolumeUnmountReport
|
type VolumeUnmountReport = types.VolumeUnmountReport
|
||||||
|
|
||||||
|
// VolumeExportOptions describes the options required to export a volume.
|
||||||
|
type VolumeExportOptions struct {
|
||||||
|
OutputPath string
|
||||||
|
}
|
||||||
|
@ -6,6 +6,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/containers/podman/v5/libpod"
|
"github.com/containers/podman/v5/libpod"
|
||||||
"github.com/containers/podman/v5/libpod/define"
|
"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)
|
report := ic.Libpod.UpdateVolumePlugins(ctx)
|
||||||
return &entities.VolumeReloadReport{VolumeReload: *report}, nil
|
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) {
|
func (ic *ContainerEngine) VolumeReload(ctx context.Context) (*entities.VolumeReloadReport, error) {
|
||||||
return nil, errors.New("volume reload is not supported for remote clients")
|
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() {
|
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)
|
volName := "my_vol_" + RandomString(10)
|
||||||
session := podmanTest.Podman([]string{"volume", "create", volName})
|
session := podmanTest.Podman([]string{"volume", "create", volName})
|
||||||
session.WaitWithDefaultTimeout()
|
session.WaitWithDefaultTimeout()
|
||||||
|
@ -260,8 +260,6 @@ EOF
|
|||||||
|
|
||||||
# stdout with NULs is easier to test here than in ginkgo
|
# stdout with NULs is easier to test here than in ginkgo
|
||||||
@test "podman volume export to stdout" {
|
@test "podman volume export to stdout" {
|
||||||
skip_if_remote "N/A on podman-remote"
|
|
||||||
|
|
||||||
local volname="myvol_$(random_string 10)"
|
local volname="myvol_$(random_string 10)"
|
||||||
local mountpoint="/data$(random_string 8)"
|
local mountpoint="/data$(random_string 8)"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user