mirror of
https://github.com/containers/podman.git
synced 2025-05-20 00:27:03 +08:00
87
cmd/podmanV2/images/save.go
Normal file
87
cmd/podmanV2/images/save.go
Normal file
@ -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)
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,13 @@ func WriteResponse(w http.ResponseWriter, code int, value interface{}) {
|
||||
w.Header().Set("Content-Type", "application/octet; charset=us-ascii")
|
||||
w.WriteHeader(code)
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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:.*
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -234,3 +234,10 @@ type ImageImportOptions struct {
|
||||
type ImageImportReport struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
type ImageSaveOptions struct {
|
||||
Compress bool
|
||||
Format string
|
||||
Output string
|
||||
Quiet bool
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user