mirror of
https://github.com/containers/podman.git
synced 2025-06-30 15:49:03 +08:00
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:
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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. |
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
Reference in New Issue
Block a user