From 8a7e70919f4bab0757523ae97c170396cb13c83d Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Mon, 6 Dec 2021 16:45:58 -0700 Subject: 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 --- pkg/api/handlers/libpod/manifests.go | 265 ++++++++++++++++++++++++++++++++--- pkg/api/handlers/utils/handler.go | 4 +- 2 files changed, 244 insertions(+), 25 deletions(-) (limited to 'pkg/api/handlers') 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") } -- cgit v1.2.3-54-g00ecf