summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2019-02-19 10:08:43 -0600
committerbaude <bbaude@redhat.com>2019-02-20 12:58:05 -0600
commit711ac9305185e645f2970d09ff76c2761132202a (patch)
treee8c7c83be19cc075446b20ea49a88c529dda69f1
parent4de0bf9c74624de8a2cab1e5cbebc0beaa67339a (diff)
downloadpodman-711ac9305185e645f2970d09ff76c2761132202a.tar.gz
podman-711ac9305185e645f2970d09ff76c2761132202a.tar.bz2
podman-711ac9305185e645f2970d09ff76c2761132202a.zip
podman-remote save [image]
Add the ability to save an image from the remote-host to the remote-client. Signed-off-by: baude <bbaude@redhat.com>
-rwxr-xr-xAPI.md36
-rw-r--r--cmd/podman/commands.go2
-rw-r--r--cmd/podman/image.go1
-rw-r--r--cmd/podman/main.go1
-rw-r--r--cmd/podman/save.go106
-rw-r--r--cmd/podman/varlink/io.podman.varlink12
-rw-r--r--libpod/adapter/runtime.go12
-rw-r--r--libpod/adapter/runtime_remote.go95
-rw-r--r--libpod/image/image.go67
-rw-r--r--libpod/image/utils.go26
-rw-r--r--pkg/varlinkapi/images.go99
-rw-r--r--test/e2e/save_test.go2
-rw-r--r--utils/utils.go30
13 files changed, 361 insertions, 128 deletions
diff --git a/API.md b/API.md
index c1d8c6a36..0cb06e3bb 100755
--- a/API.md
+++ b/API.md
@@ -59,6 +59,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[func ImageExists(name: string) int](#ImageExists)
+[func ImageSave(options: ImageSaveOptions) MoreResponse](#ImageSave)
+
[func ImagesPrune(all: bool) []string](#ImagesPrune)
[func ImportImage(source: string, reference: string, message: string, changes: []string, delete: bool) string](#ImportImage)
@@ -107,7 +109,7 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[func RestartPod(name: string) string](#RestartPod)
-[func SearchImages(query: string, limit: int, tlsVerify: ?bool, filter: ImageSearchFilter) ImageSearchResult](#SearchImages)
+[func SearchImages(query: string, limit: , tlsVerify: , filter: ImageSearchFilter) ImageSearchResult](#SearchImages)
[func SendFile(type: string, length: int) string](#SendFile)
@@ -163,6 +165,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[type ImageHistory](#ImageHistory)
+[type ImageSaveOptions](#ImageSaveOptions)
+
[type ImageSearchFilter](#ImageSearchFilter)
[type ImageSearchResult](#ImageSearchResult)
@@ -556,6 +560,11 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.ImageExists '{"name": "im
"exists": 1
}
~~~
+### <a name="ImageSave"></a>func ImageSave
+<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
+
+method ImageSave(options: [ImageSaveOptions](#ImageSaveOptions)) [MoreResponse](#MoreResponse)</div>
+
### <a name="ImagesPrune"></a>func ImagesPrune
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
@@ -847,7 +856,7 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.RestartPod '{"name": "135
### <a name="SearchImages"></a>func SearchImages
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
-method SearchImages(query: [string](https://godoc.org/builtin#string), limit: [](#), tlsVerify: [](#)) [ImageSearchResult](#ImageSearchResult)</div>
+method SearchImages(query: [string](https://godoc.org/builtin#string), limit: [](#), tlsVerify: [](#), filter: [ImageSearchFilter](#ImageSearchFilter)) [ImageSearchResult](#ImageSearchResult)</div>
SearchImages searches available registries for images that contain the
contents of "query" in their name. If "limit" is given, limits the amount of
search results per registry.
@@ -1410,13 +1419,30 @@ tags [[]string](#[]string)
size [int](https://godoc.org/builtin#int)
comment [string](https://godoc.org/builtin#string)
+### <a name="ImageSaveOptions"></a>type ImageSaveOptions
+
+
+
+name [string](https://godoc.org/builtin#string)
+
+format [string](https://godoc.org/builtin#string)
+
+output [string](https://godoc.org/builtin#string)
+
+outputType [string](https://godoc.org/builtin#string)
+
+moreTags [[]string](#[]string)
+
+quiet [bool](https://godoc.org/builtin#bool)
+
+compress [bool](https://godoc.org/builtin#bool)
### <a name="ImageSearchFilter"></a>type ImageSearchFilter
-Represents a filter for SearchImages
-is_official [bool](https://godoc.org/builtin#bool)
-is_automated [bool](https://godoc.org/builtin#bool)
+is_official [](#)
+
+is_automated [](#)
star_count [int](https://godoc.org/builtin#int)
### <a name="ImageSearchResult"></a>type ImageSearchResult
diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go
index 27ac342ba..387e35767 100644
--- a/cmd/podman/commands.go
+++ b/cmd/podman/commands.go
@@ -30,7 +30,6 @@ func getMainCommands() []*cobra.Command {
_restoreCommand,
_rmCommand,
_runCommand,
- _saveCommand,
_searchCommand,
_signCommand,
_startCommand,
@@ -53,7 +52,6 @@ func getMainCommands() []*cobra.Command {
func getImageSubCommands() []*cobra.Command {
return []*cobra.Command{
_loadCommand,
- _saveCommand,
_signCommand,
}
}
diff --git a/cmd/podman/image.go b/cmd/podman/image.go
index 4f9c7cd6a..3c8942ef5 100644
--- a/cmd/podman/image.go
+++ b/cmd/podman/image.go
@@ -28,6 +28,7 @@ var imageSubCommands = []*cobra.Command{
_pullCommand,
_pushCommand,
_rmiCommand,
+ _saveCommand,
_tagCommand,
}
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index ecb72f58b..5fa6cf233 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -48,6 +48,7 @@ var mainCommands = []*cobra.Command{
_pullCommand,
_pushCommand,
_rmiCommand,
+ _saveCommand,
_tagCommand,
_versionCommand,
imageCommand.Command,
diff --git a/cmd/podman/save.go b/cmd/podman/save.go
index ff4a22453..ba5209f34 100644
--- a/cmd/podman/save.go
+++ b/cmd/podman/save.go
@@ -1,21 +1,10 @@
package main
import (
- "fmt"
- "io"
"os"
- "strings"
- "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"
- "github.com/containers/image/types"
"github.com/containers/libpod/cmd/podman/cliconfig"
- "github.com/containers/libpod/cmd/podman/libpodruntime"
- libpodImage "github.com/containers/libpod/libpod/image"
- imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/containers/libpod/libpod/adapter"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -52,7 +41,7 @@ func init() {
saveCommand.SetUsageTemplate(UsageTemplate())
flags := saveCommand.Flags()
flags.BoolVar(&saveCommand.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(&saveCommand.Format, "format", "", "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-dir (directory with v2s2 manifest type)")
+ flags.StringVar(&saveCommand.Format, "format", "docker-archive", "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-dir (directory with v2s2 manifest type)")
flags.StringVarP(&saveCommand.Output, "output", "o", "/dev/stdout", "Write to a file, default is STDOUT")
flags.BoolVarP(&saveCommand.Quiet, "quiet", "q", false, "Suppress the output")
}
@@ -64,7 +53,7 @@ func saveCmd(c *cliconfig.SaveValues) error {
return errors.Errorf("need at least 1 argument")
}
- runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand)
+ runtime, err := adapter.GetRuntime(&c.PodmanCommand)
if err != nil {
return errors.Wrapf(err, "could not create runtime")
}
@@ -74,11 +63,6 @@ func saveCmd(c *cliconfig.SaveValues) error {
return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'")
}
- var writer io.Writer
- if !c.Quiet {
- writer = os.Stderr
- }
-
output := c.Output
if output == "/dev/stdout" {
fi := os.Stdout
@@ -89,87 +73,5 @@ func saveCmd(c *cliconfig.SaveValues) error {
if err := validateFileName(output); err != nil {
return err
}
-
- source := args[0]
- newImage, err := runtime.ImageRuntime().NewFromLocal(source)
- if err != nil {
- return err
- }
-
- var destRef types.ImageReference
- var manifestType string
- switch c.Format {
- case "oci-archive":
- destImageName := imageNameForSaveDestination(newImage, 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(newImage, 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", c.String("format"))
- }
-
- // supports saving multiple tags to the same tar archive
- var additionaltags []reference.NamedTagged
- if len(args) > 1 {
- additionaltags, err = libpodImage.GetAdditionalTags(args[1:])
- if err != nil {
- return err
- }
- }
- if err := newImage.PushImageToReference(getContext(), destRef, manifestType, "", "", writer, c.Bool("compress"), libpodImage.SigningOptions{}, &libpodImage.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", args)
- }
-
- return 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 *libpodImage.Image, imgUserInput string) string {
- if strings.Contains(img.ID(), imgUserInput) {
- return ""
- }
-
- prepend := ""
- localRegistryPrefix := fmt.Sprintf("%s/", libpodImage.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/<tag1>:<tag2> or localhost/<tag>: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)
+ return runtime.SaveImage(getContext(), c)
}
diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink
index c53a5454a..cae77e5b3 100644
--- a/cmd/podman/varlink/io.podman.varlink
+++ b/cmd/podman/varlink/io.podman.varlink
@@ -26,6 +26,16 @@ type ContainerChanges (
deleted: []string
)
+type ImageSaveOptions (
+ name: string,
+ format: string,
+ output: string,
+ outputType: string,
+ moreTags: []string,
+ quiet: bool,
+ compress: bool
+)
+
type VolumeCreateOpts (
volumeName: string,
driver: string,
@@ -1090,6 +1100,8 @@ method GetVolumes(args: []string, all: bool) -> (volumes: []Volume)
# VolumesPrune removes unused volumes on the host
method VolumesPrune() -> (prunedNames: []string, prunedErrors: []string)
+method ImageSave(options: ImageSaveOptions) -> (reply: MoreResponse)
+
# ImageNotFound means the image could not be found by the provided name or ID in local storage.
error ImageNotFound (id: string)
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/<tag1>:<tag2> or localhost/<tag>: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)
+}
diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go
index d12ab97ab..77df77a29 100644
--- a/pkg/varlinkapi/images.go
+++ b/pkg/varlinkapi/images.go
@@ -736,3 +736,102 @@ func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall, all bool) error {
}
return call.ReplyImagesPrune(prunedImages)
}
+
+// ImageSave ....
+func (i *LibpodAPI) ImageSave(call iopodman.VarlinkCall, options iopodman.ImageSaveOptions) error {
+ newImage, err := i.Runtime.ImageRuntime().NewFromLocal(options.Name)
+ if err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+
+ // Determine if we are dealing with a tarball or dir
+ var output string
+ outputToDir := false
+ if options.Format == "oci-archive" || options.Format == "docker-archive" {
+ tempfile, err := ioutil.TempFile("", "varlink_send")
+ if err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ output = tempfile.Name()
+ tempfile.Close()
+ } else {
+ var err error
+ outputToDir = true
+ output, err = ioutil.TempDir("", "varlink_send")
+ if err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ }
+ if err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ if call.WantsMore() {
+ call.Continues = true
+ }
+
+ saveOutput := bytes.NewBuffer([]byte{})
+ c := make(chan error)
+ go func() {
+ err := newImage.Save(getContext(), options.Name, options.Format, output, options.MoreTags, options.Quiet, options.Compress)
+ c <- err
+ close(c)
+ }()
+
+ // TODO When pull output gets fixed for the remote client, we need to look into how we can turn below
+ // into something re-usable. it is in build too
+ var log []string
+ done := false
+ for {
+ line, err := saveOutput.ReadString('\n')
+ if err == nil {
+ log = append(log, line)
+ continue
+ } else if err == io.EOF {
+ select {
+ case err := <-c:
+ if err != nil {
+ logrus.Errorf("reading of output during save failed for %s", newImage.ID())
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ done = true
+ default:
+ if !call.WantsMore() {
+ time.Sleep(1 * time.Second)
+ break
+ }
+ br := iopodman.MoreResponse{
+ Logs: log,
+ }
+ call.ReplyImageSave(br)
+ log = []string{}
+ }
+ } else {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ if done {
+ break
+ }
+ }
+ call.Continues = false
+
+ sendfile := output
+ // Image has been saved to `output`
+ if outputToDir {
+ // If the output is a directory, we need to tar up the directory to send it back
+ //Create a tempfile for the directory tarball
+ outputFile, err := ioutil.TempFile("", "varlink_save_dir")
+ if err != nil {
+ return err
+ }
+ defer outputFile.Close()
+ if err := utils.TarToFilesystem(output, outputFile); err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ sendfile = outputFile.Name()
+ }
+ br := iopodman.MoreResponse{
+ Logs: log,
+ Id: sendfile,
+ }
+ return call.ReplyPushImage(br)
+}
diff --git a/test/e2e/save_test.go b/test/e2e/save_test.go
index b354492b8..9f64e49a7 100644
--- a/test/e2e/save_test.go
+++ b/test/e2e/save_test.go
@@ -1,5 +1,3 @@
-// +build !remoteclient
-
package integration
import (
diff --git a/utils/utils.go b/utils/utils.go
index 4a91b304f..33b0eb1c5 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -4,12 +4,15 @@ import (
"bytes"
"fmt"
"io"
+ "os"
"os/exec"
"strings"
+ "github.com/containers/storage/pkg/archive"
systemdDbus "github.com/coreos/go-systemd/dbus"
"github.com/godbus/dbus"
"github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
)
// ExecCmd executes a command with args and returns its output as a string along
@@ -139,3 +142,30 @@ func CopyDetachable(dst io.Writer, src io.Reader, keys []byte) (written int64, e
}
return written, err
}
+
+// UntarToFileSystem untars an os.file of a tarball to a destination in the filesystem
+func UntarToFileSystem(dest string, tarball *os.File, options *archive.TarOptions) error {
+ logrus.Debugf("untarring %s", tarball.Name())
+ return archive.Untar(tarball, dest, options)
+}
+
+// TarToFilesystem creates a tarball from source and writes to an os.file
+// provided
+func TarToFilesystem(source string, tarball *os.File) error {
+ tb, err := Tar(source)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(tarball, tb)
+ if err != nil {
+ return err
+ }
+ logrus.Debugf("wrote tarball file %s", tarball.Name())
+ return nil
+}
+
+// Tar creates a tarball from source and returns a readcloser of it
+func Tar(source string) (io.ReadCloser, error) {
+ logrus.Debugf("creating tarball of %s", source)
+ return archive.Tar(source, archive.Uncompressed)
+}