Skip to content

Add remote support for podman volume import and podman volume export #26434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 20 additions & 47 deletions cmd/podman/volumes/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import (
"context"
"errors"
"fmt"
"os"

"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"
)

Expand All @@ -21,7 +19,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),
Expand All @@ -32,10 +29,7 @@ Allow content of volume to be exported into external tar.`
)

var (
// Temporary struct to hold cli values.
cliExportOpts = struct {
Output string
}{}
targetPath string
)

func init() {
Expand All @@ -46,54 +40,33 @@ 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(&targetPath, outputFlagName, "o", "", "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]")
if targetPath == "" && cmd.Flag("output").Changed {
return errors.New("must provide valid path for file to write to")
}
inspectOpts.Type = common.VolumeType
volumeData, errs, err := containerEngine.VolumeInspect(ctx, args, inspectOpts)
if 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)

exportOpts := entities.VolumeExportOptions{}

if targetPath != "" {
targetFile, err := os.Create(targetPath)
if err != nil {
return fmt.Errorf("unable to create target file path %q: %w", targetPath, err)
}
defer targetFile.Close()
exportOpts.Output = targetFile
} else {
exportOpts.Output = os.Stdout
}
// 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)
}
}

if err := containerEngine.VolumeExport(ctx, args[0], exportOpts); err != nil {
return err
Comment on lines +68 to +69
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit might as well just return directly here

}
logrus.Debugf("Exporting volume data from %s to %s", mountPoint, cliExportOpts.Output)
err = utils.CreateTarFromSrc(mountPoint, cliExportOpts.Output)
return err
return nil
}
71 changes: 16 additions & 55 deletions cmd/podman/volumes/import.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package volumes

import (
"errors"
"context"
"fmt"
"os"

"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/parse"
"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/spf13/cobra"
)

var (
importDescription = `Imports contents into a podman volume from specified tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz).`
importCommand = &cobra.Command{
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
Use: "import VOLUME [SOURCE]",
Short: "Import a tarball contents into a podman volume",
Long: importDescription,
Expand All @@ -37,65 +34,29 @@ func init() {
}

func importVol(cmd *cobra.Command, args []string) error {
var inspectOpts entities.InspectOptions
var tarFile *os.File
containerEngine := registry.ContainerEngine()
ctx := registry.Context()
// create a slice of volumes since inspect expects slice as arg
volumes := []string{args[0]}
tarPath := args[1]
opts := entities.VolumeImportOptions{}

if tarPath != "-" {
err := parse.ValidateFileName(tarPath)
if err != nil {
filepath := args[1]
if filepath == "-" {
opts.Input = os.Stdin
} else {
if err := parse.ValidateFileName(filepath); err != nil {
return err
}

// open tar file
tarFile, err = os.Open(tarPath)
targetFile, err := os.Open(filepath)
if err != nil {
return err
return fmt.Errorf("unable open input file: %w", err)
}
} else {
tarFile = os.Stdin
defer targetFile.Close()
opts.Input = targetFile
}

inspectOpts.Type = common.VolumeType
inspectOpts.Type = common.VolumeType
volumeData, errs, err := containerEngine.VolumeInspect(ctx, volumes, inspectOpts)
if 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 {
containerEngine := registry.ContainerEngine()
ctx := context.Background()

if err := containerEngine.VolumeImport(ctx, args[0], opts); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same thing could return directly with the 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)
}
}
}
// dont care if volume is mounted or not we are gonna import everything to mountPoint
return utils.UntarToFileSystem(mountPoint, tarFile, nil)
return nil
}
2 changes: 0 additions & 2 deletions docs/source/markdown/podman-volume-export.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ podman\-volume\-export - Export volume to external tar
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
Expand Down
2 changes: 0 additions & 2 deletions docs/source/markdown/podman-volume-import.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ The contents of the volume is merged with the content of the tarball with the la

The given volume must already exist and is not created by podman volume import.

Note: Following command is not supported by podman-remote.

#### **--help**

Print usage statement
Expand Down
57 changes: 57 additions & 0 deletions libpod/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
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/archive"
"github.com/containers/storage/pkg/directory"
"github.com/sirupsen/logrus"
)

// Volume is a libpod named volume.
Expand Down Expand Up @@ -294,3 +299,55 @@ func (v *Volume) Unmount() error {
func (v *Volume) NeedsMount() bool {
return v.needsMount()
}

// Export volume to tar.
// Returns a ReadCloser which points to a tar of all the volume's contents.
func (v *Volume) Export() (io.ReadCloser, error) {
v.lock.Lock()
err := v.mount()
mountPoint := v.mountPoint()
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)
}
}()
Comment on lines +307 to +320
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we actually want to unlock, I realize this is what the code did before but like in what world is it sane to allow volume rm while export/import is running

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We unlock for commands like podman cp so I see no reason not to here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhh, I guess fair enough as we don't want things like a volume ls being blocked for a long time but when I make the listing parts not holding the lock I like to revalue.


volContents, err := utils.TarWithChroot(mountPoint)
if err != nil {
return nil, fmt.Errorf("creating tar of volume %s contents: %w", v.Name(), err)
}

return volContents, nil
}

// Import a volume from a tar file, provided as an io.Reader.
func (v *Volume) Import(r io.Reader) error {
v.lock.Lock()
err := v.mount()
mountPoint := v.mountPoint()
v.lock.Unlock()
if err != nil {
return 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)
}
}()
Comment on lines +332 to +346
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here


if err := archive.Untar(r, mountPoint, nil); err != nil {
return fmt.Errorf("extracting into volume %s: %w", v.Name(), err)
}

return nil
}
47 changes: 45 additions & 2 deletions pkg/api/handlers/libpod/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ package libpod

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"

"errors"

"github.com/containers/podman/v5/libpod"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/api/handlers/utils"
Expand Down Expand Up @@ -215,3 +214,47 @@ 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.VolumeNotFound(w, name, err)
return
}

contents, err := vol.Export()
if err != nil {
utils.Error(w, http.StatusInternalServerError, err)
return
}
utils.WriteResponse(w, http.StatusOK, contents)
}

// ImportVolume imports a volume
func ImportVolume(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.VolumeNotFound(w, name, err)
return
}

if r.Body == nil {
utils.Error(w, http.StatusInternalServerError, errors.New("must provide tar file to import in request body"))
return
}
defer r.Body.Close()

if err := vol.Import(r.Body); err != nil {
utils.Error(w, http.StatusInternalServerError, err)
return
}

utils.WriteResponse(w, http.StatusNoContent, "")
}
Loading