diff options
author | Jhon Honce <jhonce@redhat.com> | 2021-12-06 16:45:58 -0700 |
---|---|---|
committer | Jhon Honce <jhonce@redhat.com> | 2022-01-14 16:13:35 -0700 |
commit | 8a7e70919f4bab0757523ae97c170396cb13c83d (patch) | |
tree | 0ec2b5aa4e3c1e6574e606a0e7db3638fdeda578 | |
parent | ec2b213ab611cb197e86c45d03fb10af667ad95c (diff) | |
download | podman-8a7e70919f4bab0757523ae97c170396cb13c83d.tar.gz podman-8a7e70919f4bab0757523ae97c170396cb13c83d.tar.bz2 podman-8a7e70919f4bab0757523ae97c170396cb13c83d.zip |
Refactor manifest list operations
* Update method/function signatures use the manifest list name and
images associated with the operation explicitly, in general
func f(ctx context.Context, manifestListName string,
ImageNames []string, options *fOptions)
* Leverage gorilla/mux Subrouters to support API v3.x and v4.x for
manifests
* Make manifest API endpoints more RESTful
* Add PUT /manifest/{id} to update existing manifests
* Add manifests.Annotate to go bindings, uncommented unit test
* Add DELETE /manifest/{Id} to remove existing manifest list, use
PUT /manifest/{id} to remove images from a list
* Deprecated POST /manifest/{id}/add and /manifest/{id}/remove, use
PUT /manifest/{id} instead
* Corrected swagger godoc and updated to cover API changes
* Update podman manifest commands to use registry.Context()
* Expose utils.GetVar() to obtain query parameters by name
* Unexpose server.registerSwaggerHandlers, not sure why this was ever
exposed.
* Refactored code to use http.Header instead of map[string]string when
operating on HTTP headers.
* Add API-Version header support in bindings to allow calling explicate
versions of the API. Header is _NOT_ forwarded to the API service.
Signed-off-by: Jhon Honce <jhonce@redhat.com>
32 files changed, 1054 insertions, 406 deletions
diff --git a/cmd/podman/manifest/add.go b/cmd/podman/manifest/add.go index 9d219601c..d09533967 100644 --- a/cmd/podman/manifest/add.go +++ b/cmd/podman/manifest/add.go @@ -26,14 +26,14 @@ type manifestAddOptsWrapper struct { var ( manifestAddOpts = manifestAddOptsWrapper{} addCmd = &cobra.Command{ - Use: "add [options] LIST LIST", + Use: "add [options] LIST IMAGE [IMAGE...]", Short: "Add images to a manifest list or image index", Long: "Adds an image to a manifest list or image index.", RunE: add, + Args: cobra.MinimumNArgs(2), ValidArgsFunction: common.AutocompleteImages, Example: `podman manifest add mylist:v1.11 image:v1.11-amd64 podman manifest add mylist:v1.11 transport:imageName`, - Args: cobra.ExactArgs(2), } ) @@ -93,10 +93,6 @@ func add(cmd *cobra.Command, args []string) error { return err } - // FIXME: (@vrothberg) this interface confuses me a lot. Why are they - // not two arguments? - manifestAddOpts.Images = []string{args[1], args[0]} - if manifestAddOpts.CredentialsCLI != "" { creds, err := util.ParseRegistryCreds(manifestAddOpts.CredentialsCLI) if err != nil { @@ -114,10 +110,10 @@ func add(cmd *cobra.Command, args []string) error { manifestAddOpts.SkipTLSVerify = types.NewOptionalBool(!manifestAddOpts.TLSVerifyCLI) } - listID, err := registry.ImageEngine().ManifestAdd(context.Background(), manifestAddOpts.ManifestAddOptions) + listID, err := registry.ImageEngine().ManifestAdd(context.Background(), args[0], args[1:], manifestAddOpts.ManifestAddOptions) if err != nil { return err } - fmt.Printf("%s\n", listID) + fmt.Println(listID) return nil } diff --git a/cmd/podman/manifest/annotate.go b/cmd/podman/manifest/annotate.go index d806ce9e6..a032a1fe5 100644 --- a/cmd/podman/manifest/annotate.go +++ b/cmd/podman/manifest/annotate.go @@ -1,14 +1,12 @@ package manifest import ( - "context" "fmt" "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -20,8 +18,8 @@ var ( Short: "Add or update information about an entry in a manifest list or image index", Long: "Adds or updates information about an entry in a manifest list or image index.", RunE: annotate, - Example: `podman manifest annotate --annotation left=right mylist:v1.11 image:v1.11-amd64`, Args: cobra.ExactArgs(2), + Example: `podman manifest annotate --annotation left=right mylist:v1.11 image:v1.11-amd64`, ValidArgsFunction: common.AutocompleteImages, } ) @@ -63,18 +61,10 @@ func init() { } func annotate(cmd *cobra.Command, args []string) error { - listImageSpec := args[0] - instanceSpec := args[1] - if listImageSpec == "" { - return errors.Errorf(`invalid image name "%s"`, listImageSpec) - } - if instanceSpec == "" { - return errors.Errorf(`invalid image digest "%s"`, instanceSpec) - } - updatedListID, err := registry.ImageEngine().ManifestAnnotate(context.Background(), args, manifestAnnotateOpts) + id, err := registry.ImageEngine().ManifestAnnotate(registry.Context(), args[0], args[1], manifestAnnotateOpts) if err != nil { return err } - fmt.Printf("%s\n", updatedListID) + fmt.Println(id) return nil } diff --git a/cmd/podman/manifest/create.go b/cmd/podman/manifest/create.go index 95c9f89b0..acef53045 100644 --- a/cmd/podman/manifest/create.go +++ b/cmd/podman/manifest/create.go @@ -1,7 +1,6 @@ package manifest import ( - "context" "fmt" "github.com/containers/podman/v3/cmd/podman/common" @@ -36,7 +35,7 @@ func init() { } func create(cmd *cobra.Command, args []string) error { - imageID, err := registry.ImageEngine().ManifestCreate(context.Background(), args[:1], args[1:], manifestCreateOpts) + imageID, err := registry.ImageEngine().ManifestCreate(registry.Context(), args[0], args[1:], manifestCreateOpts) if err != nil { return err } diff --git a/cmd/podman/manifest/inspect.go b/cmd/podman/manifest/inspect.go index d444f9066..ac8ee3dc4 100644 --- a/cmd/podman/manifest/inspect.go +++ b/cmd/podman/manifest/inspect.go @@ -1,7 +1,6 @@ package manifest import ( - "context" "fmt" "github.com/containers/podman/v3/cmd/podman/common" @@ -29,10 +28,10 @@ func init() { } func inspect(cmd *cobra.Command, args []string) error { - buf, err := registry.ImageEngine().ManifestInspect(context.Background(), args[0]) + buf, err := registry.ImageEngine().ManifestInspect(registry.Context(), args[0]) if err != nil { return err } - fmt.Printf("%s\n", buf) + fmt.Println(string(buf)) return nil } diff --git a/cmd/podman/manifest/remove.go b/cmd/podman/manifest/remove.go index c44c0991e..4716af201 100644 --- a/cmd/podman/manifest/remove.go +++ b/cmd/podman/manifest/remove.go @@ -1,7 +1,6 @@ package manifest import ( - "context" "fmt" "github.com/containers/podman/v3/cmd/podman/common" @@ -16,9 +15,9 @@ var ( Short: "Remove an entry from a manifest list or image index", Long: "Removes an image from a manifest list or image index.", RunE: remove, + Args: cobra.ExactArgs(2), ValidArgsFunction: common.AutocompleteImages, Example: `podman manifest remove mylist:v1.11 sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736`, - Args: cobra.ExactArgs(2), } ) @@ -30,18 +29,10 @@ func init() { } func remove(cmd *cobra.Command, args []string) error { - listImageSpec := args[0] - instanceSpec := args[1] - if listImageSpec == "" { - return errors.Errorf(`invalid image name "%s"`, listImageSpec) - } - if instanceSpec == "" { - return errors.Errorf(`invalid image digest "%s"`, instanceSpec) - } - updatedListID, err := registry.ImageEngine().ManifestRemove(context.Background(), args) + updatedListID, err := registry.ImageEngine().ManifestRemoveDigest(registry.Context(), args[0], args[1]) if err != nil { - return errors.Wrapf(err, "error removing from manifest list %s", listImageSpec) + return errors.Wrapf(err, "error removing from manifest list %s", args[0]) } - fmt.Printf("%s\n", updatedListID) + fmt.Println(updatedListID) return nil } diff --git a/cmd/podman/manifest/rm.go b/cmd/podman/manifest/rm.go index 5e78197ed..b4a52653b 100644 --- a/cmd/podman/manifest/rm.go +++ b/cmd/podman/manifest/rm.go @@ -12,13 +12,13 @@ import ( var ( rmCmd = &cobra.Command{ - Use: "rm LIST", + Use: "rm LIST [LIST...]", Short: "Remove manifest list or image index from local storage", Long: "Remove manifest list or image index from local storage.", RunE: rm, + Args: cobra.MinimumNArgs(1), ValidArgsFunction: common.AutocompleteImages, Example: `podman manifest rm mylist:v1.11`, - Args: cobra.ExactArgs(1), } ) diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index eb0b6827f..ef0839d1f 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -3,7 +3,11 @@ package libpod import ( "context" "encoding/json" + "fmt" + "io/ioutil" "net/http" + "net/url" + "strings" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/manifest" @@ -15,6 +19,8 @@ import ( "github.com/containers/podman/v3/pkg/auth" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/infra/abi" + "github.com/containers/podman/v3/pkg/errorhandling" + "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -24,40 +30,93 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Name []string `schema:"name"` - Image []string `schema:"image"` - All bool `schema:"all"` + Name string `schema:"name"` + Images []string `schema:"images"` + All bool `schema:"all"` }{ // Add defaults here once needed. } + + // Support 3.x API calls, alias image to images + if image, ok := r.URL.Query()["image"]; ok { + query.Images = image + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) return } - // TODO: (jhonce) When c/image is refactored the roadmap calls for this check to be pushed into that library. - for _, n := range query.Name { - if _, err := reference.ParseNormalizedNamed(n); err != nil { + // Support 4.x API calls, map query parameter to path + if name, ok := mux.Vars(r)["name"]; ok { + n, err := url.QueryUnescape(name) + if err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - errors.Wrapf(err, "invalid image name %s", n)) + errors.Wrapf(err, "failed to parse name parameter %q", name)) return } + query.Name = n + } + + if _, err := reference.ParseNormalizedNamed(query.Name); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "invalid image name %s", query.Name)) + return } imageEngine := abi.ImageEngine{Libpod: runtime} createOptions := entities.ManifestCreateOptions{All: query.All} - manID, err := imageEngine.ManifestCreate(r.Context(), query.Name, query.Image, createOptions) + manID, err := imageEngine.ManifestCreate(r.Context(), query.Name, query.Images, createOptions) + if err != nil { + utils.InternalServerError(w, err) + return + } + + status := http.StatusOK + if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == utils.ErrVersionNotSupported { + status = http.StatusCreated + } + + buffer, err := ioutil.ReadAll(r.Body) + if err != nil { + utils.InternalServerError(w, err) + return + } + + // Treat \r\n as empty body + if len(buffer) < 3 { + utils.WriteResponse(w, status, handlers.IDResponse{ID: manID}) + return + } + + body := new(entities.ManifestModifyOptions) + if err := json.Unmarshal(buffer, body); err != nil { + utils.InternalServerError(w, errors.Wrap(err, "Decode()")) + return + } + + // gather all images for manifest list + var images []string + if len(query.Images) > 0 { + images = append(query.Images) + } + if len(body.Images) > 0 { + images = append(body.Images) + } + + id, err := imageEngine.ManifestAdd(r.Context(), query.Name, images, body.ManifestAddOptions) if err != nil { utils.InternalServerError(w, err) return } - utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: manID}) + + utils.WriteResponse(w, status, handlers.IDResponse{ID: id}) } -// ExistsManifest check if a manifest list exists -func ExistsManifest(w http.ResponseWriter, r *http.Request) { +// ManifestExists return true if manifest list exists. +func ManifestExists(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) name := utils.GetName(r) @@ -94,10 +153,18 @@ func ManifestInspect(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, schema2List) } +// ManifestAdd remove digest from manifest list +// +// Deprecated: As of 4.0.0 use ManifestModify instead func ManifestAdd(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) - var addOptions entities.ManifestAddOptions - if err := json.NewDecoder(r.Body).Decode(&addOptions); err != nil { + + // Wrapper to support 3.x with 4.x libpod + query := struct { + entities.ManifestAddOptions + Images []string + }{} + if err := json.NewDecoder(r.Body).Decode(&query); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } @@ -108,15 +175,8 @@ func ManifestAdd(w http.ResponseWriter, r *http.Request) { return } - // FIXME: we really need to clean up the manifest API. Swagger states - // the arguments were strings not string slices. The use of string - // slices, mixing lists and images is incredibly confusing. - if len(addOptions.Images) == 1 { - addOptions.Images = append(addOptions.Images, name) - } - imageEngine := abi.ImageEngine{Libpod: runtime} - newID, err := imageEngine.ManifestAdd(r.Context(), addOptions) + newID, err := imageEngine.ManifestAdd(r.Context(), name, query.Images, query.ManifestAddOptions) if err != nil { utils.InternalServerError(w, err) return @@ -124,7 +184,10 @@ func ManifestAdd(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: newID}) } -func ManifestRemove(w http.ResponseWriter, r *http.Request) { +// ManifestRemoveDigest remove digest from manifest list +// +// Deprecated: As of 4.0.0 use ManifestModify instead +func ManifestRemoveDigest(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { @@ -155,7 +218,10 @@ func ManifestRemove(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: manifestList.ID()}) } -func ManifestPush(w http.ResponseWriter, r *http.Request) { +// ManifestPushV3 push image to registry +// +// Deprecated: As of 4.0.0 use ManifestPush instead +func ManifestPushV3(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { @@ -207,3 +273,156 @@ func ManifestPush(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, digest) } + +// ManifestPush push image to registry +// +// As of 4.0.0 +func ManifestPush(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) + + query := struct { + All bool `schema:"all"` + TLSVerify bool `schema:"tlsVerify"` + }{ + // Add defaults here once needed. + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + destination := utils.GetVar(r, "destination") + if err := utils.IsRegistryReference(destination); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) + return + } + + authconf, authfile, err := auth.GetCredentials(r) + if err != nil { + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse registry header for %s", r.URL.String())) + return + } + 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, + 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} + source := utils.GetName(r) + digest, err := imageEngine.ManifestPush(context.Background(), source, destination, options) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "error pushing image %q", destination)) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: digest}) +} + +// ManifestModify efficiently updates the named manifest list +func ManifestModify(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + imageEngine := abi.ImageEngine{Libpod: runtime} + + body := new(entities.ManifestModifyOptions) + if err := json.NewDecoder(r.Body).Decode(body); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + + name := utils.GetName(r) + if _, err := runtime.LibimageRuntime().LookupManifestList(name); err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, err) + return + } + + var report entities.ManifestModifyReport + switch { + case strings.EqualFold("update", body.Operation): + id, err := imageEngine.ManifestAdd(r.Context(), name, body.Images, body.ManifestAddOptions) + if err != nil { + report.Errors = append(report.Errors, err) + break + } + report = entities.ManifestModifyReport{ + ID: id, + Images: body.Images, + } + case strings.EqualFold("remove", body.Operation): + for _, image := range body.Images { + id, err := imageEngine.ManifestRemoveDigest(r.Context(), name, image) + if err != nil { + report.Errors = append(report.Errors, err) + continue + } + report.ID = id + report.Images = append(report.Images, image) + } + case strings.EqualFold("annotate", body.Operation): + options := entities.ManifestAnnotateOptions{ + Annotation: body.Annotation, + Arch: body.Arch, + Features: body.Features, + OS: body.OS, + OSFeatures: body.OSFeatures, + OSVersion: body.OSVersion, + Variant: body.Variant, + } + for _, image := range body.Images { + id, err := imageEngine.ManifestAnnotate(r.Context(), name, image, options) + if err != nil { + report.Errors = append(report.Errors, err) + continue + } + report.ID = id + report.Images = append(report.Images, image) + } + default: + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + fmt.Errorf("illegal operation %q for %q", body.Operation, r.URL.String())) + return + } + + statusCode := http.StatusOK + switch { + case len(report.Errors) > 0 && len(report.Images) > 0: + statusCode = http.StatusConflict + case len(report.Errors) > 0: + statusCode = http.StatusInternalServerError + } + utils.WriteResponse(w, statusCode, report) +} + +// ManifestDelete removes a manifest list from storage +func ManifestDelete(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + imageEngine := abi.ImageEngine{Libpod: runtime} + + name := utils.GetName(r) + if _, err := runtime.LibimageRuntime().LookupManifestList(name); err != nil { + utils.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound, err) + return + } + + results, errs := imageEngine.ManifestRm(r.Context(), []string{name}) + errsString := errorhandling.ErrorsToStrings(errs) + report := handlers.LibpodImagesRemoveReport{ + ImageRemoveReport: *results, + Errors: errsString, + } + utils.WriteResponse(w, http.StatusOK, report) +} diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 96b7a957c..ee83755a1 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -174,7 +174,7 @@ func FilterMapToString(filters map[string][]string) (string, error) { return string(f), nil } -func getVar(r *http.Request, k string) string { +func GetVar(r *http.Request, k string) string { val := mux.Vars(r)[k] safeVal, err := url.PathUnescape(val) if err != nil { @@ -186,5 +186,5 @@ func getVar(r *http.Request, k string) string { // GetName extracts the name from the mux func GetName(r *http.Request) string { - return getVar(r, "name") + return GetVar(r, "name") } diff --git a/pkg/api/server/register_manifest.go b/pkg/api/server/register_manifest.go index 010d8a79e..8cd3d8b22 100644 --- a/pkg/api/server/register_manifest.go +++ b/pkg/api/server/register_manifest.go @@ -8,7 +8,9 @@ import ( ) func (s *APIServer) registerManifestHandlers(r *mux.Router) error { - // swagger:operation POST /libpod/manifests/create manifests ManifestCreateLibpod + v3 := r.PathPrefix("/v{version:[0-3][0-9A-Za-z.-]*}/libpod/manifests").Subrouter() + v4 := r.PathPrefix("/v{version:[4-9][0-9A-Za-z.-]*}/libpod/manifests").Subrouter() + // swagger:operation POST /libpod/manifests manifests ManifestCreateLibpod // --- // summary: Create // description: Create a manifest list @@ -18,18 +20,30 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // - in: query // name: name // type: string - // description: manifest list name + // description: manifest list or index name to create // required: true // - in: query - // name: image + // name: images // type: string - // description: name of the image + // required: true + // description: | + // One or more names of an image or a manifest list. Repeat parameter as needed. + // + // Support for multiple images, as of version 4.0.0 + // Alias of `image` is support for compatibility with < 4.0.0 + // Response status code is 200 with < 4.0.0 for compatibility // - in: query // name: all // type: boolean // description: add all contents if given list + // - in: body + // name: options + // description: options for new manifest + // required: false + // schema: + // $ref: "#/definitions/ManifestModifyOptions" // responses: - // 200: + // 201: // schema: // $ref: "#/definitions/IDResponse" // 400: @@ -38,17 +52,21 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchImage" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/manifests/create"), s.APIHandler(libpod.ManifestCreate)).Methods(http.MethodPost) + v3.Handle("/create", s.APIHandler(libpod.ManifestCreate)).Methods(http.MethodPost) + v4.Handle("/{name:.*}", s.APIHandler(libpod.ManifestCreate)).Methods(http.MethodPost) // swagger:operation GET /libpod/manifests/{name}/exists manifests ManifestExistsLibpod // --- // summary: Exists - // description: Check if manifest list exists + // description: | + // Check if manifest list exists + // + // Note: There is no contract that the manifest list will exist for a follow-on operation // parameters: // - in: path // name: name // type: string // required: true - // description: the name of the manifest list + // description: the name or ID of the manifest list // produces: // - application/json // responses: @@ -58,11 +76,12 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // $ref: '#/responses/NoSuchManifest' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/manifests/{name}/exists"), s.APIHandler(libpod.ExistsManifest)).Methods(http.MethodGet) + v3.Handle("/{name:.*}/exists", s.APIHandler(libpod.ManifestExists)).Methods(http.MethodGet) + v4.Handle("/{name:.*}/exists", s.APIHandler(libpod.ManifestExists)).Methods(http.MethodGet) // swagger:operation GET /libpod/manifests/{name}/json manifests ManifestInspectLibpod // --- // summary: Inspect - // description: Display a manifest list + // description: Display attributes of given manifest list // produces: // - application/json // parameters: @@ -70,7 +89,7 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // name: name // type: string // required: true - // description: the name or ID of the manifest + // description: the name or ID of the manifest list // responses: // 200: // $ref: "#/responses/InspectManifest" @@ -78,11 +97,53 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchManifest" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/manifests/{name:.*}/json"), s.APIHandler(libpod.ManifestInspect)).Methods(http.MethodGet) + v3.Handle("/{name:.*}/json", s.APIHandler(libpod.ManifestInspect)).Methods(http.MethodGet) + v4.Handle("/{name:.*}/json", s.APIHandler(libpod.ManifestInspect)).Methods(http.MethodGet) + // swagger:operation PUT /libpod/manifests/{name} manifests ManifestModifyLibpod + // --- + // summary: Modify manifest list + // description: | + // Add/Remove an image(s) to a manifest list + // + // Note: operations are not atomic when multiple Images are provided. + // + // As of v4.0.0 + // produces: + // - application/json + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the manifest + // - in: body + // name: options + // description: options for mutating a manifest + // required: true + // schema: + // $ref: "#/definitions/ManifestModifyOptions" + // responses: + // 200: + // schema: + // $ref: "#/definitions/ManifestModifyReport" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 400: + // $ref: "#/responses/BadParamError" + // 409: + // description: Operation had partial success, both Images and Errors may have members + // schema: + // $ref: "#/definitions/ManifestModifyReport" + // 500: + // $ref: "#/responses/InternalError" + v4.Handle("/{name:.*}", s.APIHandler(libpod.ManifestModify)).Methods(http.MethodPut) // swagger:operation POST /libpod/manifests/{name}/add manifests ManifestAddLibpod // --- // summary: Add image - // description: Add an image to a manifest list + // description: | + // Add an image to a manifest list + // + // Deprecated: As of 4.0.0 use ManifestModifyLibpod instead // produces: // - application/json // parameters: @@ -95,7 +156,7 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // name: options // description: options for creating a manifest // schema: - // $ref: "#/definitions/ManifestAddOpts" + // $ref: "#/definitions/ManifestAddOptions" // responses: // 200: // schema: @@ -106,11 +167,14 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // $ref: "#/responses/BadParamError" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/manifests/{name:.*}/add"), s.APIHandler(libpod.ManifestAdd)).Methods(http.MethodPost) - // swagger:operation DELETE /libpod/manifests/{name} manifests ManifestDeleteLibpod + v3.Handle("/{name:.*}/add", s.APIHandler(libpod.ManifestAdd)).Methods(http.MethodPost) + // swagger:operation DELETE /libpod/manifests/{name} manifests ManifestDeleteV3Libpod // --- - // summary: Remove - // description: Remove an image from a manifest list + // summary: Remove image from a manifest list + // description: | + // Remove an image from a manifest list + // + // Deprecated: As of 4.0.0 use ManifestModifyLibpod instead // produces: // - application/json // parameters: @@ -133,11 +197,37 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchManifest" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/manifests/{name:.*}"), s.APIHandler(libpod.ManifestRemove)).Methods(http.MethodDelete) - // swagger:operation POST /libpod/manifests/{name}/push manifests ManifestPushLibpod + v3.Handle("/{name:.*}", s.APIHandler(libpod.ManifestRemoveDigest)).Methods(http.MethodDelete) + // swagger:operation DELETE /libpod/manifests/{name} manifests ManifestDeleteLibpod // --- - // summary: Push - // description: Push a manifest list or image index to a registry + // summary: Delete manifest list + // description: | + // Delete named manifest list + // + // As of v4.0.0 + // produces: + // - application/json + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: The name or ID of the list to be deleted + // responses: + // 200: + // $ref: "#/responses/DocsLibpodImagesRemoveResponse" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 500: + // $ref: "#/responses/InternalError" + v4.Handle("/{name:.*}", s.APIHandler(libpod.ManifestDelete)).Methods(http.MethodDelete) + // swagger:operation POST /libpod/manifests/{name}/push manifests ManifestPushV3Libpod + // --- + // summary: Push manifest to registry + // description: | + // Push a manifest list or image index to a registry + // + // Deprecated: As of 4.0.0 use ManifestPushLibpod instead // produces: // - application/json // parameters: @@ -165,6 +255,47 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchManifest" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/manifests/{name}/push"), s.APIHandler(libpod.ManifestPush)).Methods(http.MethodPost) + v3.Handle("/{name}/push", s.APIHandler(libpod.ManifestPushV3)).Methods(http.MethodPost) + // swagger:operation POST /libpod/manifests/{name}/registry/{destination} manifests ManifestPushLibpod + // --- + // summary: Push manifest list to registry + // description: | + // Push a manifest list or image index to the named registry + // + // As of v4.0.0 + // produces: + // - application/json + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the manifest list + // - in: path + // name: destination + // type: string + // required: true + // description: the registry for the manifest list + // - in: query + // name: all + // description: push all images + // type: boolean + // default: false + // - in: query + // name: tlsVerify + // type: boolean + // default: false + // description: skip TLS verification for registries + // responses: + // 200: + // schema: + // $ref: "#/definitions/IDResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 500: + // $ref: "#/responses/InternalError" + v4.Handle("/{name:.*}/registry/{destination:.*}", s.APIHandler(libpod.ManifestPush)).Methods(http.MethodPost) return nil } diff --git a/pkg/api/server/register_swagger.go b/pkg/api/server/register_swagger.go index dca1df14b..48af7713f 100644 --- a/pkg/api/server/register_swagger.go +++ b/pkg/api/server/register_swagger.go @@ -7,8 +7,8 @@ import ( "github.com/gorilla/mux" ) -// RegisterSwaggerHandlers maps the swagger endpoint for the server -func (s *APIServer) RegisterSwaggerHandlers(r *mux.Router) error { +// registerSwaggerHandlers maps the swagger endpoint for the server +func (s *APIServer) registerSwaggerHandlers(r *mux.Router) error { // This handler does _*NOT*_ provide an UI rather just a swagger spec that an UI could render r.HandleFunc(VersionedPath("/libpod/swagger"), s.APIHandler(libpod.ServeSwagger)).Methods(http.MethodGet) return nil diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 8c5c7aeeb..65b7e2474 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -151,7 +151,7 @@ func newServer(runtime *libpod.Runtime, listener *net.Listener, opts entities.Se server.registerPluginsHandlers, server.registerPodsHandlers, server.registerSecretHandlers, - server.RegisterSwaggerHandlers, + server.registerSwaggerHandlers, server.registerSwarmHandlers, server.registerSystemHandlers, server.registerVersionHandlers, diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index f423c011d..419225007 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -140,7 +140,7 @@ func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]t // MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can // conveniently be used in the http stack. -func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (map[string]string, error) { +func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (http.Header, error) { if sys == nil { sys = &types.SystemContext{} } @@ -163,18 +163,18 @@ func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password stri if err != nil { return nil, err } - return map[string]string{xRegistryConfigHeader: content}, nil + return http.Header{xRegistryConfigHeader: []string{content}}, nil } // MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can // conveniently be used in the http stack. -func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (map[string]string, error) { +func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (http.Header, error) { if username != "" { content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) if err != nil { return nil, err } - return map[string]string{xRegistryAuthHeader: content}, nil + return http.Header{xRegistryAuthHeader: []string{content}}, nil } if sys == nil { @@ -188,7 +188,7 @@ func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string if err != nil { return nil, err } - return map[string]string{xRegistryAuthHeader: content}, nil + return http.Header{xRegistryAuthHeader: []string{content}}, nil } // RemoveAuthfile is a convenience function that is meant to be called in a diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go index f7e6e4ef6..2c79f0b7c 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/auth_test.go @@ -85,8 +85,8 @@ func TestMakeXRegistryConfigHeaderGetCredentialsRoundtrip(t *testing.T) { require.NoError(t, err) req, err := http.NewRequest(http.MethodPost, "/", nil) require.NoError(t, err, tc.name) - for k, v := range headers { - req.Header.Set(k, v) + for _, v := range headers.Values(xRegistryConfigHeader) { + req.Header.Add(xRegistryConfigHeader, v) } override, resPath, err := GetCredentials(req) @@ -137,8 +137,8 @@ func TestMakeXRegistryAuthHeaderGetCredentialsRoundtrip(t *testing.T) { require.NoError(t, err) req, err := http.NewRequest(http.MethodPost, "/", nil) require.NoError(t, err, tc.name) - for k, v := range headers { - req.Header.Set(k, v) + for _, v := range headers.Values(xRegistryAuthHeader) { + req.Header.Set(xRegistryAuthHeader, v) } override, resPath, err := GetCredentials(req) @@ -219,7 +219,7 @@ func TestMakeXRegistryConfigHeader(t *testing.T) { require.Len(t, res, 1, tc.name) header, ok := res[xRegistryConfigHeader] require.True(t, ok, tc.name) - decodedHeader, err := base64.URLEncoding.DecodeString(header) + decodedHeader, err := base64.URLEncoding.DecodeString(header[0]) require.NoError(t, err, tc.name) // Don't test for a specific JSON representation, just for the expected contents. expected := map[string]interface{}{} @@ -282,7 +282,7 @@ func TestMakeXRegistryAuthHeader(t *testing.T) { require.Len(t, res, 1, tc.name) header, ok := res[xRegistryAuthHeader] require.True(t, ok, tc.name) - decodedHeader, err := base64.URLEncoding.DecodeString(header) + decodedHeader, err := base64.URLEncoding.DecodeString(header[0]) require.NoError(t, err, tc.name) // Don't test for a specific JSON representation, just for the expected contents. expected := map[string]interface{}{} diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index b2e949f67..332aa97c8 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -15,7 +15,6 @@ import ( "github.com/blang/semver" "github.com/containers/podman/v3/pkg/terminal" "github.com/containers/podman/v3/version" - jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" @@ -35,16 +34,24 @@ type Connection struct { type valueKey string const ( - clientKey = valueKey("Client") + clientKey = valueKey("Client") + versionKey = valueKey("ServiceVersion") ) // GetClient from context build by NewConnection() func GetClient(ctx context.Context) (*Connection, error) { - c, ok := ctx.Value(clientKey).(*Connection) - if !ok { - return nil, errors.Errorf("ClientKey not set in context") + if c, ok := ctx.Value(clientKey).(*Connection); ok { + return c, nil } - return c, nil + return nil, errors.Errorf("%s not set in context", clientKey) +} + +// ServiceVersion from context build by NewConnection() +func ServiceVersion(ctx context.Context) *semver.Version { + if v, ok := ctx.Value(versionKey).(*semver.Version); ok { + return v + } + return new(semver.Version) } // JoinURL elements with '/' @@ -52,6 +59,7 @@ func JoinURL(elements ...string) string { return "/" + strings.Join(elements, "/") } +// NewConnection creates a new service connection without an identity func NewConnection(ctx context.Context, uri string) (context.Context, error) { return NewConnectionWithIdentity(ctx, uri, "") } @@ -116,9 +124,11 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) } ctx = context.WithValue(ctx, clientKey, &connection) - if err := pingNewConnection(ctx); err != nil { + serviceVersion, err := pingNewConnection(ctx) + if err != nil { return nil, errors.Wrap(err, "unable to connect to Podman socket") } + ctx = context.WithValue(ctx, versionKey, serviceVersion) return ctx, nil } @@ -139,15 +149,15 @@ func tcpClient(_url *url.URL) Connection { // pingNewConnection pings to make sure the RESTFUL service is up // and running. it should only be used when initializing a connection -func pingNewConnection(ctx context.Context) error { +func pingNewConnection(ctx context.Context) (*semver.Version, error) { client, err := GetClient(ctx) if err != nil { - return err + return nil, err } // the ping endpoint sits at / in this case response, err := client.DoRequest(ctx, nil, http.MethodGet, "/_ping", nil, nil) if err != nil { - return err + return nil, err } defer response.Body.Close() @@ -155,23 +165,23 @@ func pingNewConnection(ctx context.Context) error { versionHdr := response.Header.Get("Libpod-API-Version") if versionHdr == "" { logrus.Info("Service did not provide Libpod-API-Version Header") - return nil + return new(semver.Version), nil } versionSrv, err := semver.ParseTolerant(versionHdr) if err != nil { - return err + return nil, err } switch version.APIVersion[version.Libpod][version.MinimalAPI].Compare(versionSrv) { case -1, 0: // Server's job when Client version is equal or older - return nil + return &versionSrv, nil case 1: - return errors.Errorf("server API version is too old. Client %q server %q", + return nil, errors.Errorf("server API version is too old. Client %q server %q", version.APIVersion[version.Libpod][version.MinimalAPI].String(), versionSrv.String()) } } - return errors.Errorf("ping response was %d", response.StatusCode) + return nil, errors.Errorf("ping response was %d", response.StatusCode) } func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) (Connection, error) { @@ -306,26 +316,29 @@ func unixClient(_url *url.URL) Connection { } // DoRequest assembles the http request and returns the response -func (c *Connection) DoRequest(ctx context.Context, httpBody io.Reader, httpMethod, endpoint string, queryParams url.Values, header map[string]string, pathValues ...string) (*APIResponse, error) { +func (c *Connection) DoRequest(ctx context.Context, httpBody io.Reader, httpMethod, endpoint string, queryParams url.Values, headers http.Header, pathValues ...string) (*APIResponse, error) { var ( err error response *http.Response ) - params := make([]interface{}, len(pathValues)+3) + params := make([]interface{}, len(pathValues)+1) + + if v := headers.Values("API-Version"); len(v) > 0 { + params[0] = v[0] + } else { + // Including the semver suffices breaks older services... so do not include them + v := version.APIVersion[version.Libpod][version.CurrentAPI] + params[0] = fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) + } - // Including the semver suffices breaks older services... so do not include them - v := version.APIVersion[version.Libpod][version.CurrentAPI] - params[0] = v.Major - params[1] = v.Minor - params[2] = v.Patch for i, pv := range pathValues { // url.URL lacks the semantics for escaping embedded path parameters... so we manually // escape each one and assume the caller included the correct formatting in "endpoint" - params[i+3] = url.PathEscape(pv) + params[i+1] = url.PathEscape(pv) } - uri := fmt.Sprintf("http://d/v%d.%d.%d/libpod"+endpoint, params...) + uri := fmt.Sprintf("http://d/v%s/libpod"+endpoint, params...) logrus.Debugf("DoRequest Method: %s URI: %v", httpMethod, uri) req, err := http.NewRequestWithContext(ctx, httpMethod, uri, httpBody) @@ -335,9 +348,17 @@ func (c *Connection) DoRequest(ctx context.Context, httpBody io.Reader, httpMeth if len(queryParams) > 0 { req.URL.RawQuery = queryParams.Encode() } - for key, val := range header { - req.Header.Set(key, val) + + for key, val := range headers { + if key == "API-Version" { + continue + } + + for _, v := range val { + req.Header.Add(key, v) + } } + // Give the Do three chances in the case of a comm/service hiccup for i := 1; i <= 3; i++ { response, err = c.Client.Do(req) // nolint @@ -349,7 +370,7 @@ func (c *Connection) DoRequest(ctx context.Context, httpBody io.Reader, httpMeth return &APIResponse{response, req}, err } -// Get raw Transport.DialContext from client +// GetDialer returns raw Transport.DialContext from client func (c *Connection) GetDialer(ctx context.Context) (net.Conn, error) { client := c.Client transport := client.Transport.(*http.Transport) @@ -360,16 +381,6 @@ func (c *Connection) GetDialer(ctx context.Context) (net.Conn, error) { return nil, errors.New("Unable to get dial context") } -// FiltersToString converts our typical filter format of a -// map[string][]string to a query/html safe string. -func FiltersToString(filters map[string][]string) (string, error) { - lowerCaseKeys := make(map[string][]string) - for k, v := range filters { - lowerCaseKeys[strings.ToLower(k)] = v - } - return jsoniter.MarshalToString(lowerCaseKeys) -} - // IsInformational returns true if the response code is 1xx func (h *APIResponse) IsInformational() bool { return h.Response.StatusCode/100 == 1 diff --git a/pkg/bindings/containers/attach.go b/pkg/bindings/containers/attach.go index baa3f182e..c02265cd8 100644 --- a/pkg/bindings/containers/attach.go +++ b/pkg/bindings/containers/attach.go @@ -108,9 +108,9 @@ func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Wri }() } - headers := make(map[string]string) - headers["Connection"] = "Upgrade" - headers["Upgrade"] = "tcp" + headers := make(http.Header) + headers.Add("Connection", "Upgrade") + headers.Add("Upgrade", "tcp") var socket net.Conn socketSet := false @@ -157,7 +157,7 @@ func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Wri } stdoutChan := make(chan error) - stdinChan := make(chan error, 1) //stdin channel should not block + stdinChan := make(chan error, 1) // stdin channel should not block if isSet.stdin { go func() { diff --git a/pkg/bindings/errors.go b/pkg/bindings/errors.go index ec837b39c..be184b916 100644 --- a/pkg/bindings/errors.go +++ b/pkg/bindings/errors.go @@ -25,7 +25,7 @@ func (h APIResponse) Process(unmarshalInto interface{}) error { return h.ProcessWithError(unmarshalInto, &errorhandling.ErrorModel{}) } -// Process drains the response body, and processes the HTTP status code +// ProcessWithError drains the response body, and processes the HTTP status code // Note: Closing the response.Body is left to the caller func (h APIResponse) ProcessWithError(unmarshalInto interface{}, unmarshalErrorInto interface{}) error { data, err := ioutil.ReadAll(h.Response.Body) diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 6b5159f52..9880c73e4 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -300,7 +300,7 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO } var ( - headers map[string]string + headers http.Header err error ) if options.SystemContext != nil && options.SystemContext.DockerAuthConfig != nil { @@ -421,7 +421,7 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO return nil, err } - //add tmp file to context dir + // add tmp file to context dir tarContent = append(tarContent, tmpSecretFile.Name()) modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFile.Name())) @@ -634,7 +634,7 @@ func nTar(excludes []string, sources ...string) (io.ReadCloser, error) { if lerr := tw.WriteHeader(hdr); lerr != nil { return lerr } - } //skip other than file,folder and symlinks + } // skip other than file,folder and symlinks return nil }) merr = multierror.Append(merr, err) diff --git a/pkg/bindings/internal/util/util.go b/pkg/bindings/internal/util/util.go index bcf6959f2..f8f99d6c1 100644 --- a/pkg/bindings/internal/util/util.go +++ b/pkg/bindings/internal/util/util.go @@ -104,3 +104,11 @@ func ToParams(o interface{}) (url.Values, error) { } return params, nil } + +func MapToArrayString(data map[string]string) []string { + l := make([]string, 0) + for k, v := range data { + l = append(l, k+"="+v) + } + return l +} diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go index af74eb406..50e324efa 100644 --- a/pkg/bindings/manifests/manifests.go +++ b/pkg/bindings/manifests/manifests.go @@ -3,15 +3,18 @@ package manifests import ( "context" "errors" + "fmt" "net/http" "net/url" "strconv" "strings" + "github.com/blang/semver" "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/bindings/images" + "github.com/containers/podman/v3/version" jsoniter "github.com/json-iterator/go" ) @@ -19,7 +22,7 @@ import ( // the new manifest can also be specified. The all boolean specifies to add all entries // of a list if the name provided is a manifest list. The ID of the new manifest list // is returned as a string. -func Create(ctx context.Context, names, images []string, options *CreateOptions) (string, error) { +func Create(ctx context.Context, name string, images []string, options *CreateOptions) (string, error) { var idr handlers.IDResponse if options == nil { options = new(CreateOptions) @@ -28,21 +31,19 @@ func Create(ctx context.Context, names, images []string, options *CreateOptions) if err != nil { return "", err } - if len(names) < 1 { + if len(name) < 1 { return "", errors.New("creating a manifest requires at least one name argument") } params, err := options.ToParams() if err != nil { return "", err } - for _, name := range names { - params.Add("name", name) - } + for _, i := range images { - params.Add("image", i) + params.Add("images", i) } - response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/create", params, nil) + response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/%s", params, nil, name) if err != nil { return "", err } @@ -67,70 +68,96 @@ func Exists(ctx context.Context, name string, options *ExistsOptions) (bool, err } // Inspect returns a manifest list for a given name. -func Inspect(ctx context.Context, name string, options *InspectOptions) (*manifest.Schema2List, error) { - var list manifest.Schema2List - if options == nil { - options = new(InspectOptions) - } - _ = options +func Inspect(ctx context.Context, name string, _ *InspectOptions) (*manifest.Schema2List, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } + response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/manifests/%s/json", nil, nil, name) if err != nil { return nil, err } defer response.Body.Close() + var list manifest.Schema2List return &list, response.Process(&list) } // Add adds a manifest to a given manifest list. Additional options for the manifest // can also be specified. The ID of the new manifest list is returned as a string func Add(ctx context.Context, name string, options *AddOptions) (string, error) { - var idr handlers.IDResponse if options == nil { options = new(AddOptions) } + + if bindings.ServiceVersion(ctx).GTE(semver.MustParse("4.0.0")) { + optionsv4 := ModifyOptions{ + All: options.All, + Annotations: options.Annotation, + Arch: options.Arch, + Features: options.Features, + Images: options.Images, + OS: options.OS, + OSFeatures: nil, + OSVersion: options.OSVersion, + Variant: options.Variant, + } + optionsv4.WithOperation("update") + return Modify(ctx, name, options.Images, &optionsv4) + } + + // API Version < 4.0.0 conn, err := bindings.GetClient(ctx) if err != nil { return "", err } - optionsString, err := jsoniter.MarshalToString(options) + opts, err := jsoniter.MarshalToString(options) if err != nil { return "", err } - stringReader := strings.NewReader(optionsString) - response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/manifests/%s/add", nil, nil, name) + reader := strings.NewReader(opts) + + headers := make(http.Header) + v := version.APIVersion[version.Libpod][version.MinimalAPI] + headers.Add("API-Version", + fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)) + response, err := conn.DoRequest(ctx, reader, http.MethodPost, "/manifests/%s/add", nil, headers, name) if err != nil { return "", err } defer response.Body.Close() + var idr handlers.IDResponse return idr.ID, response.Process(&idr) } // Remove deletes a manifest entry from a manifest list. Both name and the digest to be // removed are mandatory inputs. The ID of the new manifest list is returned as a string. -func Remove(ctx context.Context, name, digest string, options *RemoveOptions) (string, error) { - var idr handlers.IDResponse - if options == nil { - options = new(RemoveOptions) +func Remove(ctx context.Context, name, digest string, _ *RemoveOptions) (string, error) { + if bindings.ServiceVersion(ctx).GTE(semver.MustParse("4.0.0")) { + optionsv4 := new(ModifyOptions).WithOperation("remove") + return Modify(ctx, name, []string{digest}, optionsv4) } - _ = options + + // API Version < 4.0.0 conn, err := bindings.GetClient(ctx) if err != nil { return "", err } + + headers := http.Header{} + headers.Add("API-Version", "3.4.0") + params := url.Values{} params.Set("digest", digest) - response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/manifests/%s", params, nil, name) + response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/manifests/%s", params, headers, name) if err != nil { return "", err } defer response.Body.Close() + var idr handlers.IDResponse return idr.ID, response.Process(&idr) } @@ -151,19 +178,26 @@ func Push(ctx context.Context, name, destination string, options *images.PushOpt if err != nil { return "", err } + params, err := options.ToParams() 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 + // 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) - response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/%s/push", params, nil, name) + + var response *bindings.APIResponse + if bindings.ServiceVersion(ctx).GTE(semver.MustParse("4.0.0")) { + response, err = conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/%s/registry/%s", params, nil, name, destination) + } else { + params.Set("image", name) + params.Set("destination", destination) + response, err = conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/%s/push", params, nil, name) + } if err != nil { return "", err } @@ -172,25 +206,37 @@ func Push(ctx context.Context, name, destination string, options *images.PushOpt return idr.ID, err } -// There is NO annotate endpoint. this binding could never work -// Annotate updates the image configuration of a given manifest list -//func Annotate(ctx context.Context, name, digest string, options image.ManifestAnnotateOpts) (string, error) { -// var idr handlers.IDResponse -// conn, err := bindings.GetClient(ctx) -// if err != nil { -// return "", err -// } -// params := url.Values{} -// params.Set("digest", digest) -// optionsString, err := jsoniter.MarshalToString(options) -// if err != nil { -// return "", err -// } -// stringReader := strings.NewReader(optionsString) -// response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/manifests/%s/annotate", params, name) -// if err != nil { -// return "", err -// } -// defer response.Body.Close() -// return idr.ID, response.Process(&idr) -//} +// Modify modifies the given manifest list using options and the optional list of images +func Modify(ctx context.Context, name string, images []string, options *ModifyOptions) (string, error) { + if options == nil || *options.Operation == "" { + return "", errors.New(`the field ModifyOptions.Operation must be set to either "update" or "remove"`) + } + options.WithImages(images) + + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + opts, err := jsoniter.MarshalToString(options) + if err != nil { + return "", err + } + reader := strings.NewReader(opts) + + response, err := conn.DoRequest(ctx, reader, http.MethodPut, "/manifests/%s", nil, nil, name) + if err != nil { + return "", err + } + defer response.Body.Close() + + var idr handlers.IDResponse + return idr.ID, response.Process(&idr) +} + +// Annotate modifies the given manifest list using options and the optional list of images +// +// As of 4.0.0 +func Annotate(ctx context.Context, name string, images []string, options *ModifyOptions) (string, error) { + options.WithOperation("annotate") + return Modify(ctx, name, images, options) +} diff --git a/pkg/bindings/manifests/types.go b/pkg/bindings/manifests/types.go index fde90a865..5ff28ee30 100644 --- a/pkg/bindings/manifests/types.go +++ b/pkg/bindings/manifests/types.go @@ -18,7 +18,7 @@ type ExistsOptions struct { } //go:generate go run ../generator/generator.go AddOptions -// AddOptions are optional options for adding manifests +// AddOptions are optional options for adding manifest lists type AddOptions struct { All *bool Annotation map[string]string @@ -31,6 +31,24 @@ type AddOptions struct { } //go:generate go run ../generator/generator.go RemoveOptions -// RemoveOptions are optional options for removing manifests +// RemoveOptions are optional options for removing manifest lists type RemoveOptions struct { } + +//go:generate go run ../generator/generator.go ModifyOptions +// ModifyOptions are optional options for modifying manifest lists +type ModifyOptions struct { + // Operation values are "update", "remove" and "annotate". This allows the service to + // efficiently perform each update on a manifest list. + Operation *string + All *bool // All when true, operate on all images in a manifest list that may be included in Images + Annotations map[string]string // Annotations to add to manifest list + Arch *string // Arch overrides the architecture for the image + Features []string // Feature list for the image + Images []string // Images is an optional list of images to add/remove to/from manifest list depending on operation + OS *string // OS overrides the operating system for the image + OSFeatures []string // OS features for the image + OSVersion *string // OSVersion overrides the operating system for the image + Variant *string // Variant overrides the operating system variant for the image + +} diff --git a/pkg/bindings/manifests/types_modify_options.go b/pkg/bindings/manifests/types_modify_options.go new file mode 100644 index 000000000..ee5d94dbf --- /dev/null +++ b/pkg/bindings/manifests/types_modify_options.go @@ -0,0 +1,168 @@ +// Code generated by go generate; DO NOT EDIT. +package manifests + +import ( + "net/url" + + "github.com/containers/podman/v3/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *ModifyOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *ModifyOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} + +// WithOperation set field Operation to given value +func (o *ModifyOptions) WithOperation(value string) *ModifyOptions { + o.Operation = &value + return o +} + +// GetOperation returns value of field Operation +func (o *ModifyOptions) GetOperation() string { + if o.Operation == nil { + var z string + return z + } + return *o.Operation +} + +// WithAll set all when true, operate on all images in a manifest list that may be included in Images +func (o *ModifyOptions) WithAll(value bool) *ModifyOptions { + o.All = &value + return o +} + +// GetAll returns value of all when true, operate on all images in a manifest list that may be included in Images +func (o *ModifyOptions) GetAll() bool { + if o.All == nil { + var z bool + return z + } + return *o.All +} + +// WithAnnotations set annotations to add to manifest list +func (o *ModifyOptions) WithAnnotations(value map[string]string) *ModifyOptions { + o.Annotations = value + return o +} + +// GetAnnotations returns value of annotations to add to manifest list +func (o *ModifyOptions) GetAnnotations() map[string]string { + if o.Annotations == nil { + var z map[string]string + return z + } + return o.Annotations +} + +// WithArch set arch overrides the architecture for the image +func (o *ModifyOptions) WithArch(value string) *ModifyOptions { + o.Arch = &value + return o +} + +// GetArch returns value of arch overrides the architecture for the image +func (o *ModifyOptions) GetArch() string { + if o.Arch == nil { + var z string + return z + } + return *o.Arch +} + +// WithFeatures set feature list for the image +func (o *ModifyOptions) WithFeatures(value []string) *ModifyOptions { + o.Features = value + return o +} + +// GetFeatures returns value of feature list for the image +func (o *ModifyOptions) GetFeatures() []string { + if o.Features == nil { + var z []string + return z + } + return o.Features +} + +// WithImages set images is an optional list of images to add/remove to/from manifest list depending on operation +func (o *ModifyOptions) WithImages(value []string) *ModifyOptions { + o.Images = value + return o +} + +// GetImages returns value of images is an optional list of images to add/remove to/from manifest list depending on operation +func (o *ModifyOptions) GetImages() []string { + if o.Images == nil { + var z []string + return z + } + return o.Images +} + +// WithOS set oS overrides the operating system for the image +func (o *ModifyOptions) WithOS(value string) *ModifyOptions { + o.OS = &value + return o +} + +// GetOS returns value of oS overrides the operating system for the image +func (o *ModifyOptions) GetOS() string { + if o.OS == nil { + var z string + return z + } + return *o.OS +} + +// WithOSFeatures set oS features for the image +func (o *ModifyOptions) WithOSFeatures(value []string) *ModifyOptions { + o.OSFeatures = value + return o +} + +// GetOSFeatures returns value of oS features for the image +func (o *ModifyOptions) GetOSFeatures() []string { + if o.OSFeatures == nil { + var z []string + return z + } + return o.OSFeatures +} + +// WithOSVersion set oSVersion overrides the operating system for the image +func (o *ModifyOptions) WithOSVersion(value string) *ModifyOptions { + o.OSVersion = &value + return o +} + +// GetOSVersion returns value of oSVersion overrides the operating system for the image +func (o *ModifyOptions) GetOSVersion() string { + if o.OSVersion == nil { + var z string + return z + } + return *o.OSVersion +} + +// WithVariant set variant overrides the operating system variant for the image +func (o *ModifyOptions) WithVariant(value string) *ModifyOptions { + o.Variant = &value + return o +} + +// GetVariant returns value of variant overrides the operating system variant for the image +func (o *ModifyOptions) GetVariant() string { + if o.Variant == nil { + var z string + return z + } + return *o.Variant +} diff --git a/pkg/bindings/test/manifests_test.go b/pkg/bindings/test/manifests_test.go index e65632057..280006d15 100644 --- a/pkg/bindings/test/manifests_test.go +++ b/pkg/bindings/test/manifests_test.go @@ -12,7 +12,7 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("Podman containers ", func() { +var _ = Describe("podman manifest", func() { var ( bt *bindingTest s *gexec.Session @@ -24,7 +24,8 @@ var _ = Describe("Podman containers ", func() { s = bt.startAPIService() time.Sleep(1 * time.Second) err := bt.NewConnection() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + }) AfterEach(func() { @@ -32,17 +33,19 @@ var _ = Describe("Podman containers ", func() { bt.cleanup() }) - It("create manifest", func() { + It("create", func() { // create manifest list without images - id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) - Expect(err).To(BeNil()) + id, err := manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", []string{}, nil) + Expect(err).ToNot(HaveOccurred(), err) list, err := manifests.Inspect(bt.conn, id, nil) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + Expect(len(list.Manifests)).To(BeZero()) // creating a duplicate should fail as a 500 - _, err = manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) - Expect(err).ToNot(BeNil()) + _, err = manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", nil, nil) + Expect(err).To(HaveOccurred()) + code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -50,96 +53,113 @@ var _ = Describe("Podman containers ", func() { Expect(len(errs)).To(BeZero()) // create manifest list with images - id, err = manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{alpine.name}, nil) - Expect(err).To(BeNil()) + id, err = manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", []string{alpine.name}, nil) + Expect(err).ToNot(HaveOccurred()) + list, err = manifests.Inspect(bt.conn, id, nil) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + Expect(len(list.Manifests)).To(BeNumerically("==", 1)) }) - It("inspect bogus manifest", func() { + It("inspect", func() { _, err := manifests.Inspect(bt.conn, "larry", nil) - Expect(err).ToNot(BeNil()) + Expect(err).To(HaveOccurred()) + code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) }) - It("add manifest", func() { + It("add", func() { // add to bogus should 404 _, err := manifests.Add(bt.conn, "foobar", nil) - Expect(err).ToNot(BeNil()) + Expect(err).To(HaveOccurred()) + code, _ := bindings.CheckResponseCode(err) - Expect(code).To(BeNumerically("==", http.StatusNotFound)) + Expect(code).To(BeNumerically("==", http.StatusNotFound), err.Error()) + + id, err := manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", []string{}, nil) + Expect(err).ToNot(HaveOccurred()) - id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) - Expect(err).To(BeNil()) options := new(manifests.AddOptions).WithImages([]string{alpine.name}) _, err = manifests.Add(bt.conn, id, options) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + list, err := manifests.Inspect(bt.conn, id, nil) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + Expect(len(list.Manifests)).To(BeNumerically("==", 1)) // add bogus name to existing list should fail options.WithImages([]string{"larry"}) _, err = manifests.Add(bt.conn, id, options) - Expect(err).ToNot(BeNil()) + Expect(err).To(HaveOccurred()) + code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) }) - It("remove manifest", func() { + It("remove digest", func() { // removal on bogus manifest list should be 404 _, err := manifests.Remove(bt.conn, "larry", "1234", nil) - Expect(err).ToNot(BeNil()) + Expect(err).To(HaveOccurred()) + code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) - id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{alpine.name}, nil) - Expect(err).To(BeNil()) + id, err := manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", []string{alpine.name}, nil) + Expect(err).ToNot(HaveOccurred()) + data, err := manifests.Inspect(bt.conn, id, nil) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + Expect(len(data.Manifests)).To(BeNumerically("==", 1)) // removal on a good manifest list with a bad digest should be 400 _, err = manifests.Remove(bt.conn, id, "!234", nil) - Expect(err).ToNot(BeNil()) + Expect(err).To(HaveOccurred()) + code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusBadRequest)) digest := data.Manifests[0].Digest.String() _, err = manifests.Remove(bt.conn, id, digest, nil) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) // removal on good manifest with good digest should work data, err = manifests.Inspect(bt.conn, id, nil) - Expect(err).To(BeNil()) - Expect(len(data.Manifests)).To(BeZero()) + Expect(err).ToNot(HaveOccurred()) + + Expect(data.Manifests).Should(BeEmpty()) }) - // There is NO annotate endpoint, this could never work.:w - - //It("annotate manifest", func() { - // id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) - // Expect(err).To(BeNil()) - // opts := image.ManifestAddOpts{Images: []string{"docker.io/library/alpine:latest"}} - // - // _, err = manifests.Add(bt.conn, id, opts) - // Expect(err).To(BeNil()) - // data, err := manifests.Inspect(bt.conn, id) - // Expect(err).To(BeNil()) - // Expect(len(data.Manifests)).To(BeNumerically("==", 1)) - // digest := data.Manifests[0].Digest.String() - // annoOpts := image.ManifestAnnotateOpts{OS: "foo"} - // _, err = manifests.Annotate(bt.conn, id, digest, annoOpts) - // Expect(err).To(BeNil()) - // list, err := manifests.Inspect(bt.conn, id) - // Expect(err).To(BeNil()) - // Expect(len(list.Manifests)).To(BeNumerically("==", 1)) - // Expect(list.Manifests[0].Platform.OS).To(Equal("foo")) - //}) + It("annotate", func() { + id, err := manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", []string{}, nil) + Expect(err).ToNot(HaveOccurred()) + + opts := manifests.AddOptions{Images: []string{"quay.io/libpod/alpine:latest"}} + + _, err = manifests.Add(bt.conn, id, &opts) + Expect(err).ToNot(HaveOccurred()) + + data, err := manifests.Inspect(bt.conn, id, nil) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(data.Manifests)).To(BeNumerically("==", 1)) + + digest := data.Manifests[0].Digest.String() + annoOpts := new(manifests.ModifyOptions).WithOS("foo") + _, err = manifests.Annotate(bt.conn, id, []string{digest}, annoOpts) + Expect(err).ToNot(HaveOccurred()) + + list, err := manifests.Inspect(bt.conn, id, nil) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(list.Manifests)).To(BeNumerically("==", 1)) + Expect(list.Manifests[0].Platform.OS).To(Equal("foo")) + }) It("push manifest", func() { - Skip("TODO") + Skip("TODO: implement test for manifest push to registry") }) }) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index bec505163..bf9fcfd27 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -31,12 +31,12 @@ type ImageEngine interface { Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error - ManifestCreate(ctx context.Context, names, images []string, opts ManifestCreateOptions) (string, error) + ManifestCreate(ctx context.Context, name string, images []string, opts ManifestCreateOptions) (string, error) ManifestExists(ctx context.Context, name string) (*BoolReport, error) ManifestInspect(ctx context.Context, name string) ([]byte, error) - 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) + ManifestAdd(ctx context.Context, listName string, imageNames []string, opts ManifestAddOptions) (string, error) + ManifestAnnotate(ctx context.Context, names, image string, opts ManifestAnnotateOptions) (string, error) + ManifestRemoveDigest(ctx context.Context, names, image string) (string, error) ManifestRm(ctx context.Context, names []string) (*ImageRemoveReport, []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 62e7f67c8..bc41d7844 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -94,7 +94,7 @@ type ImageRemoveOptions struct { LookupManifest bool } -// ImageRemoveResponse is the response for removing one or more image(s) from storage +// ImageRemoveReport is the response for removing one or more image(s) from storage // and images what was untagged vs actually removed. type ImageRemoveReport struct { // Deleted images. diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index 3f89e4d30..81f3e837b 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -2,35 +2,79 @@ package entities import "github.com/containers/image/v5/types" -// TODO: add comments to *all* types and fields. - +// ManifestCreateOptions provides model for creating manifest type ManifestCreateOptions struct { All bool `schema:"all"` } -// swagger:model ManifestAddOpts +// ManifestAddOptions provides model for adding digests to manifest list +// +// swagger:model type ManifestAddOptions struct { - All bool `json:"all" schema:"all"` - Annotation []string `json:"annotation" schema:"annotation"` - Arch string `json:"arch" schema:"arch"` - Authfile string `json:"-" schema:"-"` - CertDir string `json:"-" schema:"-"` - Features []string `json:"features" schema:"features"` - Images []string `json:"images" schema:"images"` - OS string `json:"os" schema:"os"` - OSVersion string `json:"os_version" schema:"os_version"` - Password string `json:"-" schema:"-"` + ManifestAnnotateOptions + // True when operating on a list to include all images + All bool `json:"all" schema:"all"` + // authfile to use when pushing manifest list + Authfile string `json:"-" schema:"-"` + // Home directory for certificates when pushing a manifest list + CertDir string `json:"-" schema:"-"` + // Password to authenticate to registry when pushing manifest list + Password string `json:"-" schema:"-"` + // Should TLS registry certificate be verified? SkipTLSVerify types.OptionalBool `json:"-" schema:"-"` - Username string `json:"-" schema:"-"` - Variant string `json:"variant" schema:"variant"` + // Username to authenticate to registry when pushing manifest list + Username string `json:"-" schema:"-"` + // Images is an optional list of images to add to manifest list + Images []string `json:"images" schema:"images"` } +// ManifestAnnotateOptions provides model for annotating manifest list type ManifestAnnotateOptions struct { - Annotation []string `json:"annotation"` - Arch string `json:"arch" schema:"arch"` - Features []string `json:"features" schema:"features"` - OS string `json:"os" schema:"os"` + // Annotation to add to manifest list + Annotation []string `json:"annotation" schema:"annotation"` + // Arch overrides the architecture for the image + Arch string `json:"arch" schema:"arch"` + // Feature list for the image + Features []string `json:"features" schema:"features"` + // OS overrides the operating system for the image + OS string `json:"os" schema:"os"` + // OS features for the image OSFeatures []string `json:"os_features" schema:"os_features"` - OSVersion string `json:"os_version" schema:"os_version"` - Variant string `json:"variant" schema:"variant"` + // OSVersion overrides the operating system for the image + OSVersion string `json:"os_version" schema:"os_version"` + // Variant for the image + Variant string `json:"variant" schema:"variant"` +} + +// ManifestModifyOptions provides the model for mutating a manifest +// +// swagger 2.0 does not support oneOf for schema validation. +// +// Operation "update" uses all fields. +// Operation "remove" uses fields: Operation and Images +// Operation "annotate" uses fields: Operation and Annotations +// +// swagger:model +type ManifestModifyOptions struct { + Operation string `json:"operation" schema:"operation"` // Valid values: update, remove, annotate + ManifestAddOptions + ManifestRemoveOptions +} + +// ManifestRemoveOptions provides the model for removing digests from a manifest +// +// swagger:model +type ManifestRemoveOptions struct { +} + +// ManifestModifyReport provides the model for removed digests and changed manifest +// +// swagger:model +type ManifestModifyReport struct { + // Manifest List ID + ID string `json:"Id"` + // Images to removed from manifest list, otherwise not provided. + Images []string `json:"images,omitempty" schema:"images"` + // Errors associated with operation + Errors []error `json:"errors,omitempty"` } diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index d1bd5e2e4..d8733130b 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -22,13 +22,10 @@ import ( ) // ManifestCreate implements logic for creating manifest lists via ImageEngine -func (ir *ImageEngine) ManifestCreate(ctx context.Context, names []string, images []string, opts entities.ManifestCreateOptions) (string, error) { - // FIXME: change the interface of manifest create `names []string` -> - // `name string`. - if len(names) == 0 { +func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images []string, opts entities.ManifestCreateOptions) (string, error) { + if len(name) == 0 { return "", errors.New("no name specified for creating a manifest list") } - name := names[0] manifestList, err := ir.Libpod.LibimageRuntime().CreateManifestList(name) if err != nil { @@ -175,18 +172,12 @@ func (ir *ImageEngine) remoteManifestInspect(ctx context.Context, name string) ( } // ManifestAdd adds images to the manifest list -func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAddOptions) (string, error) { - // FIXME: the name options below are *mandatory* arguments and should - // be reflected as such in the signature. - - if len(opts.Images) < 2 { - return "", errors.New("manifest add requires two images") +func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []string, opts entities.ManifestAddOptions) (string, error) { + if len(images) < 1 { + return "", errors.New("manifest add requires at least one image") } - imageName := opts.Images[0] - listName := opts.Images[1] - - manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(listName) + manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(name) if err != nil { return "", err } @@ -200,53 +191,46 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAd Password: opts.Password, } - instanceDigest, err := manifestList.Add(ctx, imageName, addOptions) - if err != nil { - return "", err - } + for _, image := range images { + instanceDigest, err := manifestList.Add(ctx, image, addOptions) + if err != nil { + return "", err + } - annotateOptions := &libimage.ManifestListAnnotateOptions{ - Architecture: opts.Arch, - Features: opts.Features, - OS: opts.OS, - OSVersion: opts.OSVersion, - Variant: opts.Variant, - } - if len(opts.Annotation) != 0 { - annotations := make(map[string]string) - for _, annotationSpec := range opts.Annotation { - spec := strings.SplitN(annotationSpec, "=", 2) - if len(spec) != 2 { - return "", errors.Errorf("no value given for annotation %q", spec[0]) + annotateOptions := &libimage.ManifestListAnnotateOptions{ + Architecture: opts.Arch, + Features: opts.Features, + OS: opts.OS, + OSVersion: opts.OSVersion, + Variant: opts.Variant, + } + if len(opts.Annotation) != 0 { + annotations := make(map[string]string) + for _, annotationSpec := range opts.Annotation { + spec := strings.SplitN(annotationSpec, "=", 2) + if len(spec) != 2 { + return "", errors.Errorf("no value given for annotation %q", spec[0]) + } + annotations[spec[0]] = spec[1] } - annotations[spec[0]] = spec[1] + annotateOptions.Annotations = annotations } - annotateOptions.Annotations = annotations - } - if err := manifestList.AnnotateInstance(instanceDigest, annotateOptions); err != nil { - return "", err + if err := manifestList.AnnotateInstance(instanceDigest, annotateOptions); err != nil { + return "", err + } } - return manifestList.ID(), nil } // ManifestAnnotate updates an entry of the manifest list -func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, names []string, opts entities.ManifestAnnotateOptions) (string, error) { - // FIXME: the `names` are *mandatory* arguments and should be - // reflected as such in the signature. - - if len(names) < 2 { - return "", errors.New("manifest annotate requires two names") - } - - listName := names[0] - instanceDigest, err := digest.Parse(names[1]) +func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, image string, opts entities.ManifestAnnotateOptions) (string, error) { + instanceDigest, err := digest.Parse(image) if err != nil { - return "", errors.Errorf(`invalid image digest "%s": %v`, names[1], err) + return "", errors.Errorf(`invalid image digest "%s": %v`, image, err) } - manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(listName) + manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(name) if err != nil { return "", err } @@ -277,22 +261,14 @@ func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, names []string, opt return manifestList.ID(), nil } -// ManifestRemove removes specified digest from the specified manifest list -func (ir *ImageEngine) ManifestRemove(ctx context.Context, names []string) (string, error) { - // FIXME: the `names` are *mandatory* arguments and should be - // reflected as such in the signature. - - if len(names) < 2 { - return "", errors.New("manifest remove requires two names") - } - - listName := names[0] - instanceDigest, err := digest.Parse(names[1]) +// ManifestRemoveDigest removes specified digest from the specified manifest list +func (ir *ImageEngine) ManifestRemoveDigest(ctx context.Context, name, image string) (string, error) { + instanceDigest, err := digest.Parse(image) if err != nil { - return "", errors.Errorf(`invalid image digest "%s": %v`, names[1], err) + return "", errors.Errorf(`invalid image digest "%s": %v`, image, err) } - manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(listName) + manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(name) if err != nil { return "", err } diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go index 177e9cff4..ec7de2743 100644 --- a/pkg/domain/infra/runtime_abi.go +++ b/pkg/domain/infra/runtime_abi.go @@ -26,7 +26,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine, return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) } -// NewContainerEngine factory provides a libpod runtime for image-related operations +// NewImageEngine factory provides a libpod runtime for image-related operations func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error) { switch facts.EngineMode { case entities.ABIMode: diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go index 62634f561..f89c59bc7 100644 --- a/pkg/domain/infra/tunnel/manifest.go +++ b/pkg/domain/infra/tunnel/manifest.go @@ -7,16 +7,16 @@ import ( "strings" "github.com/containers/image/v5/types" - images "github.com/containers/podman/v3/pkg/bindings/images" + "github.com/containers/podman/v3/pkg/bindings/images" "github.com/containers/podman/v3/pkg/bindings/manifests" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/pkg/errors" ) // ManifestCreate implements manifest create via ImageEngine -func (ir *ImageEngine) ManifestCreate(ctx context.Context, names, images []string, opts entities.ManifestCreateOptions) (string, error) { +func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images []string, opts entities.ManifestCreateOptions) (string, error) { options := new(manifests.CreateOptions).WithAll(opts.All) - imageID, err := manifests.Create(ir.ClientCtx, names, images, options) + imageID, err := manifests.Create(ir.ClientCtx, name, images, options) if err != nil { return imageID, errors.Wrapf(err, "error creating manifest") } @@ -33,7 +33,7 @@ func (ir *ImageEngine) ManifestExists(ctx context.Context, name string) (*entiti } // ManifestInspect returns contents of manifest list with given name -func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte, error) { +func (ir *ImageEngine) ManifestInspect(_ context.Context, name string) ([]byte, error) { list, err := manifests.Inspect(ir.ClientCtx, name, nil) if err != nil { return nil, errors.Wrapf(err, "error getting content of manifest list or image %s", name) @@ -47,9 +47,9 @@ func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte } // ManifestAdd adds images to the manifest list -func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAddOptions) (string, error) { +func (ir *ImageEngine) ManifestAdd(_ context.Context, name string, imageNames []string, opts entities.ManifestAddOptions) (string, error) { options := new(manifests.AddOptions).WithAll(opts.All).WithArch(opts.Arch).WithVariant(opts.Variant) - options.WithFeatures(opts.Features).WithImages(opts.Images).WithOS(opts.OS).WithOSVersion(opts.OSVersion) + options.WithFeatures(opts.Features).WithImages(imageNames).WithOS(opts.OS).WithOSVersion(opts.OSVersion) if len(opts.Annotation) != 0 { annotations := make(map[string]string) for _, annotationSpec := range opts.Annotation { @@ -62,25 +62,25 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAd options.WithAnnotation(annotations) } - listID, err := manifests.Add(ir.ClientCtx, opts.Images[1], options) + id, err := manifests.Add(ir.ClientCtx, name, options) if err != nil { - return listID, errors.Wrapf(err, "error adding to manifest list %s", opts.Images[1]) + return id, errors.Wrapf(err, "error adding to manifest list %s", name) } - return listID, nil + return id, nil } // ManifestAnnotate updates an entry of the manifest list -func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, names []string, opts entities.ManifestAnnotateOptions) (string, error) { +func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, images string, opts entities.ManifestAnnotateOptions) (string, error) { return "", errors.New("not implemented") } -// ManifestRemove removes the digest from manifest list -func (ir *ImageEngine) ManifestRemove(ctx context.Context, names []string) (string, error) { - updatedListID, err := manifests.Remove(ir.ClientCtx, names[0], names[1], nil) +// ManifestRemoveDigest removes the digest from manifest list +func (ir *ImageEngine) ManifestRemoveDigest(ctx context.Context, name string, image string) (string, error) { + updatedListID, err := manifests.Remove(ir.ClientCtx, name, image, nil) if err != nil { - return updatedListID, errors.Wrapf(err, "error removing from manifest %s", names[0]) + return updatedListID, errors.Wrapf(err, "error removing from manifest %s", name) } - return fmt.Sprintf("%s :%s\n", updatedListID, names[1]), nil + return fmt.Sprintf("%s :%s\n", updatedListID, image), nil } // ManifestRm removes the specified manifest list from storage diff --git a/test/apiv2/15-manifest.at b/test/apiv2/15-manifest.at new file mode 100644 index 000000000..0dd7026fa --- /dev/null +++ b/test/apiv2/15-manifest.at @@ -0,0 +1,19 @@ +# -*- sh -*- +# +# Tests for manifest list endpoints + +t POST /v3.4.0/libpod/manifests/create?name=abc 200 \ + .Id~[0-9a-f]\\{64\\} +id_abc=$(jq -r '.Id' <<<"$output") + +t POST /v4.0.0/libpod/manifests/xyz 201 \ + .Id~[0-9a-f]\\{64\\} +echo xyz $output +id_xyz=$(jq -r '.Id' <<<"$output") + +t GET /v3.4.0/libpod/manifests/$id_abc/exists 204 +t GET /v4.0.0/libpod/manifests/$id_xyz/exists 204 + +# /v3.x cannot delete a manifest list +t DELETE /v4.0.0/libpod/manifests/$id_abc 200 +t DELETE /v4.0.0/libpod/manifests/$id_xyz 200 diff --git a/test/apiv2/test-apiv2 b/test/apiv2/test-apiv2 index 391095539..19e8c12d0 100755 --- a/test/apiv2/test-apiv2 +++ b/test/apiv2/test-apiv2 @@ -364,7 +364,9 @@ function start_service() { echo $WORKDIR # Some tests use shortnames; force registry override to work around # docker.io throttling. - env CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf $PODMAN_BIN \ +# FIXME esm revisit pulling expected images re: shortnames caused tests to fail +# env CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf + $PODMAN_BIN \ --root $WORKDIR/server_root --syslog=true \ system service \ --time 15 \ @@ -497,7 +499,7 @@ function wait_for_port() { ############ function podman() { echo "\$ $PODMAN_BIN $*" >>$WORKDIR/output.log - env CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf \ +# env CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf \ $PODMAN_BIN --root $WORKDIR/server_root "$@" >>$WORKDIR/output.log 2>&1 } diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index 4644e3748..53e1fc8f2 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -80,7 +80,7 @@ func (p *PodmanTestIntegration) StartRemoteService() { args := []string{} if _, found := os.LookupEnv("DEBUG_SERVICE"); found { - args = append(args, "--log-level", "debug") + args = append(args, "--log-level", "trace") } remoteSocket := p.RemoteSocket args = append(args, "system", "service", "--time", "0", remoteSocket) @@ -151,7 +151,7 @@ func (p *PodmanTestIntegration) StopRemoteService() { } } -//MakeOptions assembles all the podman main options +// MakeOptions assembles all the podman main options func getRemoteOptions(p *PodmanTestIntegration, args []string) []string { podmanOptions := strings.Split(fmt.Sprintf("--root %s --runroot %s --runtime %s --conmon %s --cni-config-dir %s --cgroup-manager %s", p.Root, p.RunRoot, p.OCIRuntime, p.ConmonBinary, p.CNIConfigDir, p.CgroupManager), " ") diff --git a/test/e2e/manifest_test.go b/test/e2e/manifest_test.go index 5978214ff..b86d5ecbe 100644 --- a/test/e2e/manifest_test.go +++ b/test/e2e/manifest_test.go @@ -44,19 +44,19 @@ var _ = Describe("Podman manifest", func() { processTestResult(f) }) - It("podman manifest create", func() { + It("create w/o image", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) }) - It("podman manifest create", func() { + It("create w/ image", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo", imageList}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) }) - It("podman manifest inspect", func() { + It("inspect", func() { session := podmanTest.Podman([]string{"manifest", "inspect", BB}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -71,29 +71,27 @@ var _ = Describe("Podman manifest", func() { Expect(session).Should(Exit(0)) }) - It("podman manifest add", func() { + It("add w/ inspect", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - session = podmanTest.Podman([]string{"manifest", "add", "--arch=arm64", "foo", imageListInstance}) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - }) + id := strings.TrimSpace(string(session.Out.Contents())) - It("podman manifest add one", func() { - session := podmanTest.Podman([]string{"manifest", "create", "foo"}) + session = podmanTest.Podman([]string{"manifest", "inspect", id}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) + session = podmanTest.Podman([]string{"manifest", "add", "--arch=arm64", "foo", imageListInstance}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) + session = podmanTest.Podman([]string{"manifest", "inspect", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) Expect(session.OutputToString()).To(ContainSubstring(imageListARM64InstanceDigest)) }) - It("podman manifest tag", func() { + It("tag", func() { session := podmanTest.Podman([]string{"manifest", "create", "foobar"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -112,7 +110,7 @@ var _ = Describe("Podman manifest", func() { Expect(session2.OutputToString()).To(Equal(session.OutputToString())) }) - It("podman manifest add --all", func() { + It(" add --all", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -122,14 +120,17 @@ var _ = Describe("Podman manifest", func() { session = podmanTest.Podman([]string{"manifest", "inspect", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - Expect(session.OutputToString()).To(ContainSubstring(imageListAMD64InstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListARMInstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListARM64InstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListPPC64LEInstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListS390XInstanceDigest)) + Expect(session.OutputToString()).To( + And( + ContainSubstring(imageListAMD64InstanceDigest), + ContainSubstring(imageListARMInstanceDigest), + ContainSubstring(imageListARM64InstanceDigest), + ContainSubstring(imageListPPC64LEInstanceDigest), + ContainSubstring(imageListS390XInstanceDigest), + )) }) - It("podman manifest add --os", func() { + It("add --os", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -142,7 +143,7 @@ var _ = Describe("Podman manifest", func() { Expect(session.OutputToString()).To(ContainSubstring(`"os": "bar"`)) }) - It("podman manifest annotate", func() { + It("annotate", func() { SkipIfRemote("Not supporting annotate on remote connections") session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() @@ -159,7 +160,7 @@ var _ = Describe("Podman manifest", func() { Expect(session.OutputToString()).To(ContainSubstring(`"architecture": "bar"`)) }) - It("podman manifest remove", func() { + It("remove digest", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -176,14 +177,18 @@ var _ = Describe("Podman manifest", func() { session = podmanTest.Podman([]string{"manifest", "inspect", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - Expect(session.OutputToString()).To(ContainSubstring(imageListAMD64InstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListARMInstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListPPC64LEInstanceDigest)) - Expect(session.OutputToString()).To(ContainSubstring(imageListS390XInstanceDigest)) - Expect(session.OutputToString()).To(Not(ContainSubstring(imageListARM64InstanceDigest))) + Expect(session.OutputToString()).To( + And( + ContainSubstring(imageListAMD64InstanceDigest), + ContainSubstring(imageListARMInstanceDigest), + ContainSubstring(imageListPPC64LEInstanceDigest), + ContainSubstring(imageListS390XInstanceDigest), + Not( + ContainSubstring(imageListARM64InstanceDigest)), + )) }) - It("podman manifest remove not-found", func() { + It("remove not-found", func() { session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -199,7 +204,7 @@ var _ = Describe("Podman manifest", func() { Expect(session).Should(Exit(0)) }) - It("podman manifest push", func() { + It("push --all", func() { SkipIfRemote("manifest push to dir not supported in remote mode") session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() @@ -216,20 +221,24 @@ var _ = Describe("Podman manifest", func() { session = podmanTest.Podman([]string{"manifest", "push", "--all", "foo", "dir:" + dest}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) + files, err := filepath.Glob(dest + string(os.PathSeparator) + "*") - Expect(err).To(BeNil()) + Expect(err).ShouldNot(HaveOccurred()) check := SystemExec("sha256sum", files) check.WaitWithDefaultTimeout() Expect(check).Should(Exit(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))) + Expect(check.OutputToString()).To( + And( + ContainSubstring(strings.TrimPrefix(imageListAMD64InstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListARMInstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListPPC64LEInstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListS390XInstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListARM64InstanceDigest, prefix)), + )) }) - It("podman push --all", func() { + It("push", func() { SkipIfRemote("manifest push to dir not supported in remote mode") session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() @@ -251,15 +260,19 @@ var _ = Describe("Podman manifest", func() { check := SystemExec("sha256sum", files) check.WaitWithDefaultTimeout() Expect(check).Should(Exit(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))) + Expect(check.OutputToString()).To( + And( + ContainSubstring(strings.TrimPrefix(imageListAMD64InstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListARMInstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListPPC64LEInstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListS390XInstanceDigest, prefix)), + ContainSubstring(strings.TrimPrefix(imageListARM64InstanceDigest, prefix)), + )) }) - It("podman manifest push --rm", func() { + It("push --rm", func() { SkipIfRemote("remote does not support --rm") session := podmanTest.Podman([]string{"manifest", "create", "foo"}) session.WaitWithDefaultTimeout() @@ -285,7 +298,7 @@ var _ = Describe("Podman manifest", func() { Expect(session).To(ExitWithError()) }) - It("podman manifest exists", func() { + It("exists", func() { manifestList := "manifest-list" session := podmanTest.Podman([]string{"manifest", "create", manifestList}) session.WaitWithDefaultTimeout() @@ -300,7 +313,7 @@ var _ = Describe("Podman manifest", func() { Expect(session).Should(Exit(1)) }) - It("podman manifest rm should not remove referenced images", func() { + It("rm should not remove referenced images", func() { manifestList := "manifestlist" imageName := "quay.io/libpod/busybox" @@ -320,11 +333,9 @@ var _ = Describe("Podman manifest", func() { session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - //image should still show up - session = podmanTest.Podman([]string{"images"}) + // image should still show up + session = podmanTest.Podman([]string{"image", "exists", imageName}) session.WaitWithDefaultTimeout() - Expect(session.OutputToString()).To(ContainSubstring(imageName)) Expect(session).Should(Exit(0)) }) - }) |