Merge pull request #5672 from baude/v2save

podmanv2 save image
This commit is contained in:
OpenShift Merge Robot
2020-04-03 22:41:18 +02:00
committed by GitHub
10 changed files with 216 additions and 16 deletions

View 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)
}

View File

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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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:.*

View File

@ -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

View File

@ -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
}

View File

@ -234,3 +234,10 @@ type ImageImportOptions struct {
type ImageImportReport struct {
Id string
}
type ImageSaveOptions struct {
Compress bool
Format string
Output string
Quiet bool
}

View File

@ -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)
}

View File

@ -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)
}