From 48fb4967a20e14aac76bfc29ad2921eb71a8f719 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 30 Jan 2020 23:54:05 -0800 Subject: [PATCH 1/5] errors: compliance requires error codes to be string enum constants. --- pkg/api/errors.go | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/pkg/api/errors.go b/pkg/api/errors.go index f1e0b482..a6b12819 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,6 +36,27 @@ const ( UNSUPPORTED ) +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 { var errMap = map[ErrorCode]Error{ BLOB_UNKNOWN: { @@ -135,8 +158,18 @@ 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) + + for _, e := range errors { + el = append(el, &e) + } + + return ErrorList{el} +} From 909a97b922aa1398943aa3cbedb10ead90302c11 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 30 Jan 2020 23:54:39 -0800 Subject: [PATCH 2/5] storage: compliance allows for a full blob upload without a session implement a new method which just takes the repo name, body and digest and creates a blob out of this --- pkg/storage/storage.go | 65 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 4c3ee0be..dc226317 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,61 @@ 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) From d9fcf713ca29c9e7aace183914e9df2ebdf865b2 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 30 Jan 2020 23:57:03 -0800 Subject: [PATCH 3/5] auth: compliance requires error codes be returned a certain way use the new NewErrorList() method and the enum constants as strings --- pkg/api/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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))) } From f9a1a0fe48445951ccaef06eb000e4975f8baa20 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 30 Jan 2020 23:58:08 -0800 Subject: [PATCH 4/5] routes: handle compliance requirements - that errors be returned a certain way using the new NewErrorList() method and the string enum constants - allow for full blob upload without a session with repo name and digest --- pkg/api/routes.go | 132 +++++++++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 397c6357..88114b14 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,10 @@ 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 +285,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 +293,11 @@ 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 +334,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 +356,13 @@ 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 +404,9 @@ 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 +449,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 +463,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 +503,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 +550,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) @@ -587,6 +587,54 @@ 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 { + rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Length")).Msg("invalid content length") + w.WriteHeader(http.StatusBadRequest) + + return + } + + 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.WriteHeader(http.StatusCreated) + return + } + // blob mounts not allowed since we don't have access control yet, and this // may be a uncommon use case, but remain compliant if _, ok := r.URL.Query()["mount"]; ok { @@ -603,7 +651,7 @@ func (rh *RouteHandler) CreateBlobUpload(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}))) default: rh.c.Log.Error().Err(err).Msg("unexpected error") w.WriteHeader(http.StatusInternalServerError) @@ -649,13 +697,13 @@ 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 +794,11 @@ 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 +900,11 @@ 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 +923,13 @@ 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 +973,9 @@ 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) From 58040f45621be5b393896eae255604c2c92e91ed Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Thu, 30 Jan 2020 23:59:36 -0800 Subject: [PATCH 5/5] check: add unit tests to cover the new code, fix linter errors --- pkg/api/errors.go | 7 +- pkg/api/routes.go | 114 +++++++++++++++++++++------------ pkg/compliance/v1_0_0/check.go | 70 +++++++++++++++++++- pkg/storage/storage.go | 1 + pkg/storage/storage_test.go | 10 +++ 5 files changed, 157 insertions(+), 45 deletions(-) diff --git a/pkg/api/errors.go b/pkg/api/errors.go index a6b12819..b97bf11d 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -54,10 +54,11 @@ func (e ErrorCode) String() string { DENIED: "DENIED", UNSUPPORTED: "UNSUPPORTED", } + return m[e] } -func NewError(code ErrorCode, detail ...interface{}) Error { +func NewError(code ErrorCode, detail ...interface{}) Error { //nolint (interfacer) var errMap = map[ErrorCode]Error{ BLOB_UNKNOWN: { Message: "blob unknown to registry", @@ -166,9 +167,11 @@ func NewError(code ErrorCode, detail ...interface{}) Error { func NewErrorList(errors ...Error) ErrorList { el := make([]*Error, 0) + er := Error{} for _, e := range errors { - el = append(el, &e) + er = e + el = append(el, &er) } return ErrorList{el} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 88114b14..d973d7a8 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -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, NewErrorList(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, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) + WriteJSON(w, http.StatusInternalServerError, + NewErrorList(NewError(MANIFEST_INVALID, 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, NewErrorList(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, NewErrorList(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, NewErrorList(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) @@ -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, NewErrorList(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, NewErrorList(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, NewErrorList(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, NewErrorList(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, NewErrorList(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, NewErrorList(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) @@ -587,6 +598,18 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) return } + // blob mounts not allowed since we don't have access control yet, and this + // may be a uncommon use case, but remain compliant + if _, ok := r.URL.Query()["mount"]; ok { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + if _, ok := r.URL.Query()["from"]; ok { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + // a full blob upload if "digest" is present digests, ok := r.URL.Query()["digest"] if ok { @@ -607,16 +630,18 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) 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 { + 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") - w.WriteHeader(http.StatusBadRequest) + WriteJSON(w, http.StatusBadRequest, + NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"digest": digest}))) return } - size, err := rh.c.ImageStore.FullBlobUpload(name, r.Body, digest) + 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) @@ -631,19 +656,10 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) return } + w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest)) + w.Header().Set(BlobUploadUUID, sessionID) w.WriteHeader(http.StatusCreated) - return - } - // blob mounts not allowed since we don't have access control yet, and this - // may be a uncommon use case, but remain compliant - if _, ok := r.URL.Query()["mount"]; ok { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - if _, ok := r.URL.Query()["from"]; ok { - w.WriteHeader(http.StatusMethodNotAllowed) return } @@ -697,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, NewErrorList(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, NewErrorList(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, NewErrorList(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, NewErrorList(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) @@ -794,11 +814,14 @@ func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request) if err != nil { switch err { case errors.ErrBadUploadRange: - WriteJSON(w, http.StatusRequestedRangeNotSatisfiable, NewErrorList(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, NewErrorList(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, NewErrorList(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) @@ -900,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, NewErrorList(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, NewErrorList(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, NewErrorList(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) @@ -923,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, NewErrorList(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, NewErrorList(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, NewErrorList(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, NewErrorList(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) @@ -973,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, NewErrorList(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, NewErrorList(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 dc226317..2ee742ac 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -727,6 +727,7 @@ func (is *ImageStore) FullBlobUpload(repo string, body io.Reader, digest string) digester := sha256.New() mw := io.MultiWriter(f, digester) n, err := io.Copy(mw, body) + if err != nil { return "", -1, err } 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)