diff options
author | OpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com> | 2021-01-17 06:52:35 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-17 06:52:35 -0500 |
commit | 341c4b1fd94dda26be5ce588490e3d7284a54dbb (patch) | |
tree | 82f8c6bfa113b8b350aa069e81436b88bddf3080 | |
parent | 73b036db566a8f3606f0b376728efe03fcf8685d (diff) | |
parent | cf51c7ed9f955390a0e417f208046e0b8fbadb26 (diff) | |
download | podman-341c4b1fd94dda26be5ce588490e3d7284a54dbb.tar.gz podman-341c4b1fd94dda26be5ce588490e3d7284a54dbb.tar.bz2 podman-341c4b1fd94dda26be5ce588490e3d7284a54dbb.zip |
Merge pull request #8942 from rhatdan/push
Allow podman push to push manifest lists
-rw-r--r-- | cmd/podman/images/push.go | 2 | ||||
-rw-r--r-- | cmd/podman/manifest/push.go | 19 | ||||
-rw-r--r-- | cmd/podman/utils/alias.go | 2 | ||||
-rw-r--r-- | docs/source/markdown/podman-manifest-push.1.md | 10 | ||||
-rw-r--r-- | docs/source/markdown/podman-push.1.md | 11 | ||||
-rw-r--r-- | docs/source/markdown/podman.1.md | 2 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/images.go | 49 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/manifests.go | 61 | ||||
-rw-r--r-- | pkg/bindings/images/types.go | 2 | ||||
-rw-r--r-- | pkg/bindings/images/types_push_options.go | 16 | ||||
-rw-r--r-- | pkg/bindings/manifests/manifests.go | 13 | ||||
-rw-r--r-- | pkg/bindings/manifests/types.go | 6 | ||||
-rw-r--r-- | pkg/bindings/manifests/types_push_options.go | 104 | ||||
-rw-r--r-- | pkg/domain/entities/engine_image.go | 2 | ||||
-rw-r--r-- | pkg/domain/entities/images.go | 4 | ||||
-rw-r--r-- | pkg/domain/entities/manifest.go | 8 | ||||
-rw-r--r-- | pkg/domain/infra/abi/images.go | 40 | ||||
-rw-r--r-- | pkg/domain/infra/abi/manifest.go | 27 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/manifest.go | 22 | ||||
-rw-r--r-- | test/e2e/manifest_test.go | 35 |
20 files changed, 189 insertions, 246 deletions
diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go index d82083cd8..d53a9c066 100644 --- a/cmd/podman/images/push.go +++ b/cmd/podman/images/push.go @@ -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) diff --git a/cmd/podman/manifest/push.go b/cmd/podman/manifest/push.go index 96fea4a21..40b45c40e 100644 --- a/cmd/podman/manifest/push.go +++ b/cmd/podman/manifest/push.go @@ -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 } diff --git a/cmd/podman/utils/alias.go b/cmd/podman/utils/alias.go index 10b96fa98..469233b59 100644 --- a/cmd/podman/utils/alias.go +++ b/cmd/podman/utils/alias.go @@ -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) } diff --git a/docs/source/markdown/podman-manifest-push.1.md b/docs/source/markdown/podman-manifest-push.1.md index 0c1e5ea04..2beb65517 100644 --- a/docs/source/markdown/podman-manifest-push.1.md +++ b/docs/source/markdown/podman-manifest-push.1.md @@ -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. diff --git a/docs/source/markdown/podman-push.1.md b/docs/source/markdown/podman-push.1.md index 68ea528cb..f7624ed5f 100644 --- a/docs/source/markdown/podman-push.1.md +++ b/docs/source/markdown/podman-push.1.md @@ -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. diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 67484c3ec..733fadc30 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -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. | diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 5b15527b7..97cd5a65e 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -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) - } - - 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 { + options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + 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 } diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index 2031dd42f..dce861f6f 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -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() - if err != nil { - utils.InternalServerError(w, err) - return + defer auth.RemoveAuthfile(authfile) + var username, password string + if authConf != nil { + username = authConf.Username + password = authConf.Password + } - sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) - opts := manifests.PushOptions{ - Store: runtime.GetStore(), - ImageListSelection: copy2.CopySpecificImages, - SystemContext: sc, + + 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 query.All { - opts.ImageListSelection = copy2.CopyAllImages + if _, found := r.URL.Query()["tlsVerify"]; found { + options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) } - newD, err := newImage.PushManifest(dest, opts) + 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 } - utils.WriteResponse(w, http.StatusOK, newD.String()) + utils.WriteResponse(w, http.StatusOK, digest) } diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go index 3adb4356b..b3799b8c4 100644 --- a/pkg/bindings/images/types.go +++ b/pkg/bindings/images/types.go @@ -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 diff --git a/pkg/bindings/images/types_push_options.go b/pkg/bindings/images/types_push_options.go index 15210f30b..0c12ce4ac 100644 --- a/pkg/bindings/images/types_push_options.go +++ b/pkg/bindings/images/types_push_options.go @@ -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 diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go index ef4e2a908..b6db64b02 100644 --- a/pkg/bindings/manifests/manifests.go +++ b/pkg/bindings/manifests/manifests.go @@ -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 diff --git a/pkg/bindings/manifests/types.go b/pkg/bindings/manifests/types.go index f5054d424..7f84d69fc 100644 --- a/pkg/bindings/manifests/types.go +++ b/pkg/bindings/manifests/types.go @@ -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 -} diff --git a/pkg/bindings/manifests/types_push_options.go b/pkg/bindings/manifests/types_push_options.go deleted file mode 100644 index 1d689f699..000000000 --- a/pkg/bindings/manifests/types_push_options.go +++ /dev/null @@ -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 -} diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 26a136f13..935ee6f20 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -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) } diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 0805152c3..78a7d8aa7 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -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 diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index 01180951a..6a645e20b 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -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 -} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 1c233d9d5..1288ab09b 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -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 { diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index a68ed8788..139032ad6 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -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 } diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go index c71349fe0..22ca44165 100644 --- a/pkg/domain/infra/tunnel/manifest.go +++ b/pkg/domain/infra/tunnel/manifest.go @@ -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 } diff --git a/test/e2e/manifest_test.go b/test/e2e/manifest_test.go index bc47f7500..3e13057d3 100644 --- a/test/e2e/manifest_test.go +++ b/test/e2e/manifest_test.go @@ -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)) |