Allow podman push to push manifest lists

When doing a podman images, manifests lists look just like images, so
it is logical that users would assume that they can just podman push them
to a registry.  The problem is we throw out weird errors when this happens
and users need to somehow figure out this is a manifest list rather then
an image, and frankly the user will not understand the difference.

This PR will make podman push just do the right thing, by failing over and
attempting to push the manifest if it fails to push the image.

Fix up handling of manifest push

Protocol should bring back a digest string, which can either be
printed or stored in a file.

We should not reimplement the manifest push setup code in the tunnel
code but take advantage of the api path, to make sure remote and local
work the same way.

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
This commit is contained in:
Daniel J Walsh
2021-01-15 03:49:42 -05:00
parent 5a166b2973
commit cf51c7ed9f
20 changed files with 190 additions and 247 deletions

View File

@ -75,6 +75,8 @@ func init() {
func pushFlags(cmd *cobra.Command) {
flags := cmd.Flags()
// For now default All flag to true, for pushing of manifest lists
pushOptions.All = true
authfileFlagName := "authfile"
flags.StringVar(&pushOptions.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault)

View File

@ -1,11 +1,14 @@
package manifest
import (
"io/ioutil"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v2/cmd/podman/common"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/utils"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/util"
"github.com/pkg/errors"
@ -15,7 +18,7 @@ import (
// manifestPushOptsWrapper wraps entities.ManifestPushOptions and prevents leaking
// CLI-only fields into the API types.
type manifestPushOptsWrapper struct {
entities.ManifestPushOptions
entities.ImagePushOptions
TLSVerifyCLI bool // CLI only
CredentialsCLI string
@ -41,8 +44,8 @@ func init() {
Parent: manifestCmd,
})
flags := pushCmd.Flags()
flags.BoolVar(&manifestPushOpts.Purge, "purge", false, "remove the manifest list if push succeeds")
flags.BoolVar(&manifestPushOpts.All, "all", false, "also push the images in the list")
flags.BoolVar(&manifestPushOpts.Rm, "rm", false, "remove the manifest list if push succeeds")
flags.BoolVar(&manifestPushOpts.All, "all", true, "also push the images in the list")
authfileFlagName := "authfile"
flags.StringVar(&manifestPushOpts.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
@ -72,6 +75,7 @@ func init() {
flags.BoolVar(&manifestPushOpts.TLSVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
flags.BoolVarP(&manifestPushOpts.Quiet, "quiet", "q", false, "don't output progress information when pushing lists")
flags.SetNormalizeFunc(utils.AliasFlags)
if registry.IsRemote() {
_ = flags.MarkHidden("cert-dir")
@ -107,8 +111,15 @@ func push(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("tls-verify") {
manifestPushOpts.SkipTLSVerify = types.NewOptionalBool(!manifestPushOpts.TLSVerifyCLI)
}
if err := registry.ImageEngine().ManifestPush(registry.Context(), args[0], args[1], manifestPushOpts.ManifestPushOptions); err != nil {
digest, err := registry.ImageEngine().ManifestPush(registry.Context(), args[0], args[1], manifestPushOpts.ImagePushOptions)
if err != nil {
return err
}
if manifestPushOpts.DigestFile != "" {
if err := ioutil.WriteFile(manifestPushOpts.DigestFile, []byte(digest), 0644); err != nil {
return err
}
}
return nil
}

View File

@ -23,6 +23,8 @@ func AliasFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
name = "ns"
case "storage":
name = "external"
case "purge":
name = "rm"
}
return pflag.NormalizedName(name)
}

View File

@ -17,7 +17,7 @@ The list image's ID and the digest of the image's manifest.
#### **--all**
Push the images mentioned in the manifest list or image index, in addition to
the list or index itself.
the list or index itself. (Default true)
#### **--authfile**=*path*
@ -46,14 +46,14 @@ After copying the image, write the digest of the resulting image to the file.
Manifest list type (oci or v2s2) to use when pushing the list (default is oci).
#### **--purge**
Delete the manifest list or image index from local storage if pushing succeeds.
#### **--quiet**, **-q**
When writing the manifest, suppress progress output
#### **--rm**
Delete the manifest list or image index from local storage if pushing succeeds.
#### **--remove-signatures**
Don't copy signatures when pushing images.

View File

@ -1,7 +1,7 @@
% podman-push(1)
## NAME
podman\-push - Push an image from local storage to elsewhere
podman\-push - Push an image, manifest list or image index from local storage to elsewhere
## SYNOPSIS
**podman push** [*options*] *image* [*destination*]
@ -9,10 +9,11 @@ podman\-push - Push an image from local storage to elsewhere
**podman image push** [*options*] *image* [*destination*]
## DESCRIPTION
Pushes an image from local storage to a specified destination.
Push is mainly used to push images to registries, however **podman push**
can be used to save images to tarballs and directories using the following
transports: **dir:**, **docker-archive:**, **docker-daemon:** and **oci-archive:**.
Pushes an image, manifest list or image index from local storage to a specified
destination. Push is mainly used to push images to registries, however
**podman push** can be used to save images to tarballs and directories using the
following transports:
**dir:**, **docker-archive:**, **docker-daemon:** and **oci-archive:**.
## Image storage
Images are pushed from those stored in local image storage.

View File

@ -246,7 +246,7 @@ the exit codes follow the `chroot` standard, see below:
| [podman-port(1)](podman-port.1.md) | List port mappings for a container. |
| [podman-ps(1)](podman-ps.1.md) | Prints out information about containers. |
| [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. |
| [podman-push(1)](podman-push.1.md) | Push an image from local storage to elsewhere. |
| [podman-push(1)](podman-push.1.md) | Push an image, manifest list or image index from local storage to elsewhere.|
| [podman-rename(1)](podman-rename.1.md) | Rename an existing container. |
| [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. |
| [podman-rm(1)](podman-rm.1.md) | Remove one or more containers. |

View File

@ -25,7 +25,6 @@ import (
utils2 "github.com/containers/podman/v2/utils"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Commit
@ -410,6 +409,8 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
query := struct {
Destination string `schema:"destination"`
TLSVerify bool `schema:"tlsVerify"`
Format string `schema:"format"`
All bool `schema:"all"`
}{
// This is where you can override the golang default value for one of fields
}
@ -434,45 +435,31 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
return
}
newImage, err := runtime.ImageRuntime().NewFromLocal(source)
if err != nil {
utils.ImageNotFound(w, source, errors.Wrapf(err, "failed to find image %s", source))
return
}
authConf, authfile, key, err := auth.GetCredentials(r)
authconf, authfile, key, err := auth.GetCredentials(r)
if err != nil {
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
return
}
defer auth.RemoveAuthfile(authfile)
logrus.Errorf("AuthConf: %v", authConf)
var username, password string
if authconf != nil {
username = authconf.Username
password = authconf.Password
dockerRegistryOptions := &image.DockerRegistryOptions{
DockerRegistryCreds: authConf,
}
if sys := runtime.SystemContext(); sys != nil {
dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
dockerRegistryOptions.RegistriesConfPath = sys.SystemRegistriesConfPath
options := entities.ImagePushOptions{
Authfile: authfile,
Username: username,
Password: password,
Format: query.Format,
All: query.All,
}
if _, found := r.URL.Query()["tlsVerify"]; found {
dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
}
err = newImage.PushImageToHeuristicDestination(
context.Background(),
destination,
"", // manifest type
authfile,
"", // digest file
"", // signature policy
os.Stderr,
false, // force compression
image.SigningOptions{},
dockerRegistryOptions,
nil, // additional tags
)
if err != nil {
imageEngine := abi.ImageEngine{Libpod: runtime}
if err := imageEngine.Push(context.Background(), source, destination, options); err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "error pushing image %q", destination))
return
}

View File

@ -1,17 +1,18 @@
package libpod
import (
"context"
"encoding/json"
"net/http"
"github.com/containers/buildah/manifests"
copy2 "github.com/containers/image/v5/copy"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/image"
"github.com/containers/podman/v2/pkg/api/handlers"
"github.com/containers/podman/v2/pkg/api/handlers/utils"
"github.com/containers/podman/v2/pkg/auth"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/domain/infra/abi"
"github.com/gorilla/schema"
"github.com/opencontainers/go-digest"
@ -123,15 +124,13 @@ func ManifestRemove(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: newID})
}
func ManifestPush(w http.ResponseWriter, r *http.Request) {
// FIXME: parameters are missing (tlsVerify, format).
// Also, we should use the ABI function to avoid duplicate code.
// Also, support for XRegistryAuth headers are missing.
runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
All bool `schema:"all"`
Destination string `schema:"destination"`
Format string `schema:"format"`
TLSVerify bool `schema:"tlsVerify"`
}{
// Add defaults here once needed.
}
@ -140,35 +139,43 @@ func ManifestPush(w http.ResponseWriter, r *http.Request) {
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return
}
name := utils.GetName(r)
newImage, err := runtime.ImageRuntime().NewFromLocal(name)
if err != nil {
utils.ImageNotFound(w, name, err)
if _, err := utils.ParseDockerReference(query.Destination); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
return
}
dest, err := alltransports.ParseImageName(query.Destination)
source := utils.GetName(r)
authConf, authfile, key, err := auth.GetCredentials(r)
if err != nil {
utils.Error(w, "invalid destination parameter", http.StatusBadRequest, errors.Errorf("invalid destination parameter %q", query.Destination))
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
return
}
rtc, err := runtime.GetConfig()
defer auth.RemoveAuthfile(authfile)
var username, password string
if authConf != nil {
username = authConf.Username
password = authConf.Password
}
options := entities.ImagePushOptions{
Authfile: authfile,
Username: username,
Password: password,
Format: query.Format,
All: query.All,
}
if sys := runtime.SystemContext(); sys != nil {
options.CertDir = sys.DockerCertPath
}
if _, found := r.URL.Query()["tlsVerify"]; found {
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
}
imageEngine := abi.ImageEngine{Libpod: runtime}
digest, err := imageEngine.ManifestPush(context.Background(), source, query.Destination, options)
if err != nil {
utils.InternalServerError(w, err)
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "error pushing image %q", query.Destination))
return
}
sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false)
opts := manifests.PushOptions{
Store: runtime.GetStore(),
ImageListSelection: copy2.CopySpecificImages,
SystemContext: sc,
}
if query.All {
opts.ImageListSelection = copy2.CopyAllImages
}
newD, err := newImage.PushManifest(dest, opts)
if err != nil {
utils.InternalServerError(w, err)
return
}
utils.WriteResponse(w, http.StatusOK, newD.String())
utils.WriteResponse(w, http.StatusOK, digest)
}

View File

@ -99,6 +99,8 @@ type ImportOptions struct {
//go:generate go run ../generator/generator.go PushOptions
// PushOptions are optional options for importing images
type PushOptions struct {
// All indicates whether to push all images related to the image list
All *bool
// Authfile is the path to the authentication file. Ignored for remote
// calls.
Authfile *string

View File

@ -87,6 +87,22 @@ func (o *PushOptions) ToParams() (url.Values, error) {
return params, nil
}
// WithAll
func (o *PushOptions) WithAll(value bool) *PushOptions {
v := &value
o.All = v
return o
}
// GetAll
func (o *PushOptions) GetAll() bool {
var all bool
if o.All == nil {
return all
}
return *o.All
}
// WithAuthfile
func (o *PushOptions) WithAuthfile(value string) *PushOptions {
v := &value

View File

@ -5,11 +5,13 @@ import (
"errors"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/containers/image/v5/manifest"
"github.com/containers/podman/v2/pkg/api/handlers"
"github.com/containers/podman/v2/pkg/bindings"
"github.com/containers/podman/v2/pkg/bindings/images"
jsoniter "github.com/json-iterator/go"
)
@ -112,12 +114,12 @@ func Remove(ctx context.Context, name, digest string, options *RemoveOptions) (s
// Push takes a manifest list and pushes to a destination. If the destination is not specified,
// the name will be used instead. If the optional all boolean is specified, all images specified
// in the list will be pushed as well.
func Push(ctx context.Context, name, destination string, options *PushOptions) (string, error) {
func Push(ctx context.Context, name, destination string, options *images.PushOptions) (string, error) {
var (
idr handlers.IDResponse
)
if options == nil {
options = new(PushOptions)
options = new(images.PushOptions)
}
if len(destination) < 1 {
destination = name
@ -130,8 +132,15 @@ func Push(ctx context.Context, name, destination string, options *PushOptions) (
if err != nil {
return "", err
}
//SkipTLSVerify is special. We need to delete the param added by
//toparams and change the key and flip the bool
if options.SkipTLSVerify != nil {
params.Del("SkipTLSVerify")
params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
}
params.Set("image", name)
params.Set("destination", destination)
params.Set("format", *options.Format)
_, err = conn.DoRequest(nil, http.MethodPost, "/manifests/%s/push", params, nil, name)
if err != nil {
return "", err

View File

@ -28,9 +28,3 @@ type AddOptions struct {
// RemoveOptions are optional options for removing manifests
type RemoveOptions struct {
}
//go:generate go run ../generator/generator.go PushOptions
// RemoveOptions are optional options for pushing manifests
type PushOptions struct {
All *bool
}

View File

@ -1,104 +0,0 @@
package manifests
import (
"net/url"
"reflect"
"strconv"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
)
/*
This file is generated automatically by go generate. Do not edit.
*/
// Changed
func (o *PushOptions) Changed(fieldName string) bool {
r := reflect.ValueOf(o)
value := reflect.Indirect(r).FieldByName(fieldName)
return !value.IsNil()
}
// ToParams
func (o *PushOptions) ToParams() (url.Values, error) {
params := url.Values{}
if o == nil {
return params, nil
}
json := jsoniter.ConfigCompatibleWithStandardLibrary
s := reflect.ValueOf(o)
if reflect.Ptr == s.Kind() {
s = s.Elem()
}
sType := s.Type()
for i := 0; i < s.NumField(); i++ {
fieldName := sType.Field(i).Name
if !o.Changed(fieldName) {
continue
}
fieldName = strings.ToLower(fieldName)
f := s.Field(i)
if reflect.Ptr == f.Kind() {
f = f.Elem()
}
switch f.Kind() {
case reflect.Bool:
params.Set(fieldName, strconv.FormatBool(f.Bool()))
case reflect.String:
params.Set(fieldName, f.String())
case reflect.Int, reflect.Int64:
// f.Int() is always an int64
params.Set(fieldName, strconv.FormatInt(f.Int(), 10))
case reflect.Uint, reflect.Uint64:
// f.Uint() is always an uint64
params.Set(fieldName, strconv.FormatUint(f.Uint(), 10))
case reflect.Slice:
typ := reflect.TypeOf(f.Interface()).Elem()
switch typ.Kind() {
case reflect.String:
sl := f.Slice(0, f.Len())
s, ok := sl.Interface().([]string)
if !ok {
return nil, errors.New("failed to convert to string slice")
}
for _, val := range s {
params.Add(fieldName, val)
}
default:
return nil, errors.Errorf("unknown slice type %s", f.Kind().String())
}
case reflect.Map:
lowerCaseKeys := make(map[string][]string)
iter := f.MapRange()
for iter.Next() {
lowerCaseKeys[iter.Key().Interface().(string)] = iter.Value().Interface().([]string)
}
s, err := json.MarshalToString(lowerCaseKeys)
if err != nil {
return nil, err
}
params.Set(fieldName, s)
}
}
return params, nil
}
// WithAll
func (o *PushOptions) WithAll(value bool) *PushOptions {
v := &value
o.All = v
return o
}
// GetAll
func (o *PushOptions) GetAll() bool {
var all bool
if o.All == nil {
return all
}
return *o.All
}

View File

@ -36,6 +36,6 @@ type ImageEngine interface {
ManifestAdd(ctx context.Context, opts ManifestAddOptions) (string, error)
ManifestAnnotate(ctx context.Context, names []string, opts ManifestAnnotateOptions) (string, error)
ManifestRemove(ctx context.Context, names []string) (string, error)
ManifestPush(ctx context.Context, name, destination string, manifestPushOpts ManifestPushOptions) error
ManifestPush(ctx context.Context, name, destination string, imagePushOpts ImagePushOptions) (string, error)
Sign(ctx context.Context, names []string, options SignOptions) (*SignReport, error)
}

View File

@ -165,6 +165,8 @@ type ImagePullReport struct {
// ImagePushOptions are the arguments for pushing images.
type ImagePushOptions struct {
// All indicates that all images referenced in an manifest list should be pushed
All bool
// Authfile is the path to the authentication file. Ignored for remote
// calls.
Authfile string
@ -189,6 +191,8 @@ type ImagePushOptions struct {
// Quiet can be specified to suppress pull progress when pulling. Ignored
// for remote calls.
Quiet bool
// Rm indicates whether to remove the manifest list if push succeeds
Rm bool
// RemoveSignatures, discard any pre-existing signatures in the image.
// Ignored for remote calls.
RemoveSignatures bool

View File

@ -33,11 +33,3 @@ type ManifestAnnotateOptions struct {
OSVersion string `json:"os_version" schema:"os_version"`
Variant string `json:"variant" schema:"variant"`
}
type ManifestPushOptions struct {
Purge, Quiet, All, RemoveSignatures bool
Authfile, CertDir, Username, Password, DigestFile, Format, SignBy string
SkipTLSVerify types.OptionalBool
}

View File

@ -367,7 +367,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
return err
}
return newImage.PushImageToHeuristicDestination(
err = newImage.PushImageToHeuristicDestination(
ctx,
destination,
manifestType,
@ -379,39 +379,15 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
signOptions,
&dockerRegistryOptions,
nil)
if err != nil && errors.Cause(err) != storage.ErrImageUnknown {
// Image might be a manifest list so attempt a manifest push
if _, manifestErr := ir.ManifestPush(ctx, source, destination, options); manifestErr == nil {
return nil
}
}
return err
}
// func (r *imageRuntime) Delete(ctx context.Context, nameOrID string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) {
// image, err := r.libpod.ImageEngine().NewFromLocal(nameOrID)
// if err != nil {
// return nil, err
// }
//
// results, err := r.libpod.RemoveImage(ctx, image, opts.Force)
// if err != nil {
// return nil, err
// }
//
// report := entities.ImageDeleteReport{}
// if err := domainUtils.DeepCopy(&report, results); err != nil {
// return nil, err
// }
// return &report, nil
// }
//
// func (r *imageRuntime) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) {
// // TODO: map FilterOptions
// id, err := r.libpod.ImageEngine().PruneImages(ctx, opts.All, []string{})
// if err != nil {
// return nil, err
// }
//
// // TODO: Determine Size
// report := entities.ImagePruneReport{}
// copy(report.Report.ID, id)
// return &report, nil
// }
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrID)
if err != nil {

View File

@ -7,7 +7,6 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
@ -24,9 +23,8 @@ import (
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// ManifestCreate implements logic for creating manifest lists via ImageEngine
@ -243,14 +241,20 @@ func (ir *ImageEngine) ManifestRemove(ctx context.Context, names []string) (stri
}
// ManifestPush pushes a manifest list or image index to the destination
func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination string, opts entities.ManifestPushOptions) error {
func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination string, opts entities.ImagePushOptions) (string, error) {
listImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name)
if err != nil {
return errors.Wrapf(err, "error retrieving local image from image name %s", name)
return "", errors.Wrapf(err, "error retrieving local image from image name %s", name)
}
dest, err := alltransports.ParseImageName(destination)
if err != nil {
return err
oldErr := err
// Try adding the images default transport
destination2 := libpodImage.DefaultTransport + destination
dest, err = alltransports.ParseImageName(destination2)
if err != nil {
return "", oldErr
}
}
var manifestType string
@ -261,7 +265,7 @@ func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination strin
case "v2s2", "docker":
manifestType = manifest.DockerV2Schema2MediaType
default:
return errors.Errorf("unknown format %q. Choose one of the supported formats: 'oci' or 'v2s2'", opts.Format)
return "", errors.Errorf("unknown format %q. Choose one of the supported formats: 'oci' or 'v2s2'", opts.Format)
}
}
@ -297,13 +301,8 @@ func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination strin
options.ReportWriter = os.Stderr
}
manDigest, err := listImage.PushManifest(dest, options)
if err == nil && opts.Purge {
if err == nil && opts.Rm {
_, err = ir.Libpod.GetStore().DeleteImage(listImage.ID(), true)
}
if opts.DigestFile != "" {
if err = ioutil.WriteFile(opts.DigestFile, []byte(manDigest.String()), 0644); err != nil {
return buildahUtil.GetFailureCause(err, errors.Wrapf(err, "failed to write digest to file %q", opts.DigestFile))
}
}
return err
return manDigest.String(), err
}

View File

@ -6,6 +6,8 @@ import (
"fmt"
"strings"
"github.com/containers/image/v5/types"
images "github.com/containers/podman/v2/pkg/bindings/images"
"github.com/containers/podman/v2/pkg/bindings/manifests"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/pkg/errors"
@ -73,8 +75,20 @@ func (ir *ImageEngine) ManifestRemove(ctx context.Context, names []string) (stri
}
// ManifestPush pushes a manifest list or image index to the destination
func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination string, opts entities.ManifestPushOptions) error {
options := new(manifests.PushOptions).WithAll(opts.All)
_, err := manifests.Push(ir.ClientCtx, name, destination, options)
return err
func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination string, opts entities.ImagePushOptions) (string, error) {
options := new(images.PushOptions)
options.WithUsername(opts.Username).WithSignaturePolicy(opts.SignaturePolicy).WithQuiet(opts.Quiet)
options.WithPassword(opts.Password).WithCertDir(opts.CertDir).WithAuthfile(opts.Authfile)
options.WithCompress(opts.Compress).WithDigestFile(opts.DigestFile).WithFormat(opts.Format)
options.WithRemoveSignatures(opts.RemoveSignatures).WithSignBy(opts.SignBy)
if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined {
if s == types.OptionalBoolTrue {
options.WithSkipTLSVerify(true)
} else {
options.WithSkipTLSVerify(false)
}
}
digest, err := manifests.Push(ir.ClientCtx, name, destination, options)
return digest, err
}

View File

@ -171,6 +171,7 @@ var _ = Describe("Podman manifest", func() {
})
It("podman manifest push", func() {
SkipIfRemote("manifest push to dir not supported in remote mode")
session := podmanTest.Podman([]string{"manifest", "create", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
@ -199,8 +200,38 @@ var _ = Describe("Podman manifest", func() {
Expect(check.OutputToString()).To(ContainSubstring(strings.TrimPrefix(imageListARM64InstanceDigest, prefix)))
})
It("podman manifest push purge", func() {
SkipIfRemote("remote does not support --purge")
It("podman push --all", func() {
SkipIfRemote("manifest push to dir not supported in remote mode")
session := podmanTest.Podman([]string{"manifest", "create", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
session = podmanTest.Podman([]string{"manifest", "add", "--all", "foo", imageList})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
dest := filepath.Join(podmanTest.TempDir, "pushed")
err := os.MkdirAll(dest, os.ModePerm)
Expect(err).To(BeNil())
defer func() {
os.RemoveAll(dest)
}()
session = podmanTest.Podman([]string{"push", "foo", "dir:" + dest})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
files, err := filepath.Glob(dest + string(os.PathSeparator) + "*")
Expect(err).To(BeNil())
check := SystemExec("sha256sum", files)
check.WaitWithDefaultTimeout()
Expect(check.ExitCode()).To(Equal(0))
prefix := "sha256:"
Expect(check.OutputToString()).To(ContainSubstring(strings.TrimPrefix(imageListAMD64InstanceDigest, prefix)))
Expect(check.OutputToString()).To(ContainSubstring(strings.TrimPrefix(imageListARMInstanceDigest, prefix)))
Expect(check.OutputToString()).To(ContainSubstring(strings.TrimPrefix(imageListPPC64LEInstanceDigest, prefix)))
Expect(check.OutputToString()).To(ContainSubstring(strings.TrimPrefix(imageListS390XInstanceDigest, prefix)))
Expect(check.OutputToString()).To(ContainSubstring(strings.TrimPrefix(imageListARM64InstanceDigest, prefix)))
})
It("podman manifest push --rm", func() {
SkipIfRemote("remote does not support --rm")
session := podmanTest.Podman([]string{"manifest", "create", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))