summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorJhon Honce <jhonce@redhat.com>2021-12-06 16:45:58 -0700
committerJhon Honce <jhonce@redhat.com>2022-01-14 16:13:35 -0700
commit8a7e70919f4bab0757523ae97c170396cb13c83d (patch)
tree0ec2b5aa4e3c1e6574e606a0e7db3638fdeda578 /pkg
parentec2b213ab611cb197e86c45d03fb10af667ad95c (diff)
downloadpodman-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>
Diffstat (limited to 'pkg')
-rw-r--r--pkg/api/handlers/libpod/manifests.go265
-rw-r--r--pkg/api/handlers/utils/handler.go4
-rw-r--r--pkg/api/server/register_manifest.go177
-rw-r--r--pkg/api/server/register_swagger.go4
-rw-r--r--pkg/api/server/server.go2
-rw-r--r--pkg/auth/auth.go10
-rw-r--r--pkg/auth/auth_test.go12
-rw-r--r--pkg/bindings/connection.go85
-rw-r--r--pkg/bindings/containers/attach.go8
-rw-r--r--pkg/bindings/errors.go2
-rw-r--r--pkg/bindings/images/build.go6
-rw-r--r--pkg/bindings/internal/util/util.go8
-rw-r--r--pkg/bindings/manifests/manifests.go144
-rw-r--r--pkg/bindings/manifests/types.go22
-rw-r--r--pkg/bindings/manifests/types_modify_options.go168
-rw-r--r--pkg/bindings/test/manifests_test.go124
-rw-r--r--pkg/domain/entities/engine_image.go8
-rw-r--r--pkg/domain/entities/images.go2
-rw-r--r--pkg/domain/entities/manifest.go86
-rw-r--r--pkg/domain/infra/abi/manifest.go102
-rw-r--r--pkg/domain/infra/runtime_abi.go2
-rw-r--r--pkg/domain/infra/tunnel/manifest.go30
22 files changed, 956 insertions, 315 deletions
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