summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podmanV2/images/save.go87
-rw-r--r--libpod/define/config.go7
-rw-r--r--pkg/api/handlers/libpod/images.go53
-rw-r--r--pkg/api/handlers/utils/handler.go7
-rw-r--r--pkg/api/server/register_images.go2
-rw-r--r--pkg/bindings/images/images.go7
-rw-r--r--pkg/domain/entities/engine_image.go1
-rw-r--r--pkg/domain/entities/images.go7
-rw-r--r--pkg/domain/infra/abi/images.go8
-rw-r--r--pkg/domain/infra/tunnel/images.go53
10 files changed, 216 insertions, 16 deletions
diff --git a/cmd/podmanV2/images/save.go b/cmd/podmanV2/images/save.go
new file mode 100644
index 000000000..ae39b7bce
--- /dev/null
+++ b/cmd/podmanV2/images/save.go
@@ -0,0 +1,87 @@
+package images
+
+import (
+ "context"
+ "os"
+ "strings"
+
+ "github.com/containers/libpod/libpod/define"
+
+ "github.com/containers/libpod/cmd/podmanV2/parse"
+ "github.com/containers/libpod/cmd/podmanV2/registry"
+ "github.com/containers/libpod/pkg/domain/entities"
+ "github.com/containers/libpod/pkg/util"
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+var validFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive}
+
+var (
+ saveDescription = `Save an image to docker-archive or oci-archive on the local machine. Default is docker-archive.`
+
+ saveCommand = &cobra.Command{
+ Use: "save [flags] IMAGE",
+ Short: "Save image to an archive",
+ Long: saveDescription,
+ PersistentPreRunE: preRunE,
+ RunE: save,
+ Args: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return errors.Errorf("need at least 1 argument")
+ }
+ format, err := cmd.Flags().GetString("format")
+ if err != nil {
+ return err
+ }
+ if !util.StringInSlice(format, validFormats) {
+ return errors.Errorf("format value must be one of %s", strings.Join(validFormats, " "))
+ }
+ return nil
+ },
+ Example: `podman save --quiet -o myimage.tar imageID
+ podman save --format docker-dir -o ubuntu-dir ubuntu
+ podman save > alpine-all.tar alpine:latest`,
+ }
+)
+
+var (
+ saveOpts entities.ImageSaveOptions
+)
+
+func init() {
+ registry.Commands = append(registry.Commands, registry.CliCommand{
+ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+ Command: saveCommand,
+ })
+ flags := saveCommand.Flags()
+ flags.BoolVar(&saveOpts.Compress, "compress", false, "Compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)")
+ flags.StringVar(&saveOpts.Format, "format", define.V2s2Archive, "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-archive, docker-dir (directory with v2s2 manifest type)")
+ flags.StringVarP(&saveOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)")
+ flags.BoolVarP(&saveOpts.Quiet, "quiet", "q", false, "Suppress the output")
+
+}
+
+func save(cmd *cobra.Command, args []string) error {
+ var (
+ tags []string
+ )
+ if cmd.Flag("compress").Changed && (saveOpts.Format != define.OCIManifestDir && saveOpts.Format != define.V2s2ManifestDir && saveOpts.Format == "") {
+ return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'")
+ }
+ if len(saveOpts.Output) == 0 {
+ fi := os.Stdout
+ if terminal.IsTerminal(int(fi.Fd())) {
+ return errors.Errorf("refusing to save to terminal. Use -o flag or redirect")
+ }
+ saveOpts.Output = "/dev/stdout"
+ }
+ if err := parse.ValidateFileName(saveOpts.Output); err != nil {
+ return err
+ }
+ if len(args) > 1 {
+ tags = args[1:]
+ }
+ return registry.ImageEngine().Save(context.Background(), args[0], tags, saveOpts)
+}
diff --git a/libpod/define/config.go b/libpod/define/config.go
index 5598f97a3..7b967f17d 100644
--- a/libpod/define/config.go
+++ b/libpod/define/config.go
@@ -26,3 +26,10 @@ type InfoData struct {
// VolumeDriverLocal is the "local" volume driver. It is managed by libpod
// itself.
const VolumeDriverLocal = "local"
+
+const (
+ OCIManifestDir = "oci-dir"
+ OCIArchive = "oci-archive"
+ V2s2ManifestDir = "docker-dir"
+ V2s2Archive = "docker-archive"
+)
diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go
index e7f20854c..850de4598 100644
--- a/pkg/api/handlers/libpod/images.go
+++ b/pkg/api/handlers/libpod/images.go
@@ -16,12 +16,14 @@ import (
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/types"
"github.com/containers/libpod/libpod"
+ "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/libpod/image"
image2 "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/util"
+ utils2 "github.com/containers/libpod/utils"
"github.com/gorilla/schema"
"github.com/pkg/errors"
)
@@ -161,13 +163,16 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
}
func ExportImage(w http.ResponseWriter, r *http.Request) {
+ var (
+ output string
+ )
runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
Compress bool `schema:"compress"`
Format string `schema:"format"`
}{
- Format: "docker-archive",
+ Format: define.OCIArchive,
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
@@ -175,14 +180,27 @@ func ExportImage(w http.ResponseWriter, r *http.Request) {
errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
return
}
-
- tmpfile, err := ioutil.TempFile("", "api.tar")
- if err != nil {
- utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
- return
- }
- if err := tmpfile.Close(); err != nil {
- utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile"))
+ switch query.Format {
+ case define.OCIArchive, define.V2s2Archive:
+ tmpfile, err := ioutil.TempFile("", "api.tar")
+ if err != nil {
+ utils.Error(w, "unable to create tmpfile", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
+ return
+ }
+ output = tmpfile.Name()
+ if err := tmpfile.Close(); err != nil {
+ utils.Error(w, "unable to close tmpfile", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile"))
+ return
+ }
+ case define.OCIManifestDir, define.V2s2ManifestDir:
+ tmpdir, err := ioutil.TempDir("", "save")
+ if err != nil {
+ utils.Error(w, "unable to create tmpdir", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempdir"))
+ return
+ }
+ output = tmpdir
+ default:
+ utils.Error(w, "unknown format", http.StatusInternalServerError, errors.Errorf("unknown format %q", query.Format))
return
}
name := utils.GetName(r)
@@ -192,17 +210,28 @@ func ExportImage(w http.ResponseWriter, r *http.Request) {
return
}
- if err := newImage.Save(r.Context(), name, query.Format, tmpfile.Name(), []string{}, false, query.Compress); err != nil {
+ if err := newImage.Save(r.Context(), name, query.Format, output, []string{}, false, query.Compress); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
return
}
- rdr, err := os.Open(tmpfile.Name())
+ defer os.RemoveAll(output)
+ // if dir format, we need to tar it
+ if query.Format == "oci-dir" || query.Format == "docker-dir" {
+ rdr, err := utils2.Tar(output)
+ if err != nil {
+ utils.InternalServerError(w, err)
+ return
+ }
+ defer rdr.Close()
+ utils.WriteResponse(w, http.StatusOK, rdr)
+ return
+ }
+ rdr, err := os.Open(output)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile"))
return
}
defer rdr.Close()
- defer os.Remove(tmpfile.Name())
utils.WriteResponse(w, http.StatusOK, rdr)
}
diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go
index 32b8c5b0a..b5bd488fb 100644
--- a/pkg/api/handlers/utils/handler.go
+++ b/pkg/api/handlers/utils/handler.go
@@ -46,6 +46,13 @@ func WriteResponse(w http.ResponseWriter, code int, value interface{}) {
if _, err := io.Copy(w, v); err != nil {
logrus.Errorf("unable to copy to response: %q", err)
}
+ case io.Reader:
+ w.Header().Set("Content-Type", "application/x-tar")
+ w.WriteHeader(code)
+
+ if _, err := io.Copy(w, v); err != nil {
+ logrus.Errorf("unable to copy to response: %q", err)
+ }
default:
WriteJSON(w, code, value)
}
diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go
index e4e46025b..d45423096 100644
--- a/pkg/api/server/register_images.go
+++ b/pkg/api/server/register_images.go
@@ -955,7 +955,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
// tags:
// - images
// summary: Export an image
- // description: Export an image as a tarball
+ // description: Export an image
// parameters:
// - in: path
// name: name:.*
diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go
index dcb568d6b..1b3df609b 100644
--- a/pkg/bindings/images/images.go
+++ b/pkg/bindings/images/images.go
@@ -146,11 +146,12 @@ func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, c
if err != nil {
return err
}
- if err := response.Process(nil); err != nil {
+
+ if response.StatusCode/100 == 2 || response.StatusCode/100 == 3 {
+ _, err = io.Copy(w, response.Body)
return err
}
- _, err = io.Copy(w, response.Body)
- return err
+ return nil
}
// Prune removes unused images from local storage. The optional filters can be used to further
diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go
index 04b9d34e6..a28bfc548 100644
--- a/pkg/domain/entities/engine_image.go
+++ b/pkg/domain/entities/engine_image.go
@@ -17,4 +17,5 @@ type ImageEngine interface {
Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error)
Import(ctx context.Context, opts ImageImportOptions) (*ImageImportReport, error)
Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error
+ Save(ctx context.Context, nameOrId string, tags []string, options ImageSaveOptions) error
}
diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go
index d66de3c5e..bc8a34c13 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -234,3 +234,10 @@ type ImageImportOptions struct {
type ImageImportReport struct {
Id string
}
+
+type ImageSaveOptions struct {
+ Compress bool
+ Format string
+ Output string
+ Quiet bool
+}
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index 94008f287..9d706a112 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -405,3 +405,11 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti
}
return &entities.ImageImportReport{Id: id}, nil
}
+
+func (ir *ImageEngine) Save(ctx context.Context, nameOrId string, tags []string, options entities.ImageSaveOptions) error {
+ newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId)
+ if err != nil {
+ return err
+ }
+ return newImage.Save(ctx, nameOrId, options.Format, options.Output, tags, options.Quiet, options.Compress)
+}
diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go
index 028603d98..516914a68 100644
--- a/pkg/domain/infra/tunnel/images.go
+++ b/pkg/domain/infra/tunnel/images.go
@@ -2,12 +2,14 @@ package tunnel
import (
"context"
+ "io/ioutil"
"os"
"github.com/containers/image/v5/docker/reference"
images "github.com/containers/libpod/pkg/bindings/images"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/domain/utils"
+ utils2 "github.com/containers/libpod/utils"
"github.com/pkg/errors"
)
@@ -188,3 +190,54 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti
func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error {
return images.Push(ir.ClientCxt, source, destination, options)
}
+
+func (ir *ImageEngine) Save(ctx context.Context, nameOrId string, tags []string, options entities.ImageSaveOptions) error {
+ var (
+ f *os.File
+ err error
+ )
+
+ switch options.Format {
+ case "oci-dir", "docker-dir":
+ f, err = ioutil.TempFile("", "podman_save")
+ if err == nil {
+ defer func() { _ = os.Remove(f.Name()) }()
+ }
+ default:
+ f, err = os.Create(options.Output)
+ }
+ if err != nil {
+ return err
+ }
+
+ exErr := images.Export(ir.ClientCxt, nameOrId, f, &options.Format, &options.Compress)
+ if err := f.Close(); err != nil {
+ return err
+ }
+ if exErr != nil {
+ return exErr
+ }
+
+ if options.Format != "oci-dir" && options.Format != "docker-dir" {
+ return nil
+ }
+
+ f, err = os.Open(f.Name())
+ if err != nil {
+ return err
+ }
+ info, err := os.Stat(options.Output)
+ switch {
+ case err == nil:
+ if info.Mode().IsRegular() {
+ return errors.Errorf("%q already exists as a regular file", options.Output)
+ }
+ case os.IsNotExist(err):
+ if err := os.Mkdir(options.Output, 0755); err != nil {
+ return err
+ }
+ default:
+ return err
+ }
+ return utils2.UntarToFileSystem(options.Output, f, nil)
+}