Skip to content

Commit 9887645

Browse files
committed
Refactor volume import to support the remote client
As with `volume export`, this was coded up exclusively in cmd/ instead of in libpod. Move it into Libpod, add a REST endpoint, add bindings, and now everything talks using the ContainerEngine wiring. Also similar to `volume export` this also makes things work much better with volumes that require mounting - we can now guarantee they're actually mounted, instead of just hoping. Includes some refactoring of `volume export` as well, to simplify its implementation and ensure both Import and Export work with readers/writers, as opposed to just files. Fixes #26409 Signed-off-by: Matt Heon <[email protected]>
1 parent 63bf454 commit 9887645

File tree

16 files changed

+175
-148
lines changed

16 files changed

+175
-148
lines changed

cmd/podman/volumes/export.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package volumes
33
import (
44
"context"
55
"errors"
6+
"fmt"
7+
"os"
68

79
"github.com/containers/common/pkg/completion"
810
"github.com/containers/podman/v5/cmd/podman/common"
@@ -27,7 +29,7 @@ Allow content of volume to be exported into external tar.`
2729
)
2830

2931
var (
30-
cliExportOpts entities.VolumeExportOptions
32+
targetPath string
3133
)
3234

3335
func init() {
@@ -38,20 +40,30 @@ func init() {
3840
flags := exportCommand.Flags()
3941

4042
outputFlagName := "output"
41-
flags.StringVarP(&cliExportOpts.OutputPath, outputFlagName, "o", "/dev/stdout", "Write to a specified file (default: stdout, which must be redirected)")
43+
flags.StringVarP(&targetPath, outputFlagName, "o", "", "Write to a specified file (default: stdout, which must be redirected)")
4244
_ = exportCommand.RegisterFlagCompletionFunc(outputFlagName, completion.AutocompleteDefault)
4345
}
4446

4547
func export(cmd *cobra.Command, args []string) error {
4648
containerEngine := registry.ContainerEngine()
4749
ctx := context.Background()
4850

49-
if cliExportOpts.OutputPath == "" {
50-
return errors.New("expects output path, use --output=[path]")
51+
if targetPath == "" && cmd.Flag("output").Changed {
52+
return errors.New("must provide valid path for file to write to")
5153
}
5254

53-
if err := containerEngine.VolumeExport(ctx, args[0], cliExportOpts); err != nil {
54-
return err
55+
exportOpts := entities.VolumeExportOptions{}
56+
57+
if targetPath != "" {
58+
targetFile, err := os.Create(targetPath)
59+
if err != nil {
60+
return fmt.Errorf("unable to create target file path %q: %w", targetPath, err)
61+
}
62+
defer targetFile.Close()
63+
exportOpts.Output = targetFile
64+
} else {
65+
exportOpts.Output = os.Stdout
5566
}
56-
return nil
67+
68+
return containerEngine.VolumeExport(ctx, args[0], exportOpts)
5769
}

cmd/podman/volumes/import.go

Lines changed: 15 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
package volumes
22

33
import (
4-
"errors"
4+
"context"
55
"fmt"
66
"os"
77

88
"github.com/containers/podman/v5/cmd/podman/common"
99
"github.com/containers/podman/v5/cmd/podman/parse"
1010
"github.com/containers/podman/v5/cmd/podman/registry"
1111
"github.com/containers/podman/v5/pkg/domain/entities"
12-
"github.com/containers/podman/v5/pkg/errorhandling"
13-
"github.com/containers/podman/v5/utils"
1412
"github.com/spf13/cobra"
1513
)
1614

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

3936
func importVol(cmd *cobra.Command, args []string) error {
40-
var inspectOpts entities.InspectOptions
41-
var tarFile *os.File
42-
containerEngine := registry.ContainerEngine()
43-
ctx := registry.Context()
44-
// create a slice of volumes since inspect expects slice as arg
45-
volumes := []string{args[0]}
46-
tarPath := args[1]
37+
opts := entities.VolumeImportOptions{}
4738

48-
if tarPath != "-" {
49-
err := parse.ValidateFileName(tarPath)
50-
if err != nil {
39+
filepath := args[1]
40+
if filepath == "-" {
41+
opts.Input = os.Stdin
42+
} else {
43+
if err := parse.ValidateFileName(filepath); err != nil {
5144
return err
5245
}
5346

54-
// open tar file
55-
tarFile, err = os.Open(tarPath)
47+
targetFile, err := os.Open(filepath)
5648
if err != nil {
57-
return err
49+
return fmt.Errorf("unable open input file: %w", err)
5850
}
59-
} else {
60-
tarFile = os.Stdin
51+
defer targetFile.Close()
52+
opts.Input = targetFile
6153
}
6254

63-
inspectOpts.Type = common.VolumeType
64-
inspectOpts.Type = common.VolumeType
65-
volumeData, errs, err := containerEngine.VolumeInspect(ctx, volumes, inspectOpts)
66-
if err != nil {
67-
return err
68-
}
69-
if len(errs) > 0 {
70-
return errorhandling.JoinErrors(errs)
71-
}
72-
if len(volumeData) < 1 {
73-
return errors.New("no volume data found")
74-
}
75-
mountPoint := volumeData[0].VolumeConfigResponse.Mountpoint
76-
driver := volumeData[0].VolumeConfigResponse.Driver
77-
volumeOptions := volumeData[0].VolumeConfigResponse.Options
78-
volumeMountStatus, err := containerEngine.VolumeMounted(ctx, args[0])
79-
if err != nil {
80-
return err
81-
}
82-
if mountPoint == "" {
83-
return errors.New("volume is not mounted anywhere on host")
84-
}
85-
// Check if volume is using external plugin and export only if volume is mounted
86-
if driver != "" && driver != "local" {
87-
if !volumeMountStatus.Value {
88-
return fmt.Errorf("volume is using a driver %s and volume is not mounted on %s", driver, mountPoint)
89-
}
90-
}
91-
// Check if volume is using `local` driver and has mount options type other than tmpfs
92-
if driver == "local" {
93-
if mountOptionType, ok := volumeOptions["type"]; ok {
94-
if mountOptionType != "tmpfs" && !volumeMountStatus.Value {
95-
return fmt.Errorf("volume is using a driver %s and volume is not mounted on %s", driver, mountPoint)
96-
}
97-
}
98-
}
99-
// dont care if volume is mounted or not we are gonna import everything to mountPoint
100-
return utils.UntarToFileSystem(mountPoint, tarFile, nil)
55+
containerEngine := registry.ContainerEngine()
56+
ctx := context.Background()
57+
58+
return containerEngine.VolumeImport(ctx, args[0], opts)
10159
}

docs/source/markdown/podman-volume-export.1.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ podman\-volume\-export - Export volume to external tar
1212
on the local machine. **podman volume export** writes to STDOUT by default and can be
1313
redirected to a file using the `--output` flag.
1414

15-
Note: Following command is not supported by podman-remote.
16-
1715
**podman volume export [OPTIONS] VOLUME**
1816

1917
## OPTIONS

docs/source/markdown/podman-volume-import.1.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ The contents of the volume is merged with the content of the tarball with the la
1414

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

17-
Note: Following command is not supported by podman-remote.
18-
1917
#### **--help**
2018

2119
Print usage statement

libpod/volume.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/containers/podman/v5/libpod/lock"
1212
"github.com/containers/podman/v5/libpod/plugin"
1313
"github.com/containers/podman/v5/utils"
14+
"github.com/containers/storage/pkg/archive"
1415
"github.com/containers/storage/pkg/directory"
1516
"github.com/sirupsen/logrus"
1617
)
@@ -299,10 +300,12 @@ func (v *Volume) NeedsMount() bool {
299300
return v.needsMount()
300301
}
301302

303+
// Export volume to tar.
302304
// Returns a ReadCloser which points to a tar of all the volume's contents.
303-
func (v *Volume) ExportVolume() (io.ReadCloser, error) {
305+
func (v *Volume) Export() (io.ReadCloser, error) {
304306
v.lock.Lock()
305307
err := v.mount()
308+
mountPoint := v.mountPoint()
306309
v.lock.Unlock()
307310
if err != nil {
308311
return nil, err
@@ -316,10 +319,35 @@ func (v *Volume) ExportVolume() (io.ReadCloser, error) {
316319
}
317320
}()
318321

319-
volContents, err := utils.TarWithChroot(v.mountPoint())
322+
volContents, err := utils.TarWithChroot(mountPoint)
320323
if err != nil {
321324
return nil, fmt.Errorf("creating tar of volume %s contents: %w", v.Name(), err)
322325
}
323326

324327
return volContents, nil
325328
}
329+
330+
// Import a volume from a tar file, provided as an io.Reader.
331+
func (v *Volume) Import(r io.Reader) error {
332+
v.lock.Lock()
333+
err := v.mount()
334+
mountPoint := v.mountPoint()
335+
v.lock.Unlock()
336+
if err != nil {
337+
return err
338+
}
339+
defer func() {
340+
v.lock.Lock()
341+
defer v.lock.Unlock()
342+
343+
if err := v.unmount(false); err != nil {
344+
logrus.Errorf("Error unmounting volume %s: %v", v.Name(), err)
345+
}
346+
}()
347+
348+
if err := archive.Untar(r, mountPoint, nil); err != nil {
349+
return fmt.Errorf("extracting into volume %s: %w", v.Name(), err)
350+
}
351+
352+
return nil
353+
}

pkg/api/handlers/libpod/volumes.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ package libpod
44

55
import (
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"net/http"
910
"net/url"
1011

11-
"errors"
12-
1312
"github.com/containers/podman/v5/libpod"
1413
"github.com/containers/podman/v5/libpod/define"
1514
"github.com/containers/podman/v5/pkg/api/handlers/utils"
@@ -223,14 +222,39 @@ func ExportVolume(w http.ResponseWriter, r *http.Request) {
223222

224223
vol, err := runtime.GetVolume(name)
225224
if err != nil {
226-
utils.Error(w, http.StatusNotFound, err)
225+
utils.VolumeNotFound(w, name, err)
227226
return
228227
}
229228

230-
contents, err := vol.ExportVolume()
229+
contents, err := vol.Export()
231230
if err != nil {
232231
utils.Error(w, http.StatusInternalServerError, err)
233232
return
234233
}
235234
utils.WriteResponse(w, http.StatusOK, contents)
236235
}
236+
237+
// ImportVolume imports a volume
238+
func ImportVolume(w http.ResponseWriter, r *http.Request) {
239+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
240+
name := utils.GetName(r)
241+
242+
vol, err := runtime.GetVolume(name)
243+
if err != nil {
244+
utils.VolumeNotFound(w, name, err)
245+
return
246+
}
247+
248+
if r.Body == nil {
249+
utils.Error(w, http.StatusInternalServerError, errors.New("must provide tar file to import in request body"))
250+
return
251+
}
252+
defer r.Body.Close()
253+
254+
if err := vol.Import(r.Body); err != nil {
255+
utils.Error(w, http.StatusInternalServerError, err)
256+
return
257+
}
258+
259+
utils.WriteResponse(w, http.StatusNoContent, "")
260+
}

pkg/api/server/register_volumes.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,47 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
161161
// description: the name or ID of the volume
162162
// produces:
163163
// - application/x-tar
164-
// responses:
165-
// 200:
164+
// responses:
165+
// 200:
166166
// description: no error
167167
// schema:
168168
// type: string
169-
// format: binary
169+
// format: binary
170170
// 404:
171171
// $ref: "#/responses/volumeNotFound"
172172
// 500:
173173
// $ref: "#/responses/internalError"
174174
r.Handle(VersionedPath("/libpod/volumes/{name}/export"), s.APIHandler(libpod.ExportVolume)).Methods(http.MethodGet)
175175

176+
// swagger:operation POST /libpod/volumes/{name}/import libpod VolumeImportLibpod
177+
// ---
178+
// tags:
179+
// - volumes
180+
// summary: Populate a volume by importing provided tar
181+
// parameters:
182+
// - in: path
183+
// name: name
184+
// type: string
185+
// required: true
186+
// description: the name or ID of the volume
187+
// - in: body
188+
// name: inputStream
189+
// description: |
190+
// An uncompressed tar archive
191+
// schema:
192+
// type: string
193+
// format: binary
194+
// produces:
195+
// - application/json
196+
// responses:
197+
// 204:
198+
// description: Successful import
199+
// 404:
200+
// $ref: "#/responses/volumeNotFound"
201+
// 500:
202+
// $ref: "#/responses/internalError"
203+
r.Handle(VersionedPath("/libpod/volumes/{name}/import"), s.APIHandler(libpod.ImportVolume)).Methods(http.MethodPost)
204+
176205
/*
177206
* Docker compatibility endpoints
178207
*/

0 commit comments

Comments
 (0)