diff --git a/pkg/api/auth.go b/pkg/api/auth.go index d265e39d..bc9a44ac 100644 --- a/pkg/api/auth.go +++ b/pkg/api/auth.go @@ -58,7 +58,7 @@ func bearerAuthHandler(c *Controller) mux.MiddlewareFunc { if err != nil { c.Log.Error().Err(err).Msg("issue parsing Authorization header") w.Header().Set("Content-Type", "application/json") - WriteJSON(w, http.StatusInternalServerError, NewError(UNSUPPORTED)) + WriteJSON(w, http.StatusInternalServerError, NewErrorList(NewError(UNSUPPORTED))) return } if !permissions.Allowed { @@ -218,5 +218,5 @@ func authFail(w http.ResponseWriter, realm string, delay int) { time.Sleep(time.Duration(delay) * time.Second) w.Header().Set("WWW-Authenticate", realm) w.Header().Set("Content-Type", "application/json") - WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED)) + WriteJSON(w, http.StatusUnauthorized, NewErrorList(NewError(UNAUTHORIZED))) } diff --git a/pkg/api/errors.go b/pkg/api/errors.go index f1e0b482..b97bf11d 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -1,9 +1,11 @@ package api -import "github.com/anuvu/zot/errors" +import ( + "github.com/anuvu/zot/errors" +) type Error struct { - Code ErrorCode `json:"code"` + Code string `json:"code"` Message string `json:"message"` Description string `json:"description"` Detail interface{} `json:"detail,omitempty"` @@ -34,7 +36,29 @@ const ( UNSUPPORTED ) -func NewError(code ErrorCode, detail ...interface{}) Error { +func (e ErrorCode) String() string { + m := map[ErrorCode]string{ + BLOB_UNKNOWN: "BLOB_UNKNOWN", + BLOB_UPLOAD_INVALID: "BLOB_UPLOAD_INVALID", + BLOB_UPLOAD_UNKNOWN: "BLOB_UPLOAD_UNKNOWN", + DIGEST_INVALID: "DIGEST_INVALID", + MANIFEST_BLOB_UNKNOWN: "MANIFEST_BLOB_UNKNOWN", + MANIFEST_INVALID: "MANIFEST_INVALID", + MANIFEST_UNKNOWN: "MANIFEST_UNKNOWN", + MANIFEST_UNVERIFIED: "MANIFEST_UNVERIFIED", + NAME_INVALID: "NAME_INVALID", + NAME_UNKNOWN: "NAME_UNKNOWN", + SIZE_INVALID: "SIZE_INVALID", + TAG_INVALID: "TAG_INVALID", + UNAUTHORIZED: "UNAUTHORIZED", + DENIED: "DENIED", + UNSUPPORTED: "UNSUPPORTED", + } + + return m[e] +} + +func NewError(code ErrorCode, detail ...interface{}) Error { //nolint (interfacer) var errMap = map[ErrorCode]Error{ BLOB_UNKNOWN: { Message: "blob unknown to registry", @@ -135,8 +159,20 @@ func NewError(code ErrorCode, detail ...interface{}) Error { panic(errors.ErrUnknownCode) } - e.Code = code + e.Code = code.String() e.Detail = detail return e } + +func NewErrorList(errors ...Error) ErrorList { + el := make([]*Error, 0) + er := Error{} + + for _, e := range errors { + er = e + el = append(el, &er) + } + + return ErrorList{el} +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 397c6357..d973d7a8 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -167,7 +167,7 @@ func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) { tags, err := rh.c.ImageStore.GetImageTags(name) if err != nil { - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) return } @@ -235,7 +235,7 @@ func (rh *RouteHandler) CheckManifest(w http.ResponseWriter, r *http.Request) { reference, ok := vars["reference"] if !ok || reference == "" { - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) return } @@ -243,10 +243,12 @@ func (rh *RouteHandler) CheckManifest(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrManifestNotFound: - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") - WriteJSON(w, http.StatusInternalServerError, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusInternalServerError, + NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) } return @@ -285,7 +287,7 @@ func (rh *RouteHandler) GetManifest(w http.ResponseWriter, r *http.Request) { reference, ok := vars["reference"] if !ok || reference == "" { - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) return } @@ -293,11 +295,14 @@ func (rh *RouteHandler) GetManifest(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrRepoBadVersion: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrManifestNotFound: - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -334,7 +339,7 @@ func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) { reference, ok := vars["reference"] if !ok || reference == "" { - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) return } @@ -356,13 +361,17 @@ func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrManifestNotFound: - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) case errors.ErrBadManifest: - WriteJSON(w, http.StatusBadRequest, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) case errors.ErrBlobNotFound: - WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UNKNOWN, map[string]string{"blob": digest})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"blob": digest}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -404,9 +413,11 @@ func (rh *RouteHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrManifestNotFound: - WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -449,11 +460,11 @@ func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrBadBlobDigest: - WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusBadRequest, NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrBlobNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -463,7 +474,7 @@ func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) { } if !ok { - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))) return } @@ -503,11 +514,11 @@ func (rh *RouteHandler) GetBlob(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrBadBlobDigest: - WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusBadRequest, NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrBlobNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -550,11 +561,11 @@ func (rh *RouteHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrBadBlobDigest: - WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusBadRequest, NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrBlobNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -599,11 +610,64 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) return } + // a full blob upload if "digest" is present + digests, ok := r.URL.Query()["digest"] + if ok { + if len(digests) != 1 { + w.WriteHeader(http.StatusBadRequest) + return + } + + digest := digests[0] + + if contentType := r.Header.Get("Content-Type"); contentType != BinaryMediaType { + rh.c.Log.Warn().Str("actual", contentType).Str("expected", BinaryMediaType).Msg("invalid media type") + w.WriteHeader(http.StatusUnsupportedMediaType) + + return + } + + rh.c.Log.Info().Int64("r.ContentLength", r.ContentLength).Msg("DEBUG") + + var contentLength int64 + + var err error + + if contentLength, err = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64); err != nil || contentLength <= 0 { + rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Length")).Msg("invalid content length") + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"digest": digest}))) + + return + } + + sessionID, size, err := rh.c.ImageStore.FullBlobUpload(name, r.Body, digest) + if err != nil { + rh.c.Log.Error().Err(err).Int64("actual", size).Int64("expected", contentLength).Msg("failed full upload") + w.WriteHeader(http.StatusInternalServerError) + + return + } + + if size != contentLength { + rh.c.Log.Warn().Int64("actual", size).Int64("expected", contentLength).Msg("invalid content length") + w.WriteHeader(http.StatusInternalServerError) + + return + } + + w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest)) + w.Header().Set(BlobUploadUUID, sessionID) + w.WriteHeader(http.StatusCreated) + + return + } + u, err := rh.c.ImageStore.NewBlobUpload(name) if err != nil { switch err { case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -649,13 +713,17 @@ func (rh *RouteHandler) GetBlobUpload(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case errors.ErrBadUploadRange: - WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) case errors.ErrBadBlobDigest: - WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrUploadNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -746,11 +814,14 @@ func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request) if err != nil { switch err { case errors.ErrBadUploadRange: - WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusRequestedRangeNotSatisfiable, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrUploadNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -852,11 +923,14 @@ func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request) if err != nil { switch err { case errors.ErrBadUploadRange: - WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrUploadNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -875,13 +949,17 @@ finish: if err := rh.c.ImageStore.FinishBlobUpload(name, sessionID, r.Body, digest); err != nil { switch err { case errors.ErrBadBlobDigest: - WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest}))) case errors.ErrBadUploadRange: - WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrUploadNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -925,9 +1003,11 @@ func (rh *RouteHandler) DeleteBlobUpload(w http.ResponseWriter, r *http.Request) if err := rh.c.ImageStore.DeleteBlobUpload(name, sessionID); err != nil { switch err { case errors.ErrRepoNotFound: - WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) case errors.ErrUploadNotFound: - WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})) + WriteJSON(w, http.StatusNotFound, + NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/compliance/v1_0_0/check.go b/pkg/compliance/v1_0_0/check.go index 05a4d80f..4d7be43b 100644 --- a/pkg/compliance/v1_0_0/check.go +++ b/pkg/compliance/v1_0_0/check.go @@ -169,6 +169,44 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(resp.StatusCode(), ShouldEqual, 200) }) + Convey("Monolithic blob upload with body", func() { + Print("\nMonolithic blob upload") + // create content + content := []byte("this is a blob") + digest := godigest.FromBytes(content) + So(digest, ShouldNotBeNil) + // setting invalid URL params should fail + resp, err := resty.R(). + SetQueryParam("digest", digest.String()). + SetQueryParam("from", digest.String()). + SetHeader("Content-Type", "application/octet-stream"). + SetBody(content). + Post(baseURL + "/v2/repo2/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 405) + // setting a "?digest=<>" but without body should fail + resp, err = resty.R(). + SetQueryParam("digest", digest.String()). + SetHeader("Content-Type", "application/octet-stream"). + Post(baseURL + "/v2/repo2/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 400) + // set a "?digest=<>" + resp, err = resty.R(). + SetQueryParam("digest", digest.String()). + SetHeader("Content-Type", "application/octet-stream"). + SetBody(content). + Post(baseURL + "/v2/repo2/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 201) + loc := Location(baseURL, resp) + So(loc, ShouldNotBeEmpty) + // blob reference should be accessible + resp, err = resty.R().Get(loc) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) + Convey("Monolithic blob upload with multiple name components", func() { Print("\nMonolithic blob upload with multiple name components") resp, err := resty.R().Post(baseURL + "/v2/repo10/repo20/repo30/blobs/uploads/") @@ -258,7 +296,7 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 400) + So(resp.StatusCode(), ShouldEqual, 416) So(resp.String(), ShouldNotBeEmpty) chunk2 := []byte("this is the second chunk") @@ -326,7 +364,7 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 400) + So(resp.StatusCode(), ShouldEqual, 416) So(resp.String(), ShouldNotBeEmpty) chunk2 := []byte("this is the second chunk") @@ -450,6 +488,23 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(d, ShouldNotBeEmpty) So(d, ShouldEqual, digest.String()) + content = []byte("this is a blob") + digest = godigest.FromBytes(content) + So(digest, ShouldNotBeNil) + // create a manifest with same blob but a different tag + m = ispec.Manifest{Layers: []ispec.Descriptor{{Digest: digest}}} + content, err = json.Marshal(m) + So(err, ShouldBeNil) + digest = godigest.FromBytes(content) + So(digest, ShouldNotBeNil) + resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:2.0") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 201) + d = resp.Header().Get(api.DistContentDigestKey) + So(d, ShouldNotBeEmpty) + So(d, ShouldEqual, digest.String()) + // check/get by tag resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) @@ -475,6 +530,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) + // delete manifest by digest + resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 202) // delete again should fail resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) @@ -488,6 +547,13 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) So(resp.Body(), ShouldNotBeEmpty) + resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:2.0") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 404) + resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:2.0") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 404) + So(resp.Body(), ShouldNotBeEmpty) // check/get by reference resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 4c3ee0be..2ee742ac 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -1,7 +1,9 @@ package storage import ( + "crypto/sha256" "encoding/json" + "fmt" "io" "io/ioutil" "os" @@ -477,14 +479,6 @@ func (is *ImageStore) DeleteImageManifest(repo string, reference string) error { break } - - v, ok := m.Annotations[ispec.AnnotationRefName] - if ok && v == reference { - digest = m.Digest - found = true - - break - } } if !found { @@ -701,6 +695,62 @@ func (is *ImageStore) FinishBlobUpload(repo string, uuid string, body io.Reader, return err } +// FullBlobUpload handles a full blob upload, and no partial session is created +func (is *ImageStore) FullBlobUpload(repo string, body io.Reader, digest string) (string, int64, error) { + if err := is.InitRepo(repo); err != nil { + return "", -1, err + } + + dstDigest, err := godigest.Parse(digest) + if err != nil { + is.log.Error().Err(err).Str("digest", digest).Msg("failed to parse digest") + return "", -1, errors.ErrBadBlobDigest + } + + u, err := guuid.NewV4() + if err != nil { + return "", -1, err + } + + uuid := u.String() + + src := is.BlobUploadPath(repo, uuid) + + f, err := os.Create(src) + if err != nil { + is.log.Error().Err(err).Str("blob", src).Msg("failed to open blob") + return "", -1, errors.ErrUploadNotFound + } + + defer f.Close() + + digester := sha256.New() + mw := io.MultiWriter(f, digester) + n, err := io.Copy(mw, body) + + if err != nil { + return "", -1, err + } + + srcDigest := godigest.NewDigestFromEncoded(godigest.SHA256, fmt.Sprintf("%x", digester.Sum(nil))) + if srcDigest != dstDigest { + is.log.Error().Str("srcDigest", srcDigest.String()). + Str("dstDigest", dstDigest.String()).Msg("actual digest not equal to expected digest") + return "", -1, errors.ErrBadBlobDigest + } + + dir := path.Join(is.rootDir, repo) + dir = path.Join(dir, "blobs") + dir = path.Join(dir, dstDigest.Algorithm().String()) + _ = os.MkdirAll(dir, 0755) + dst := is.BlobPath(repo, dstDigest) + + // move the blob from uploads to final dest + _ = os.Rename(src, dst) + + return uuid, n, err +} + // DeleteBlobUpload deletes an existing blob upload that is currently in progress. func (is *ImageStore) DeleteBlobUpload(repo string, uuid string) error { blobUploadPath := is.BlobUploadPath(repo, uuid) diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 69342c27..71497e48 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -58,6 +58,16 @@ func TestAPIs(t *testing.T) { So(v, ShouldBeEmpty) }) + Convey("Full blob upload", func() { + body := []byte("this is a blob") + buf := bytes.NewBuffer(body) + d := godigest.FromBytes(body) + u, n, err := il.FullBlobUpload("test", buf, d.String()) + So(err, ShouldBeNil) + So(n, ShouldEqual, len(body)) + So(u, ShouldNotBeEmpty) + }) + Convey("New blob upload", func() { v, err := il.NewBlobUpload("test") So(err, ShouldBeNil)