mirror of
https://github.com/containers/podman.git
synced 2025-07-04 18:27:33 +08:00
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:
36
API.md
36
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 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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ var imageSubCommands = []*cobra.Command{
|
|||||||
_pullCommand,
|
_pullCommand,
|
||||||
_pushCommand,
|
_pushCommand,
|
||||||
_rmiCommand,
|
_rmiCommand,
|
||||||
|
_saveCommand,
|
||||||
_tagCommand,
|
_tagCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ var mainCommands = []*cobra.Command{
|
|||||||
_pullCommand,
|
_pullCommand,
|
||||||
_pushCommand,
|
_pushCommand,
|
||||||
_rmiCommand,
|
_rmiCommand,
|
||||||
|
_saveCommand,
|
||||||
_tagCommand,
|
_tagCommand,
|
||||||
_versionCommand,
|
_versionCommand,
|
||||||
imageCommand.Command,
|
imageCommand.Command,
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
// +build !remoteclient
|
|
||||||
|
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user