diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 3a679705..6098ed92 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -42,6 +42,7 @@ func (c *Controller) Run() error { handlers.PrintRecoveryStack(false))) c.Router = engine + c.Router.UseEncodedPath() _ = NewRouteHandler(c) c.ImageStore = storage.NewImageStore(c.Config.Storage.RootDirectory, c.Log) diff --git a/pkg/api/regexp.go b/pkg/api/regexp.go index e2df041e..45b9a86b 100644 --- a/pkg/api/regexp.go +++ b/pkg/api/regexp.go @@ -6,7 +6,7 @@ import "regexp" var ( // alphaNumericRegexp defines the alpha numeric atom, typically a // component of names. This only allows lower case characters and digits. - alphaNumericRegexp = match(`[a-z0-9]+`) + alphaNumericRegexp = match(`[a-zA-Z0-9]+`) // separatorRegexp defines the separators allowed to be embedded in name // components. This allow one period, one or two underscore and multiple diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 99bb69af..0f7e6433 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -838,7 +838,7 @@ func (rh *RouteHandler) DeleteBlobUpload(w http.ResponseWriter, r *http.Request) return } - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusNoContent) } type RepositoryList struct { diff --git a/pkg/compliance/config.go b/pkg/compliance/config.go index 721d7312..8b17140e 100644 --- a/pkg/compliance/config.go +++ b/pkg/compliance/config.go @@ -5,8 +5,9 @@ type Config struct { Port string Version string OutputJSON bool + Compliance bool } func NewConfig() *Config { - return &Config{} + return &Config{Compliance: true} } diff --git a/pkg/compliance/v1_0_0/check.go b/pkg/compliance/v1_0_0/check.go index 2458ea1f..cc5cf31b 100644 --- a/pkg/compliance/v1_0_0/check.go +++ b/pkg/compliance/v1_0_0/check.go @@ -19,6 +19,20 @@ import ( "gopkg.in/resty.v1" ) +func Location(baseURL string, resp *resty.Response, config *compliance.Config) string { + // For some API responses, the Location header is set and is supposed to + // indicate an opaque value. However, it is not clear if this value is an + // absolute URL (https://server:port/v2/...) or just a path (/v2/...) + // zot implements the latter as per the spec, but some registries appear to + // return the former - this needs to be clarified + loc := resp.Header().Get("Location") + if config.Compliance { + return loc + } + + return baseURL + loc +} + func CheckWorkflows(t *testing.T, config *compliance.Config) { if config == nil || config.Address == "" || config.Port == "" { panic("insufficient config") @@ -70,140 +84,152 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(resp.StatusCode(), ShouldEqual, 200) So(resp.String(), ShouldNotBeEmpty) r := resp.Result().(*api.RepositoryList) - So(len(r.Repositories), ShouldBeGreaterThan, 0) - So(r.Repositories[0], ShouldEqual, "a/b/c/d") - So(r.Repositories[1], ShouldEqual, "z") + if !config.Compliance { + // stricter check for zot ci/cd + So(len(r.Repositories), ShouldBeGreaterThan, 0) + So(r.Repositories[0], ShouldEqual, "a/b/c/d") + So(r.Repositories[1], ShouldEqual, "z") + } }) Convey("Get images in a repository", func() { Print("\nGet images in a repository") // non-existent repository should fail - resp, err := resty.R().Get(baseURL + "/v2/repo/tags/list") + resp, err := resty.R().Get(baseURL + "/v2/repo1/tags/list") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) So(resp.String(), ShouldNotBeEmpty) // after newly created upload should succeed - resp, err = resty.R().Post(baseURL + "/v2/repo/blobs/uploads/") + resp, err = resty.R().Post(baseURL + "/v2/repo1/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - resp, err = resty.R().Get(baseURL + "/v2/repo/tags/list") + resp, err = resty.R().Get(baseURL + "/v2/repo1/tags/list") So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) - So(resp.String(), ShouldNotBeEmpty) + if !config.Compliance { + // stricter check for zot ci/cd + So(resp.StatusCode(), ShouldEqual, 200) + So(resp.String(), ShouldNotBeEmpty) + } }) Convey("Monolithic blob upload", func() { Print("\nMonolithic blob upload") - resp, err := resty.R().Post(baseURL + "/v2/repo/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo2/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 204) - resp, err = resty.R().Get(baseURL + "/v2/repo/tags/list") + resp, err = resty.R().Get(baseURL + "/v2/repo2/tags/list") So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) - So(resp.String(), ShouldNotBeEmpty) + if !config.Compliance { + // stricter check for zot ci/cd + So(resp.StatusCode(), ShouldEqual, 200) + So(resp.String(), ShouldNotBeEmpty) + } // without a "?digest=<>" should fail content := []byte("this is a blob") digest := godigest.FromBytes(content) So(digest, ShouldNotBeNil) - resp, err = resty.R().Put(baseURL + loc) + resp, err = resty.R().Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without the Content-Length should fail - resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(baseURL + loc) + resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without any data to send, should fail resp, err = resty.R().SetQueryParam("digest", digest.String()). - SetHeader("Content-Type", "application/octet-stream").Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) // monolithic blob upload: success resp, err = resty.R().SetQueryParam("digest", digest.String()). - SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) - blobLoc := resp.Header().Get("Location") + blobLoc := Location(baseURL, resp, config) So(blobLoc, ShouldNotBeEmpty) So(resp.Header().Get("Content-Length"), ShouldEqual, "0") So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty) // upload reference should now be removed - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) // blob reference should be accessible - resp, err = resty.R().Get(baseURL + blobLoc) + resp, err = resty.R().Get(blobLoc) 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/repo1/repo2/repo3/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo10/repo20/repo30/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 204) - resp, err = resty.R().Get(baseURL + "/v2/repo1/repo2/repo3/tags/list") + resp, err = resty.R().Get(baseURL + "/v2/repo10/repo20/repo30/tags/list") So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) - So(resp.String(), ShouldNotBeEmpty) + if !config.Compliance { + // stricter check for zot ci/cd + So(resp.StatusCode(), ShouldEqual, 200) + So(resp.String(), ShouldNotBeEmpty) + } // without a "?digest=<>" should fail content := []byte("this is a blob") digest := godigest.FromBytes(content) So(digest, ShouldNotBeNil) - resp, err = resty.R().Put(baseURL + loc) + resp, err = resty.R().Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without the Content-Length should fail - resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(baseURL + loc) + resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without any data to send, should fail resp, err = resty.R().SetQueryParam("digest", digest.String()). - SetHeader("Content-Type", "application/octet-stream").Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) // monolithic blob upload: success resp, err = resty.R().SetQueryParam("digest", digest.String()). - SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) - blobLoc := resp.Header().Get("Location") + blobLoc := Location(baseURL, resp, config) So(blobLoc, ShouldNotBeEmpty) So(resp.Header().Get("Content-Length"), ShouldEqual, "0") So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty) // upload reference should now be removed - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) // blob reference should be accessible - resp, err = resty.R().Get(baseURL + blobLoc) + resp, err = resty.R().Get(blobLoc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) Convey("Chunked blob upload", func() { Print("\nChunked blob upload") - resp, err := resty.R().Post(baseURL + "/v2/repo/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo3/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) var buf bytes.Buffer @@ -215,12 +241,12 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // write first chunk contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1)) resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). - SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(baseURL + loc) + SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) // check progress - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 204) r := resp.Header().Get("Range") @@ -230,7 +256,7 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // write same chunk should fail contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1)) resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). - SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(baseURL + loc) + SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) So(resp.String(), ShouldNotBeEmpty) @@ -247,31 +273,31 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes())) resp, err = resty.R().SetQueryParam("digest", digest.String()). SetHeader("Content-Range", contentRange). - SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) - blobLoc := resp.Header().Get("Location") + blobLoc := Location(baseURL, resp, config) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) So(blobLoc, ShouldNotBeEmpty) So(resp.Header().Get("Content-Length"), ShouldEqual, "0") So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty) // upload reference should now be removed - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) // blob reference should be accessible - resp, err = resty.R().Get(baseURL + blobLoc) + resp, err = resty.R().Get(blobLoc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) Convey("Chunked blob upload with multiple name components", func() { Print("\nChunked blob upload with multiple name components") - resp, err := resty.R().Post(baseURL + "/v2/repo4/repo5/repo6/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo40/repo50/repo60/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) var buf bytes.Buffer @@ -283,12 +309,12 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // write first chunk contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1)) resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). - SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(baseURL + loc) + SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) // check progress - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 204) r := resp.Header().Get("Range") @@ -298,7 +324,7 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // write same chunk should fail contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1)) resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). - SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(baseURL + loc) + SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) So(resp.String(), ShouldNotBeEmpty) @@ -315,21 +341,21 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes())) resp, err = resty.R().SetQueryParam("digest", digest.String()). SetHeader("Content-Range", contentRange). - SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) - blobLoc := resp.Header().Get("Location") + blobLoc := Location(baseURL, resp, config) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) So(blobLoc, ShouldNotBeEmpty) So(resp.Header().Get("Content-Length"), ShouldEqual, "0") So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty) // upload reference should now be removed - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) // blob reference should be accessible - resp, err = resty.R().Get(baseURL + blobLoc) + resp, err = resty.R().Get(blobLoc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) @@ -337,25 +363,25 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { Convey("Create and delete uploads", func() { Print("\nCreate and delete uploads") // create a upload - resp, err := resty.R().Post(baseURL + "/v2/repo/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo4/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) // delete this upload - resp, err = resty.R().Delete(baseURL + loc) + resp, err = resty.R().Delete(loc) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) + So(resp.StatusCode(), ShouldEqual, 204) }) Convey("Create and delete blobs", func() { Print("\nCreate and delete blobs") // create a upload - resp, err := resty.R().Post(baseURL + "/v2/repo/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo5/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) content := []byte("this is a blob") @@ -363,15 +389,15 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(digest, ShouldNotBeNil) // monolithic blob upload resp, err = resty.R().SetQueryParam("digest", digest.String()). - SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) - blobLoc := resp.Header().Get("Location") + blobLoc := Location(baseURL, resp, config) So(blobLoc, ShouldNotBeEmpty) So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty) // delete this blob - resp, err = resty.R().Delete(baseURL + blobLoc) + resp, err = resty.R().Delete(blobLoc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) So(resp.Header().Get("Content-Length"), ShouldEqual, "0") @@ -380,21 +406,21 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { Convey("Mount blobs", func() { Print("\nMount blobs from another repository") // create a upload - resp, err := resty.R().Post(baseURL + "/v2/repo/blobs/uploads/?digest=\"abc\"&&from=\"xyz\"") + resp, err := resty.R().Post(baseURL + "/v2/repo6/blobs/uploads/?digest=\"abc\"&&from=\"xyz\"") So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 405) + So(resp.StatusCode(), ShouldBeIn, []int{201, 202, 405}) }) Convey("Manifests", func() { Print("\nManifests") // create a blob/layer - resp, err := resty.R().Post(baseURL + "/v2/repo/blobs/uploads/") + resp, err := resty.R().Post(baseURL + "/v2/repo7/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) - loc := resp.Header().Get("Location") + loc := Location(baseURL, resp, config) So(loc, ShouldNotBeEmpty) - resp, err = resty.R().Get(baseURL + loc) + resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 204) content := []byte("this is a blob") @@ -402,7 +428,7 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(digest, ShouldNotBeNil) // monolithic blob upload: success resp, err = resty.R().SetQueryParam("digest", digest.String()). - SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(baseURL + loc) + SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) blobLoc := resp.Header().Get("Location") @@ -417,7 +443,7 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { 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/repo/manifests/test:1.0") + SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 201) d := resp.Header().Get(api.DistContentDigestKey) @@ -425,48 +451,63 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(d, ShouldEqual, digest.String()) // check/get by tag - resp, err = resty.R().Head(baseURL + "/v2/repo/manifests/test:1.0") + resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, err = resty.R().Get(baseURL + "/v2/repo/manifests/test:1.0") + resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeEmpty) // check/get by reference - resp, err = resty.R().Head(baseURL + "/v2/repo/manifests/" + digest.String()) + resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, err = resty.R().Get(baseURL + "/v2/repo/manifests/" + digest.String()) + resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeEmpty) // delete manifest - resp, err = resty.R().Delete(baseURL + "/v2/repo/manifests/test:1.0") + resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // delete again should fail - resp, err = resty.R().Delete(baseURL + "/v2/repo/manifests/" + digest.String()) + resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) // check/get by tag - resp, err = resty.R().Head(baseURL + "/v2/repo/manifests/test:1.0") + resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) - resp, err = resty.R().Get(baseURL + "/v2/repo/manifests/test:1.0") + resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) So(resp.Body(), ShouldNotBeEmpty) // check/get by reference - resp, err = resty.R().Head(baseURL + "/v2/repo/manifests/" + digest.String()) + resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) - resp, err = resty.R().Get(baseURL + "/v2/repo/manifests/" + digest.String()) + resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 404) So(resp.Body(), ShouldNotBeEmpty) }) + + // this is an additional test for repository names (alphanumeric) + Convey("Repository names", func() { + Print("\nRepository names") + // create a blob/layer + resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 202) + resp, err = resty.R().Post(baseURL + "/v2/repotest123/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 202) + resp, err = resty.R().Post(baseURL + "/v2/repoTest123/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 202) + }) }) }