From 247f6dcd3f62663ae71d1f99e339aae34f8ff0dd Mon Sep 17 00:00:00 2001 From: Alexei Dodon Date: Wed, 23 Aug 2023 20:59:52 +0300 Subject: [PATCH] feat: propagate detailed error msgs to client (OCI dist-spec format) (#1681) Signed-off-by: Alexei Dodon --- .github/workflows/oci-conformance-action.yml | 2 - errors/errors.go | 47 ++- pkg/api/authn.go | 11 +- pkg/api/errors/errors.go | 92 +++--- pkg/api/errors/errors_test.go | 2 +- pkg/api/routes.go | 305 +++++++++++-------- pkg/extensions/extensions_lint_disabled.go | 2 +- pkg/extensions/lint/lint.go | 2 +- pkg/storage/common/common.go | 16 +- pkg/storage/common/common_test.go | 13 +- 10 files changed, 292 insertions(+), 200 deletions(-) diff --git a/.github/workflows/oci-conformance-action.yml b/.github/workflows/oci-conformance-action.yml index 371e35fc..ec2dc4ae 100644 --- a/.github/workflows/oci-conformance-action.yml +++ b/.github/workflows/oci-conformance-action.yml @@ -36,8 +36,6 @@ jobs: echo "SERVER_URL=http://${IP}:8080" >> $GITHUB_ENV - uses: actions/checkout@v3 with: - # TODO: change to upstream once the foloowing PR is merged: - # https://github.com/opencontainers/distribution-spec/pull/436 repository: opencontainers/distribution-spec ref: main path: distribution-spec diff --git a/errors/errors.go b/errors/errors.go index 3e4c65e9..b3cf4bfd 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -1,16 +1,57 @@ package errors -import "errors" +import ( + "errors" +) + +type Error struct { + err error + details map[string]string +} + +func (e *Error) Error() string { + return e.err.Error() +} + +func (e *Error) Is(target error) bool { + return errors.Is(e.err, target) +} + +func (e *Error) AddDetail(key, value string) *Error { + e.details[key] = value + + return e +} + +func (e *Error) GetDetails() map[string]string { + return e.details +} + +func NewError(err error) *Error { + return &Error{ + err: err, + details: GetDetails(err), // preserve details if chained error + } +} + +func GetDetails(err error) map[string]string { + var internalErr *Error + details := make(map[string]string) + + if errors.As(err, &internalErr) { + details = internalErr.GetDetails() + } + + return details +} var ( ErrBadConfig = errors.New("config: invalid config") ErrCliBadConfig = errors.New("cli: bad config") ErrRepoNotFound = errors.New("repository: not found") - ErrRepoIsNotDir = errors.New("repository: not a directory") ErrRepoBadVersion = errors.New("repository: unsupported layout version") ErrManifestNotFound = errors.New("manifest: not found") ErrBadManifest = errors.New("manifest: invalid contents") - ErrBadIndex = errors.New("index: invalid contents") ErrUploadNotFound = errors.New("uploads: not found") ErrBadUploadRange = errors.New("uploads: bad range") ErrBlobNotFound = errors.New("blob: not found") diff --git a/pkg/api/authn.go b/pkg/api/authn.go index 33d8fac4..8e7bb6b9 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -36,7 +36,7 @@ import ( "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" apiErr "zotregistry.io/zot/pkg/api/errors" - "zotregistry.io/zot/pkg/common" + zcommon "zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/log" localCtx "zotregistry.io/zot/pkg/requestcontext" storageConstants "zotregistry.io/zot/pkg/storage/constants" @@ -463,7 +463,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc { if err != nil { ctlr.Log.Error().Err(err).Msg("issue parsing Authorization header") response.Header().Set("Content-Type", "application/json") - common.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED))) + zcommon.WriteJSON(response, http.StatusInternalServerError, apiErr.NewError(apiErr.UNSUPPORTED)) return } @@ -472,8 +472,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc { response.Header().Set("Content-Type", "application/json") response.Header().Set("WWW-Authenticate", permissions.WWWAuthenticateHeader) - common.WriteJSON(response, http.StatusUnauthorized, - apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED))) + zcommon.WriteJSON(response, http.StatusUnauthorized, apiErr.NewError(apiErr.UNAUTHORIZED)) return } @@ -591,7 +590,7 @@ func getRelyingPartyArgs(cfg *config.Config, provider string) ( scopes := cfg.HTTP.Auth.OpenID.Providers[provider].Scopes // openid scope must be the first one in list - if !common.Contains(scopes, oidc.ScopeOpenID) && config.IsOpenIDSupported(provider) { + if !zcommon.Contains(scopes, oidc.ScopeOpenID) && config.IsOpenIDSupported(provider) { scopes = append([]string{oidc.ScopeOpenID}, scopes...) } @@ -663,7 +662,7 @@ func authFail(w http.ResponseWriter, r *http.Request, realm string, delay int) { } w.Header().Set("Content-Type", "application/json") - common.WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED))) + zcommon.WriteJSON(w, http.StatusUnauthorized, apiErr.NewError(apiErr.UNAUTHORIZED)) } func isAuthorizationHeaderEmpty(request *http.Request) bool { diff --git a/pkg/api/errors/errors.go b/pkg/api/errors/errors.go index d218e71d..cfc4adb7 100644 --- a/pkg/api/errors/errors.go +++ b/pkg/api/errors/errors.go @@ -5,16 +5,10 @@ import ( ) type Error struct { - Code string `json:"code"` - Message string `json:"message"` - Description string `json:"description"` - Detail interface{} `json:"detail,omitempty"` -} - -func (e Error) WithMessage(msg string) Error { - e.Message = msg - - return e + Code string `json:"code"` + Message string `json:"message"` + Description string `json:"-"` + Detail map[string]string `json:"detail"` } type ErrorList struct { @@ -66,7 +60,7 @@ func (e ErrorCode) String() string { return errMap[e] } -func NewError(code ErrorCode, detail ...interface{}) Error { +func NewError(code ErrorCode) *Error { errMap := map[ErrorCode]Error{ BLOB_UNKNOWN: { Message: "blob unknown to registry", @@ -77,12 +71,12 @@ func NewError(code ErrorCode, detail ...interface{}) Error { BLOB_UPLOAD_INVALID: { Message: "blob upload invalid", - Description: `The blob upload encountered an error and can no longer proceed.`, + Description: "The blob upload encountered an error and can no longer proceed.", }, BLOB_UPLOAD_UNKNOWN: { Message: "blob upload unknown to registry", - Description: `If a blob upload has been cancelled or was never started, this error code MAY be returned.`, + Description: "If a blob upload has been cancelled or was never started, this error code MAY be returned.", }, DIGEST_INVALID: { @@ -94,71 +88,68 @@ func NewError(code ErrorCode, detail ...interface{}) Error { }, MANIFEST_BLOB_UNKNOWN: { - Message: "blob unknown to registry", - Description: `This error MAY be returned when a manifest blob is unknown - to the registry.`, + Message: "blob unknown to registry", + Description: "This error MAY be returned when a manifest blob is unknown to the registry.", }, MANIFEST_INVALID: { Message: "manifest invalid", - Description: `During upload, manifests undergo several checks ensuring - validity. If those checks fail, this error MAY be returned, unless a more - specific error is included. The detail will contain information the failed - validation.`, + Description: "During upload, manifests undergo several checks ensuring " + + "validity. If those checks fail, this error MAY be returned, unless a more " + + "specific error is included. The detail will contain information the failed validation.", }, MANIFEST_UNKNOWN: { Message: "manifest unknown", - Description: `This error is returned when the manifest, identified by name - and tag is unknown to the repository.`, + Description: "This error is returned when the manifest, identified by name " + + "and tag is unknown to the repository.", }, MANIFEST_UNVERIFIED: { Message: "manifest failed signature verification", - Description: `During manifest upload, if the manifest fails signature - verification, this error will be returned.`, + Description: "During manifest upload, if the manifest fails signature " + + "verification, this error will be returned.", }, NAME_INVALID: { Message: "invalid repository name", - Description: `Invalid repository name encountered either during manifest - validation or any API operation.`, + Description: "Invalid repository name encountered either during manifest " + + "validation or any API operation.", }, NAME_UNKNOWN: { Message: "repository name not known to registry", - Description: `This is returned if the name used during an operation is unknown to the registry.`, + Description: "This is returned if the name used during an operation is unknown to the registry.", }, SIZE_INVALID: { Message: "provided length did not match content length", - Description: "When a layer is uploaded, the provided size will be checked against the uploaded " + - "content. If they do not match, this error will be returned.", + Description: "When a layer is uploaded, the provided size will be checked against " + + "the uploaded content. If they do not match, this error will be returned.", }, TAG_INVALID: { Message: "manifest tag did not match URI", - Description: `During a manifest upload, if the tag in the manifest does - not match the uri tag, this error will be returned.`, + Description: "During a manifest upload, if the tag in the manifest does " + + "not match the uri tag, this error will be returned.", }, UNAUTHORIZED: { Message: "authentication required", - Description: `The access controller was unable to authenticate the client. - Often this will be accompanied by a Www-Authenticate HTTP response header - indicating how to authenticate.`, + Description: "The access controller was unable to authenticate the client." + + "Often this will be accompanied by a Www-Authenticate HTTP response header " + + "indicating how to authenticate.", }, DENIED: { - Message: "requested access to the resource is denied", - Description: `The access controller denied access for the operation on a - resource.`, + Message: "requested access to the resource is denied", + Description: "The access controller denied access for the operation on a resource.", }, UNSUPPORTED: { Message: "The operation is unsupported.", - Description: `The operation was unsupported due to a missing - implementation or invalid set of parameters.`, + Description: "The operation was unsupported due to a missing " + + "implementation or invalid set of parameters.", }, INVALID_INDEX: { @@ -173,19 +164,24 @@ func NewError(code ErrorCode, detail ...interface{}) Error { } err.Code = code.String() - err.Detail = detail + err.Detail = map[string]string{ + "description": err.Description, + } + + return &err +} + +func (err *Error) AddDetail(m map[string]string) *Error { + for k, v := range m { + err.Detail[k] = v + } return err } -func NewErrorList(errors ...Error) ErrorList { - errList := make([]*Error, 0) - err := Error{} - - for _, e := range errors { - err = e - errList = append(errList, &err) - } +func NewErrorList(errors ...*Error) ErrorList { + var errList []*Error + errList = append(errList, errors...) return ErrorList{errList} } diff --git a/pkg/api/errors/errors_test.go b/pkg/api/errors/errors_test.go index 0a4744e3..d62a3055 100644 --- a/pkg/api/errors/errors_test.go +++ b/pkg/api/errors/errors_test.go @@ -10,6 +10,6 @@ import ( func TestUnknownCodeError(t *testing.T) { Convey("Retrieve a new error with unknown code", t, func() { - So(func() { _ = apiErr.NewError(123456789, nil) }, ShouldPanic) + So(func() { _ = apiErr.NewError(123456789) }, ShouldPanic) }) } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 417cf3dd..396bcc9a 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -334,8 +334,8 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req tags, err := imgStore.GetImageTags(name) if err != nil { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(map[string]string{"name": name}) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) return } @@ -422,25 +422,27 @@ func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *htt reference, ok := vars["reference"] if !ok || reference == "" { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) + e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"reference": reference}) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) return } content, digest, mediaType, err := getImageManifest(rh, imgStore, name, reference) //nolint:contextcheck if err != nil { + details := zerr.GetDetails(err) + details["reference"] = reference + if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"reference": reference}))) + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrManifestNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") - zcommon.WriteJSON(response, http.StatusInternalServerError, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) + e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(e)) } return @@ -490,24 +492,27 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http. reference, ok := vars["reference"] if !ok || reference == "" { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + err := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(map[string]string{"reference": reference}) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(err)) return } content, digest, mediaType, err := getImageManifest(rh, imgStore, name, reference) //nolint: contextcheck if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoBadVersion) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrManifestNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -655,18 +660,16 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht reference, ok := vars["reference"] if !ok || reference == "" { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) + err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"reference": reference}) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(err)) return } mediaType := request.Header.Get("Content-Type") if !storageCommon.IsSupportedMediaType(mediaType) { - // response.WriteHeader(http.StatusUnsupportedMediaType) - zcommon.WriteJSON(response, http.StatusUnsupportedMediaType, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"mediaType": mediaType}))) + err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"mediaType": mediaType}) + zcommon.WriteJSON(response, http.StatusUnsupportedMediaType, apiErr.NewErrorList(err)) return } @@ -683,25 +686,31 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body) if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrManifestNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBadManifest) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBlobNotFound) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{"blob": digest.String()}))) + details["blob"] = digest.String() + e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoBadVersion) { - zcommon.WriteJSON(response, http.StatusInternalServerError, - apiErr.NewErrorList(apiErr.NewError(apiErr.INVALID_INDEX, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.INVALID_INDEX).AddDetail(details) + zcommon.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrImageLintAnnotations) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError( - apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}).WithMessage(err.Error()))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else { // could be syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup") @@ -782,15 +791,19 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht manifestBlob, manifestDigest, mediaType, err := imgStore.GetImageManifest(name, reference) if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrManifestNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBadManifest) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.UNSUPPORTED).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -800,19 +813,24 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht } err = imgStore.DeleteImageManifest(name, reference, detectCollision) - if err != nil { + if err != nil { //nolint: dupl + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrManifestNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrManifestConflict) { - zcommon.WriteJSON(response, http.StatusConflict, - apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusConflict, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBadManifest) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED, map[string]string{"reference": reference}))) + details["reference"] = reference + e := apiErr.NewError(apiErr.UNSUPPORTED).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -868,17 +886,19 @@ func (rh *RouteHandler) CheckBlob(response http.ResponseWriter, request *http.Re ok, blen, err := imgStore.CheckBlob(name, digest) if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, - http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBlobNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, - map[string]string{"digest": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -888,8 +908,8 @@ func (rh *RouteHandler) CheckBlob(response http.ResponseWriter, request *http.Re } if !ok { - zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, - map[string]string{"digest": digest.String()}))) + e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(map[string]string{"digest": digest.String()}) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) return } @@ -1018,18 +1038,19 @@ func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Requ } if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, - http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBlobNotFound) { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{"digest": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1087,22 +1108,23 @@ func (rh *RouteHandler) DeleteBlob(response http.ResponseWriter, request *http.R err = imgStore.DeleteBlob(name, digest) if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, - http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(map[string]string{"name": name}) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBlobNotFound) { - zcommon.WriteJSON(response, - http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{".String()": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBlobReferenced) { - zcommon.WriteJSON(response, - http.StatusMethodNotAllowed, - apiErr.NewErrorList(apiErr.NewError(apiErr.DENIED, map[string]string{".String()": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.DENIED).AddDetail(details) + zcommon.WriteJSON(response, http.StatusMethodNotAllowed, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1154,9 +1176,11 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * if err != nil { upload, err := imgStore.NewBlobUpload(name) if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1211,8 +1235,15 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * contentLength, err := strconv.ParseInt(request.Header.Get("Content-Length"), 10, 64) if err != nil || contentLength <= 0 { rh.c.Log.Warn().Str("actual", request.Header.Get("Content-Length")).Msg("invalid content length") - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"digest": digest.String()}))) + details := map[string]string{"digest": digest.String()} + + if err != nil { + details["conversion error"] = err.Error() + } else { + details["Content-Length"] = request.Header.Get("Content-Length") + } + e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) return } @@ -1241,9 +1272,11 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * upload, err := imgStore.NewBlobUpload(name) if err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1291,18 +1324,20 @@ func (rh *RouteHandler) GetBlobUpload(response http.ResponseWriter, request *htt size, err := imgStore.GetBlobUpload(name, sessionID) if err != nil { - if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) - } else if errors.Is(err, zerr.ErrBadBlobDigest) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + details := zerr.GetDetails(err) + //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain + if errors.Is(err, zerr.ErrBadUploadRange) || errors.Is(err, zerr.ErrBadBlobDigest) { + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrUploadNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1380,16 +1415,20 @@ func (rh *RouteHandler) PatchBlobUpload(response http.ResponseWriter, request *h clen, err = imgStore.PutBlobChunk(name, sessionID, from, to, request.Body) } - if err != nil { + if err != nil { //nolint: dupl + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrUploadNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { // could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files") @@ -1500,16 +1539,20 @@ func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request * } _, err = imgStore.PutBlobChunk(name, sessionID, from, to, request.Body) - if err != nil { + if err != nil { //nolint:dupl + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrUploadNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { // could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files") @@ -1528,18 +1571,23 @@ func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request * finish: // blob chunks already transferred, just finish if err := imgStore.FinishBlobUpload(name, sessionID, request.Body, digest); err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + details["digest"] = digest.String() + e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrBadUploadRange) { - zcommon.WriteJSON(response, http.StatusBadRequest, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrUploadNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { // could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files") @@ -1591,12 +1639,15 @@ func (rh *RouteHandler) DeleteBlobUpload(response http.ResponseWriter, request * } if err := imgStore.DeleteBlobUpload(name, sessionID); err != nil { + details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) + details["name"] = name + e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrUploadNotFound) { - zcommon.WriteJSON(response, http.StatusNotFound, - apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details) + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e)) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/extensions/extensions_lint_disabled.go b/pkg/extensions/extensions_lint_disabled.go index d2d803d1..50b993f7 100644 --- a/pkg/extensions/extensions_lint_disabled.go +++ b/pkg/extensions/extensions_lint_disabled.go @@ -10,7 +10,7 @@ import ( ) func GetLinter(config *config.Config, log log.Logger) *lint.Linter { - log.Warn().Msg("lint extension is disabled because given zot binary doesn't" + + log.Warn().Msg("lint extension is disabled because given zot binary doesn't " + "include this feature please build a binary that does so") return nil diff --git a/pkg/extensions/lint/lint.go b/pkg/extensions/lint/lint.go index e093ef30..9150fa2b 100644 --- a/pkg/extensions/lint/lint.go +++ b/pkg/extensions/lint/lint.go @@ -105,7 +105,7 @@ func (linter *Linter) CheckMandatoryAnnotations(repo string, manifestDigest godi string(manifestDigest), string(configDigest), missingAnnotations) linter.log.Error().Msg(msg) - return false, fmt.Errorf("%s: %w", msg, zerr.ErrImageLintAnnotations) + return false, zerr.NewError(zerr.ErrImageLintAnnotations).AddDetail("missingAnnotations", msg) } return true, nil diff --git a/pkg/storage/common/common.go b/pkg/storage/common/common.go index ad5643ef..cdb44ece 100644 --- a/pkg/storage/common/common.go +++ b/pkg/storage/common/common.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io" "math/rand" "path" @@ -27,8 +28,7 @@ import ( const ( manifestWithEmptyLayersErrMsg = "layers: Array must have at least 1 items" - - cosignSignatureTagSuffix = "sig" + cosignSignatureTagSuffix = "sig" ) func GetTagsByIndex(index ispec.Index) []string { @@ -86,7 +86,7 @@ func ValidateManifest(imgStore storageTypes.ImageStore, repo, reference, mediaTy if err := ValidateManifestSchema(body); err != nil { log.Error().Err(err).Msg("OCIv1 image manifest schema validation failed") - return "", zerr.ErrBadManifest + return "", zerr.NewError(zerr.ErrBadManifest).AddDetail("jsonSchemaValidation", err.Error()) } if err := json.Unmarshal(body, &manifest); err != nil { @@ -125,7 +125,7 @@ func ValidateManifest(imgStore storageTypes.ImageStore, repo, reference, mediaTy if err := ValidateImageIndexSchema(body); err != nil { log.Error().Err(err).Msg("OCIv1 image index manifest schema validation failed") - return "", err + return "", zerr.NewError(zerr.ErrBadManifest).AddDetail("jsonSchemaValidation", err.Error()) } var indexManifest ispec.Index @@ -219,8 +219,10 @@ func CheckIfIndexNeedsUpdate(index *ispec.Index, desc *ispec.Descriptor, log.Error().Err(err). Str("old mediaType", manifest.MediaType). Str("new mediaType", desc.MediaType).Msg("cannot change media-type") + reason := fmt.Sprintf("changing manifest media-type from \"%s\" to \"%s\" is disallowed", + manifest.MediaType, desc.MediaType) - return false, "", err + return false, "", zerr.NewError(err).AddDetail("reason", reason) } oldDesc := *desc @@ -780,7 +782,7 @@ func IsNonDistributable(mediaType string) bool { func ValidateManifestSchema(buf []byte) error { if err := schema.ValidatorMediaTypeManifest.Validate(bytes.NewBuffer(buf)); err != nil { if !IsEmptyLayersError(err) { - return zerr.ErrBadManifest + return err } } @@ -789,7 +791,7 @@ func ValidateManifestSchema(buf []byte) error { func ValidateImageIndexSchema(buf []byte) error { if err := schema.ValidatorMediaTypeImageIndex.Validate(bytes.NewBuffer(buf)); err != nil { - return zerr.ErrBadManifest + return err } return nil diff --git a/pkg/storage/common/common_test.go b/pkg/storage/common/common_test.go index a303ef16..315d9640 100644 --- a/pkg/storage/common/common_test.go +++ b/pkg/storage/common/common_test.go @@ -3,6 +3,7 @@ package storage_test import ( "bytes" "encoding/json" + "errors" "os" "testing" @@ -12,7 +13,7 @@ import ( "github.com/rs/zerolog" . "github.com/smartystreets/goconvey/convey" - "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/storage" @@ -74,6 +75,10 @@ func TestValidateManifest(t *testing.T) { _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body) So(err, ShouldNotBeNil) + var internalErr *zerr.Error + So(errors.As(err, &internalErr), ShouldBeTrue) + So(internalErr.GetDetails(), ShouldContainKey, "jsonSchemaValidation") + So(internalErr.GetDetails()["jsonSchemaValidation"], ShouldEqual, "[schemaVersion: Must be less than or equal to 2]") }) Convey("manifest with non-distributable layers", func() { @@ -169,7 +174,7 @@ func TestGetReferrersErrors(t *testing.T) { return indexBuf, nil }, GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { - return []byte{}, errors.ErrBlobNotFound + return []byte{}, zerr.ErrBlobNotFound }, } @@ -188,7 +193,7 @@ func TestGetReferrersErrors(t *testing.T) { return indexBuf, nil }, GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { - return []byte{}, errors.ErrBadBlob + return []byte{}, zerr.ErrBadBlob }, } @@ -363,7 +368,7 @@ func TestGetImageIndexErrors(t *testing.T) { Convey("Trigger GetBlobContent error", t, func(c C) { imgStore := &mocks.MockedImageStore{ GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { - return []byte{}, errors.ErrBlobNotFound + return []byte{}, zerr.ErrBlobNotFound }, }