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>
This commit is contained in:
baude
2019-02-19 10:08:43 -06:00
parent 4de0bf9c74
commit 711ac93051
13 changed files with 361 additions and 128 deletions

36
API.md
View File

@ -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 ImageExists(name: string) int](#ImageExists)
[func ImageSave(options: ImageSaveOptions) MoreResponse](#ImageSave)
[func ImagesPrune(all: bool) []string](#ImagesPrune) [func ImagesPrune(all: bool) []string](#ImagesPrune)
[func ImportImage(source: string, reference: string, message: string, changes: []string, delete: bool) string](#ImportImage) [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 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) [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 ImageHistory](#ImageHistory)
[type ImageSaveOptions](#ImageSaveOptions)
[type ImageSearchFilter](#ImageSearchFilter) [type ImageSearchFilter](#ImageSearchFilter)
[type ImageSearchResult](#ImageSearchResult) [type ImageSearchResult](#ImageSearchResult)
@ -556,6 +560,11 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.ImageExists '{"name": "im
"exists": 1 "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 ### <a name="ImagesPrune"></a>func ImagesPrune
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> <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 ### <a name="SearchImages"></a>func SearchImages
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> <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 SearchImages searches available registries for images that contain the
contents of "query" in their name. If "limit" is given, limits the amount of contents of "query" in their name. If "limit" is given, limits the amount of
search results per registry. search results per registry.
@ -1410,13 +1419,30 @@ tags [[]string](#[]string)
size [int](https://godoc.org/builtin#int) size [int](https://godoc.org/builtin#int)
comment [string](https://godoc.org/builtin#string) 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 ### <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) star_count [int](https://godoc.org/builtin#int)
### <a name="ImageSearchResult"></a>type ImageSearchResult ### <a name="ImageSearchResult"></a>type ImageSearchResult

View File

@ -30,7 +30,6 @@ func getMainCommands() []*cobra.Command {
_restoreCommand, _restoreCommand,
_rmCommand, _rmCommand,
_runCommand, _runCommand,
_saveCommand,
_searchCommand, _searchCommand,
_signCommand, _signCommand,
_startCommand, _startCommand,
@ -53,7 +52,6 @@ func getMainCommands() []*cobra.Command {
func getImageSubCommands() []*cobra.Command { func getImageSubCommands() []*cobra.Command {
return []*cobra.Command{ return []*cobra.Command{
_loadCommand, _loadCommand,
_saveCommand,
_signCommand, _signCommand,
} }
} }

View File

@ -28,6 +28,7 @@ var imageSubCommands = []*cobra.Command{
_pullCommand, _pullCommand,
_pushCommand, _pushCommand,
_rmiCommand, _rmiCommand,
_saveCommand,
_tagCommand, _tagCommand,
} }

View File

@ -48,6 +48,7 @@ var mainCommands = []*cobra.Command{
_pullCommand, _pullCommand,
_pushCommand, _pushCommand,
_rmiCommand, _rmiCommand,
_saveCommand,
_tagCommand, _tagCommand,
_versionCommand, _versionCommand,
imageCommand.Command, imageCommand.Command,

View File

@ -1,21 +1,10 @@
package main package main
import ( import (
"fmt"
"io"
"os" "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/cliconfig"
"github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/libpod/adapter"
libpodImage "github.com/containers/libpod/libpod/image"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -52,7 +41,7 @@ func init() {
saveCommand.SetUsageTemplate(UsageTemplate()) saveCommand.SetUsageTemplate(UsageTemplate())
flags := saveCommand.Flags() 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.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.StringVarP(&saveCommand.Output, "output", "o", "/dev/stdout", "Write to a file, default is STDOUT")
flags.BoolVarP(&saveCommand.Quiet, "quiet", "q", false, "Suppress the output") 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") return errors.Errorf("need at least 1 argument")
} }
runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) runtime, err := adapter.GetRuntime(&c.PodmanCommand)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not create runtime") 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'") 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 output := c.Output
if output == "/dev/stdout" { if output == "/dev/stdout" {
fi := os.Stdout fi := os.Stdout
@ -89,87 +73,5 @@ func saveCmd(c *cliconfig.SaveValues) error {
if err := validateFileName(output); err != nil { if err := validateFileName(output); err != nil {
return err return err
} }
return runtime.SaveImage(getContext(), c)
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)
} }

View File

@ -26,6 +26,16 @@ type ContainerChanges (
deleted: []string deleted: []string
) )
type ImageSaveOptions (
name: string,
format: string,
output: string,
outputType: string,
moreTags: []string,
quiet: bool,
compress: bool
)
type VolumeCreateOpts ( type VolumeCreateOpts (
volumeName: string, volumeName: string,
driver: string, driver: string,
@ -1090,6 +1100,8 @@ method GetVolumes(args: []string, all: bool) -> (volumes: []Volume)
# VolumesPrune removes unused volumes on the host # VolumesPrune removes unused volumes on the host
method VolumesPrune() -> (prunedNames: []string, prunedErrors: []string) 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. # ImageNotFound means the image could not be found by the provided name or ID in local storage.
error ImageNotFound (id: string) error ImageNotFound (id: string)

View File

@ -310,3 +310,15 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti
func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) { func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) {
return r.Runtime.PruneVolumes(ctx) 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)
}

View File

@ -20,6 +20,7 @@ import (
"github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/utils"
"github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/archive"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -385,8 +386,11 @@ func (r *LocalRuntime) Export(name string, path string) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -395,7 +399,7 @@ func (r *LocalRuntime) Export(name string, path string) error {
writer := bufio.NewWriter(outputFile) writer := bufio.NewWriter(outputFile)
defer writer.Flush() 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 { if err != nil {
return err return err
} }
@ -409,7 +413,6 @@ func (r *LocalRuntime) Export(name string, path string) error {
if _, err := io.CopyN(writer, reader, length); err != nil { if _, err := io.CopyN(writer, reader, length); err != nil {
return errors.Wrap(err, "file transer failed") return errors.Wrap(err, "file transer failed")
} }
return nil return nil
} }
@ -467,28 +470,17 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti
Squash: options.Squash, Squash: options.Squash,
} }
// tar the file // 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") outputFile, err := ioutil.TempFile("", "varlink_tar_send")
if err != nil { if err != nil {
return err return err
} }
defer outputFile.Close() defer outputFile.Close()
logrus.Debugf("writing context dir tarball to %s", outputFile.Name()) defer os.Remove(outputFile.Name())
_, err = io.Copy(outputFile, input) // Create the tarball of the context dir to a tempfile
if err != nil { if err := utils.TarToFilesystem(options.ContextDirectory, outputFile); err != nil {
return err return err
} }
logrus.Debugf("completed writing context dir tarball %s", outputFile.Name())
// Send the context dir tarball over varlink. // Send the context dir tarball over varlink.
tempFile, err := r.SendFileOverVarlink(outputFile.Name()) tempFile, err := r.SendFileOverVarlink(outputFile.Name())
if err != nil { if err != nil {
@ -702,3 +694,72 @@ func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) {
} }
return prunedNames, errs 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
}

View File

@ -5,14 +5,18 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"os"
"strings" "strings"
"syscall" "syscall"
"time" "time"
types2 "github.com/containernetworking/cni/pkg/types" types2 "github.com/containernetworking/cni/pkg/types"
cp "github.com/containers/image/copy" 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/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
ociarchive "github.com/containers/image/oci/archive"
is "github.com/containers/image/storage" is "github.com/containers/image/storage"
"github.com/containers/image/tarball" "github.com/containers/image/tarball"
"github.com/containers/image/transports" "github.com/containers/image/transports"
@ -26,6 +30,7 @@ import (
"github.com/containers/storage" "github.com/containers/storage"
"github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/reexec"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
opentracing "github.com/opentracing/opentracing-go" opentracing "github.com/opentracing/opentracing-go"
"github.com/pkg/errors" "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 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
}

View File

@ -1,6 +1,7 @@
package image package image
import ( import (
"fmt"
"io" "io"
"net/url" "net/url"
"regexp" "regexp"
@ -148,3 +149,28 @@ func IsValidImageURI(imguri string) (bool, error) {
} }
return true, nil 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)
}

View File

@ -736,3 +736,102 @@ func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall, all bool) error {
} }
return call.ReplyImagesPrune(prunedImages) 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)
}

View File

@ -1,5 +1,3 @@
// +build !remoteclient
package integration package integration
import ( import (

View File

@ -4,12 +4,15 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"os"
"os/exec" "os/exec"
"strings" "strings"
"github.com/containers/storage/pkg/archive"
systemdDbus "github.com/coreos/go-systemd/dbus" systemdDbus "github.com/coreos/go-systemd/dbus"
"github.com/godbus/dbus" "github.com/godbus/dbus"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
// ExecCmd executes a command with args and returns its output as a string along // 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 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)
}