From 711ac9305185e645f2970d09ff76c2761132202a Mon Sep 17 00:00:00 2001 From: baude Date: Tue, 19 Feb 2019 10:08:43 -0600 Subject: podman-remote save [image] Add the ability to save an image from the remote-host to the remote-client. Signed-off-by: baude --- libpod/adapter/runtime.go | 12 +++++ libpod/adapter/runtime_remote.go | 95 +++++++++++++++++++++++++++++++++------- libpod/image/image.go | 67 ++++++++++++++++++++++++++++ libpod/image/utils.go | 26 +++++++++++ 4 files changed, 183 insertions(+), 17 deletions(-) (limited to 'libpod') diff --git a/libpod/adapter/runtime.go b/libpod/adapter/runtime.go index 02ef9af07..b12f63cdc 100644 --- a/libpod/adapter/runtime.go +++ b/libpod/adapter/runtime.go @@ -310,3 +310,15 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) { return r.Runtime.PruneVolumes(ctx) } + +// SaveImage is a wrapper function for saving an image to the local filesystem +func (r *LocalRuntime) SaveImage(ctx context.Context, c *cliconfig.SaveValues) error { + source := c.InputArgs[0] + additionalTags := c.InputArgs[1:] + + newImage, err := r.Runtime.ImageRuntime().NewFromLocal(source) + if err != nil { + return err + } + return newImage.Save(ctx, source, c.Format, c.Output, additionalTags, c.Quiet, c.Compress) +} diff --git a/libpod/adapter/runtime_remote.go b/libpod/adapter/runtime_remote.go index b1d4d4d25..a79f93079 100644 --- a/libpod/adapter/runtime_remote.go +++ b/libpod/adapter/runtime_remote.go @@ -20,6 +20,7 @@ import ( "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/utils" "github.com/containers/storage/pkg/archive" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -385,8 +386,11 @@ func (r *LocalRuntime) Export(name string, path string) error { if err != nil { return err } + return r.GetFileFromRemoteHost(tempPath, path, true) +} - outputFile, err := os.Create(path) +func (r *LocalRuntime) GetFileFromRemoteHost(remoteFilePath, outputPath string, delete bool) error { + outputFile, err := os.Create(outputPath) if err != nil { return err } @@ -395,7 +399,7 @@ func (r *LocalRuntime) Export(name string, path string) error { writer := bufio.NewWriter(outputFile) defer writer.Flush() - reply, err := iopodman.ReceiveFile().Send(r.Conn, varlink.Upgrade, tempPath, true) + reply, err := iopodman.ReceiveFile().Send(r.Conn, varlink.Upgrade, remoteFilePath, delete) if err != nil { return err } @@ -409,7 +413,6 @@ func (r *LocalRuntime) Export(name string, path string) error { if _, err := io.CopyN(writer, reader, length); err != nil { return errors.Wrap(err, "file transer failed") } - return nil } @@ -467,28 +470,17 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti Squash: options.Squash, } // tar the file - logrus.Debugf("creating tarball of context dir %s", options.ContextDirectory) - input, err := archive.Tar(options.ContextDirectory, archive.Uncompressed) - if err != nil { - return errors.Wrapf(err, "unable to create tarball of context dir %s", options.ContextDirectory) - } - - // Write the tarball to the fs - // TODO we might considering sending this without writing to the fs for the sake of performance - // under given conditions like memory availability. outputFile, err := ioutil.TempFile("", "varlink_tar_send") if err != nil { return err } defer outputFile.Close() - logrus.Debugf("writing context dir tarball to %s", outputFile.Name()) + defer os.Remove(outputFile.Name()) - _, err = io.Copy(outputFile, input) - if err != nil { + // Create the tarball of the context dir to a tempfile + if err := utils.TarToFilesystem(options.ContextDirectory, outputFile); err != nil { return err } - - logrus.Debugf("completed writing context dir tarball %s", outputFile.Name()) // Send the context dir tarball over varlink. tempFile, err := r.SendFileOverVarlink(outputFile.Name()) if err != nil { @@ -702,3 +694,72 @@ func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) { } return prunedNames, errs } + +// SaveImage is a wrapper function for saving an image to the local filesystem +func (r *LocalRuntime) SaveImage(ctx context.Context, c *cliconfig.SaveValues) error { + source := c.InputArgs[0] + additionalTags := c.InputArgs[1:] + + options := iopodman.ImageSaveOptions{ + Name: source, + Format: c.Format, + Output: c.Output, + MoreTags: additionalTags, + Quiet: c.Quiet, + Compress: c.Compress, + } + reply, err := iopodman.ImageSave().Send(r.Conn, varlink.More, options) + if err != nil { + return err + } + + var fetchfile string + for { + responses, flags, err := reply() + if err != nil { + return err + } + if len(responses.Id) > 0 { + fetchfile = responses.Id + } + for _, line := range responses.Logs { + fmt.Print(line) + } + if flags&varlink.Continues == 0 { + break + } + + } + if err != nil { + return err + } + + outputToDir := false + outfile := c.Output + var outputFile *os.File + // If the result is supposed to be a dir, then we need to put the tarfile + // from the host in a temporary file + if options.Format != "oci-archive" && options.Format != "docker-archive" { + outputToDir = true + outputFile, err = ioutil.TempFile("", "saveimage_tempfile") + if err != nil { + return err + } + outfile = outputFile.Name() + defer outputFile.Close() + defer os.Remove(outputFile.Name()) + } + // We now need to fetch the tarball result back to the more system + if err := r.GetFileFromRemoteHost(fetchfile, outfile, true); err != nil { + return err + } + + // If the result is a tarball, we're done + // If it is a dir, we need to untar the temporary file into the dir + if outputToDir { + if err := utils.UntarToFileSystem(c.Output, outputFile, &archive.TarOptions{}); err != nil { + return err + } + } + return nil +} diff --git a/libpod/image/image.go b/libpod/image/image.go index 028a795ea..547fb8994 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -5,14 +5,18 @@ import ( "encoding/json" "fmt" "io" + "os" "strings" "syscall" "time" types2 "github.com/containernetworking/cni/pkg/types" cp "github.com/containers/image/copy" + "github.com/containers/image/directory" + dockerarchive "github.com/containers/image/docker/archive" "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" + ociarchive "github.com/containers/image/oci/archive" is "github.com/containers/image/storage" "github.com/containers/image/tarball" "github.com/containers/image/transports" @@ -26,6 +30,7 @@ import ( "github.com/containers/storage" "github.com/containers/storage/pkg/reexec" digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" opentracing "github.com/opentracing/opentracing-go" "github.com/pkg/errors" @@ -1084,3 +1089,65 @@ func (i *Image) Comment(ctx context.Context, manifestType string) (string, error } return ociv1Img.History[0].Comment, nil } + +// Save writes a container image to the filesystem +func (i *Image) Save(ctx context.Context, source, format, output string, moreTags []string, quiet, compress bool) error { + var ( + writer io.Writer + destRef types.ImageReference + manifestType string + err error + ) + + if quiet { + writer = os.Stderr + } + switch format { + case "oci-archive": + destImageName := imageNameForSaveDestination(i, source) + destRef, err = ociarchive.NewReference(output, destImageName) // destImageName may be "" + if err != nil { + return errors.Wrapf(err, "error getting OCI archive ImageReference for (%q, %q)", output, destImageName) + } + case "oci-dir": + destRef, err = directory.NewReference(output) + if err != nil { + return errors.Wrapf(err, "error getting directory ImageReference for %q", output) + } + manifestType = imgspecv1.MediaTypeImageManifest + case "docker-dir": + destRef, err = directory.NewReference(output) + if err != nil { + return errors.Wrapf(err, "error getting directory ImageReference for %q", output) + } + manifestType = manifest.DockerV2Schema2MediaType + case "docker-archive", "": + dst := output + destImageName := imageNameForSaveDestination(i, source) + if destImageName != "" { + dst = fmt.Sprintf("%s:%s", dst, destImageName) + } + destRef, err = dockerarchive.ParseReference(dst) // FIXME? Add dockerarchive.NewReference + if err != nil { + return errors.Wrapf(err, "error getting Docker archive ImageReference for %q", dst) + } + default: + return errors.Errorf("unknown format option %q", format) + } + // supports saving multiple tags to the same tar archive + var additionaltags []reference.NamedTagged + if len(moreTags) > 0 { + additionaltags, err = GetAdditionalTags(moreTags) + if err != nil { + return err + } + } + if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", writer, compress, SigningOptions{}, &DockerRegistryOptions{}, additionaltags); err != nil { + if err2 := os.Remove(output); err2 != nil { + logrus.Errorf("error deleting %q: %v", output, err) + } + return errors.Wrapf(err, "unable to save %q", source) + } + + return nil +} diff --git a/libpod/image/utils.go b/libpod/image/utils.go index 3585428ad..544796a4b 100644 --- a/libpod/image/utils.go +++ b/libpod/image/utils.go @@ -1,6 +1,7 @@ package image import ( + "fmt" "io" "net/url" "regexp" @@ -148,3 +149,28 @@ func IsValidImageURI(imguri string) (bool, error) { } return true, nil } + +// imageNameForSaveDestination returns a Docker-like reference appropriate for saving img, +// which the user referred to as imgUserInput; or an empty string, if there is no appropriate +// reference. +func imageNameForSaveDestination(img *Image, imgUserInput string) string { + if strings.Contains(img.ID(), imgUserInput) { + return "" + } + + prepend := "" + localRegistryPrefix := fmt.Sprintf("%s/", DefaultLocalRegistry) + if !strings.HasPrefix(imgUserInput, localRegistryPrefix) { + // we need to check if localhost was added to the image name in NewFromLocal + for _, name := range img.Names() { + // If the user is saving an image in the localhost registry, getLocalImage need + // a name that matches the format localhost/: or localhost/:latest to correctly + // set up the manifest and save. + if strings.HasPrefix(name, localRegistryPrefix) && (strings.HasSuffix(name, imgUserInput) || strings.HasSuffix(name, fmt.Sprintf("%s:latest", imgUserInput))) { + prepend = localRegistryPrefix + break + } + } + } + return fmt.Sprintf("%s%s", prepend, imgUserInput) +} -- cgit v1.2.3-54-g00ecf