From 26643ef69687494a49c098acabc8017fbabb291b Mon Sep 17 00:00:00 2001 From: Brandon Mitchell Date: Thu, 20 Jun 2024 15:11:51 -0400 Subject: [PATCH] Support sha512 content Signed-off-by: Brandon Mitchell --- .github/workflows/conformance-action-pr.yml | 26 ---- .github/workflows/conformance-action.yml | 7 +- Makefile | 32 ++-- action.yml | 2 +- conformance/01_pull_test.go | 163 ++++++++++++++++++-- conformance/02_push_test.go | 112 ++++++++++++++ conformance/README.md | 3 + conformance/setup.go | 79 +++++++++- spec.md | 21 ++- 9 files changed, 387 insertions(+), 58 deletions(-) delete mode 100644 .github/workflows/conformance-action-pr.yml diff --git a/.github/workflows/conformance-action-pr.yml b/.github/workflows/conformance-action-pr.yml deleted file mode 100644 index d0390d19..00000000 --- a/.github/workflows/conformance-action-pr.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: conformance-action-pr - -on: - pull_request: - branches: - - main - -jobs: - run: - runs-on: ubuntu-latest - steps: - - name: checkout source code - uses: actions/checkout@v3 - - name: Start a test registry (zot) - run: | - set -x - make registry-ci - - name: Run OCI distribution-spec conformance - env: - OCI_ROOT_URL: http://localhost:5000 - OCI_NAMESPACE: myorg/myrepo - OCI_TEST_PULL: 1 - OCI_TEST_PUSH: 1 - OCI_TEST_CONTENT_DISCOVERY: 1 - OCI_TEST_CONTENT_MANAGEMENT: 1 - uses: ./ diff --git a/.github/workflows/conformance-action.yml b/.github/workflows/conformance-action.yml index f68d38d9..fd896ded 100644 --- a/.github/workflows/conformance-action.yml +++ b/.github/workflows/conformance-action.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main jobs: run: @@ -12,12 +15,14 @@ jobs: - name: checkout source code uses: actions/checkout@v3 - name: Start a test registry (zot) + id: registry-zot run: | set -x make registry-ci + echo "port=$(docker port oci-conformance 5000 | head -1 | cut -f2 -d:)" >>$GITHUB_OUTPUT - name: Run OCI distribution-spec conformance env: - OCI_ROOT_URL: http://localhost:5000 + OCI_ROOT_URL: http://localhost:${{ steps.registry-zot.output.port }} OCI_NAMESPACE: myorg/myrepo OCI_TEST_PULL: 1 OCI_TEST_PUSH: 1 diff --git a/Makefile b/Makefile index ae1a20b6..84a42214 100644 --- a/Makefile +++ b/Makefile @@ -92,20 +92,20 @@ conformance-test: conformance-binary: $(OUTPUT_DIRNAME)/conformance.test -TEST_REGISTRY_CONTAINER ?= ghcr.io/project-zot/zot-minimal-linux-amd64:v2.0.0-rc6@sha256:bf95a94849cd9c6f596fb10e5a2d03b74267e7886d1ba0b3dab33337d9e46e5c +# TODO: update image once changes are merged in zot +# TEST_REGISTRY_CONTAINER ?= ghcr.io/project-zot/zot-minimal-linux-amd64:v2.1.0 +TEST_REGISTRY_CONTAINER ?= ghcr.io/andaaron/zot-minimal-linux-amd64:v2.1.0-manifest-digest registry-ci: - docker rm -f oci-conformance && \ - mkdir -p $(OUTPUT_DIRNAME) && \ - echo '{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot","gc":false,"dedupe":false},"http":{"address":"0.0.0.0","port":"5000"},"log":{"level":"debug"}}' > $(shell pwd)/$(OUTPUT_DIRNAME)/zot-config.json - docker run -d \ - -v $(shell pwd)/$(OUTPUT_DIRNAME)/zot-config.json:/etc/zot/config.json \ - --name=oci-conformance \ - -p 5000:5000 \ - $(TEST_REGISTRY_CONTAINER) && \ - sleep 5 - -conformance-ci: - export OCI_ROOT_URL="http://localhost:5000" && \ + docker rm -f oci-conformance || true + mkdir -p $(OUTPUT_DIRNAME) + docker run -d --rm \ + --name=oci-conformance \ + -p 5000 \ + $(TEST_REGISTRY_CONTAINER) + sleep 5 + +conformance-ci: conformance-binary + export OCI_ROOT_URL="http://localhost:$$(docker port oci-conformance 5000 | head -1 | cut -f2 -d:)" && \ export OCI_NAMESPACE="myorg/myrepo" && \ export OCI_TEST_PULL=1 && \ export OCI_TEST_PUSH=1 && \ @@ -113,6 +113,12 @@ conformance-ci: export OCI_TEST_CONTENT_MANAGEMENT=1 && \ $(shell pwd)/$(OUTPUT_DIRNAME)/conformance.test +conformance-clean: + docker stop oci-conformance || true + [ ! -f $(OUTPUT_DIRNAME)/conformance.test ] || rm "$(OUTPUT_DIRNAME)/conformance.test" + [ ! -f "junit.xml" ] || rm junit.xml + [ ! -f "report.html" ] || rm report.html + $(OUTPUT_DIRNAME)/conformance.test: cd conformance && \ CGO_ENABLED=0 go test -c -o $(shell pwd)/$(OUTPUT_DIRNAME)/conformance.test \ diff --git a/action.yml b/action.yml index b58740d8..6b2ddb3c 100644 --- a/action.yml +++ b/action.yml @@ -31,7 +31,7 @@ runs: run: | set -x - # Enter the directory containing the checkout of this action which is surpisingly hard to do (but we did it... #OCI) + # Enter the directory containing the checkout of this action which is surprisingly hard to do (but we did it... #OCI) cd "$(dirname $(find $(find ~/work/_actions -name distribution-spec -print -quit) -name Makefile -print -quit))" # The .git folder is not present, but the dirname is the requested action ref, so use this as the conformance version diff --git a/conformance/01_pull_test.go b/conformance/01_pull_test.go index d0097d52..47ffffa2 100644 --- a/conformance/01_pull_test.go +++ b/conformance/01_pull_test.go @@ -2,7 +2,6 @@ package conformance import ( "net/http" - "os" "github.com/bloodorangeio/reggie" g "github.com/onsi/ginkgo/v2" @@ -12,8 +11,6 @@ import ( var test01Pull = func() { g.Context(titlePull, func() { - var tag string - g.Context("Setup", func() { g.Specify("Populate registry with test blob", func() { SkipIfDisabled(pull) @@ -72,9 +69,8 @@ var test01Pull = func() { g.Specify("Populate registry with test manifest", func() { SkipIfDisabled(pull) RunOnlyIf(runPullSetup) - tag = testTagName req := client.NewRequest(reggie.PUT, "/v2//manifests/", - reggie.WithReference(tag)). + reggie.WithReference(testTagName)). SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). SetBody(manifests[0].Content) resp, err := client.Do(req) @@ -98,14 +94,41 @@ var test01Pull = func() { BeNumerically("<", 300))) }) - g.Specify("Get tag name from environment", func() { + g.Specify("Populate registry with sha512 blobs", func() { SkipIfDisabled(pull) - RunOnlyIfNot(runPullSetup) - tmp := os.Getenv(envVarTagName) - if tmp != "" { - tag = tmp + RunOnlyIf(runPull512Setup) + for _, blob := range testBlobs["sha512"] { + req := client.NewRequest(reggie.POST, "/v2//blobs/uploads/"). + SetQueryParam("digest-algorithm", "sha512") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + req = client.NewRequest(reggie.PUT, resp.GetRelativeLocation()). + SetQueryParam("digest", blob.Digest). + SetHeader("Content-Type", "application/octet-stream"). + SetHeader("Content-Length", blob.ContentLength). + SetBody(blob.Content) + resp, err = client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300))) } }) + + g.Specify("Populate registry with test sha512 manifest", func() { + SkipIfDisabled(pull) + RunOnlyIf(runPull512Setup) + req := client.NewRequest(reggie.PUT, "/v2//manifests/", + reggie.WithReference(testTag512Name)). + SetQueryParam("digest", testManifests["sha512"].Digest). + SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(testManifests["sha512"].Content) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300))) + }) }) g.Context("Pull blobs", func() { @@ -130,6 +153,18 @@ var test01Pull = func() { } }) + g.Specify("HEAD request to existing sha512 blob should yield 200", func() { + SkipIfDisabled(pull) + req := client.NewRequest(reggie.HEAD, "/v2//blobs/", + reggie.WithDigest(testBlobs["sha512"][0].Digest)) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + if h := resp.Header().Get("Docker-Content-Digest"); h != "" { + Expect(h).To(Equal(testBlobs["sha512"][0].Digest)) + } + }) + g.Specify("GET nonexistent blob should result in 404 response", func() { SkipIfDisabled(pull) req := client.NewRequest(reggie.GET, "/v2//blobs/", @@ -146,6 +181,15 @@ var test01Pull = func() { Expect(err).To(BeNil()) Expect(resp.StatusCode()).To(Equal(http.StatusOK)) }) + + g.Specify("GET request to existing sha512 blob URL should yield 200", func() { + SkipIfDisabled(pull) + req := client.NewRequest(reggie.GET, "/v2//blobs/", + reggie.WithDigest(testBlobs["sha512"][0].Digest)) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + }) }) g.Context("Pull manifests", func() { @@ -182,10 +226,23 @@ var test01Pull = func() { } }) + g.Specify("HEAD request to sha512 manifest (digest) should yield 200 response", func() { + SkipIfDisabled(pull) + req := client.NewRequest(reggie.HEAD, "/v2//manifests/", + reggie.WithDigest(testManifests["sha512"].Digest)). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + if h := resp.Header().Get("Docker-Content-Digest"); h != "" { + Expect(h).To(Equal(testManifests["sha512"].Digest)) + } + }) + g.Specify("HEAD request to manifest path (tag) should yield 200 response", func() { SkipIfDisabled(pull) - Expect(tag).ToNot(BeEmpty()) - req := client.NewRequest(reggie.HEAD, "/v2//manifests/", reggie.WithReference(tag)). + Expect(testTagName).ToNot(BeEmpty()) + req := client.NewRequest(reggie.HEAD, "/v2//manifests/", reggie.WithReference(testTagName)). SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") resp, err := client.Do(req) Expect(err).To(BeNil()) @@ -195,6 +252,19 @@ var test01Pull = func() { } }) + g.Specify("HEAD request to sha512 manifest (tag) should yield 200 response", func() { + SkipIfDisabled(pull) + Expect(testTag512Name).ToNot(BeEmpty()) + req := client.NewRequest(reggie.HEAD, "/v2//manifests/", reggie.WithReference(testTag512Name)). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + if h := resp.Header().Get("Docker-Content-Digest"); h != "" { + Expect(h).To(Equal(testManifests["sha512"].Digest)) + } + }) + g.Specify("GET nonexistent manifest should return 404", func() { SkipIfDisabled(pull) req := client.NewRequest(reggie.GET, "/v2//manifests/", @@ -222,10 +292,29 @@ var test01Pull = func() { Expect(resp.StatusCode()).To(Equal(http.StatusOK)) }) + g.Specify("GET request to sha512 manifest (digest) should yield 200 response", func() { + SkipIfDisabled(pull) + req := client.NewRequest(reggie.GET, "/v2//manifests/", reggie.WithDigest(testManifests["sha512"].Digest)). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + }) + g.Specify("GET request to manifest path (tag) should yield 200 response", func() { SkipIfDisabled(pull) - Expect(tag).ToNot(BeEmpty()) - req := client.NewRequest(reggie.GET, "/v2//manifests/", reggie.WithReference(tag)). + Expect(testTagName).ToNot(BeEmpty()) + req := client.NewRequest(reggie.GET, "/v2//manifests/", reggie.WithReference(testTagName)). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + }) + + g.Specify("GET request to sha512 manifest (tag) should yield 200 response", func() { + SkipIfDisabled(pull) + Expect(testTag512Name).ToNot(BeEmpty()) + req := client.NewRequest(reggie.GET, "/v2//manifests/", reggie.WithReference(testTag512Name)). SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") resp, err := client.Do(req) Expect(err).To(BeNil()) @@ -285,6 +374,20 @@ var test01Pull = func() { Equal(http.StatusMethodNotAllowed), )) }) + g.Specify("Delete sha512 manifest created in setup", func() { + SkipIfDisabled(pull) + RunOnlyIf(runPull512Setup) + req := client.NewRequest(reggie.DELETE, "/v2//manifests/", reggie.WithDigest(testManifests["sha512"].Digest)) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAny( + SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300), + ), + Equal(http.StatusMethodNotAllowed), + )) + }) } g.Specify("Delete config[0] blob created in setup", func() { @@ -331,6 +434,24 @@ var test01Pull = func() { )) }) + for _, blob := range testBlobs["sha512"] { + g.Specify("Delete blob created in setup", func() { + SkipIfDisabled(pull) + RunOnlyIf(runPull512Setup) + req := client.NewRequest(reggie.DELETE, "/v2//blobs/", reggie.WithDigest(blob.Digest)) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAny( + SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300), + ), + Equal(http.StatusNotFound), + Equal(http.StatusMethodNotAllowed), + )) + }) + } + if !deleteManifestBeforeBlobs { g.Specify("Delete manifest[0] created in setup", func() { SkipIfDisabled(pull) @@ -360,6 +481,20 @@ var test01Pull = func() { Equal(http.StatusMethodNotAllowed), )) }) + g.Specify("Delete sha512 manifest created in setup", func() { + SkipIfDisabled(pull) + RunOnlyIf(runPull512Setup) + req := client.NewRequest(reggie.DELETE, "/v2//manifests/", reggie.WithDigest(testManifests["sha512"].Digest)) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAny( + SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300), + ), + Equal(http.StatusMethodNotAllowed), + )) + }) } }) }) diff --git a/conformance/02_push_test.go b/conformance/02_push_test.go index 8da40359..a0f50d4b 100644 --- a/conformance/02_push_test.go +++ b/conformance/02_push_test.go @@ -54,6 +54,40 @@ var test02Push = func() { }) }) + g.Context("Blob Upload sha512 Streamed", func() { + g.Specify("PATCH request with blob in body should yield 202 response", func() { + SkipIfDisabled(push) + req := client.NewRequest(reggie.POST, "/v2//blobs/uploads/"). + SetQueryParam("digest-algorithm", "sha512") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + location := resp.Header().Get("Location") + Expect(location).ToNot(BeEmpty()) + + req = client.NewRequest(reggie.PATCH, resp.GetRelativeLocation()). + SetHeader("Content-Type", "application/octet-stream"). + SetBody(testBlobs["sha512"][0].Content) + resp, err = client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusAccepted)) + lastResponse = resp + }) + + g.Specify("PUT request to session URL with digest should yield 201 response", func() { + SkipIfDisabled(push) + Expect(lastResponse).ToNot(BeNil()) + req := client.NewRequest(reggie.PUT, lastResponse.GetRelativeLocation()). + SetQueryParam("digest", testBlobs["sha512"][0].Digest). + SetHeader("Content-Type", "application/octet-stream"). + SetHeader("Content-Length", testBlobs["sha512"][0].ContentLength) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusCreated)) + location := resp.Header().Get("Location") + Expect(location).ToNot(BeEmpty()) + }) + }) + g.Context("Blob Upload Monolithic", func() { g.Specify("GET nonexistent blob should result in 404 response", func() { SkipIfDisabled(push) @@ -143,6 +177,23 @@ var test02Push = func() { Expect(resp.StatusCode()).To(Equal(http.StatusCreated)) }) + g.Specify("PUT upload of a sha512 blob should yield a 201 Response", func() { + SkipIfDisabled(push) + req := client.NewRequest(reggie.POST, "/v2//blobs/uploads/") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + req = client.NewRequest(reggie.PUT, resp.GetRelativeLocation()). + SetHeader("Content-Length", testBlobs["sha512"][1].ContentLength). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", testBlobs["sha512"][1].Digest). + SetBody(testBlobs["sha512"][1].Content) + resp, err = client.Do(req) + Expect(err).To(BeNil()) + location := resp.Header().Get("Location") + Expect(location).ToNot(BeEmpty()) + Expect(resp.StatusCode()).To(Equal(http.StatusCreated)) + }) + g.Specify("GET request to existing layer should yield 200 response", func() { SkipIfDisabled(push) req := client.NewRequest(reggie.GET, "/v2//blobs/", reggie.WithDigest(layerBlobDigest)) @@ -351,6 +402,20 @@ var test02Push = func() { } }) + g.Specify("PUT should accept a sha512 manifest upload", func() { + SkipIfDisabled(push) + req := client.NewRequest(reggie.PUT, "/v2//manifests/", + reggie.WithReference(testTag512Name)). + SetQueryParam("digest", testManifests["sha512"].Digest). + SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(testManifests["sha512"].Content) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + location := resp.Header().Get("Location") + Expect(location).ToNot(BeEmpty()) + Expect(resp.StatusCode()).To(Equal(http.StatusCreated)) + }) + g.Specify("Registry should accept a manifest upload with no layers", func() { SkipIfDisabled(push) req := client.NewRequest(reggie.PUT, "/v2//manifests/", @@ -377,6 +442,15 @@ var test02Push = func() { Expect(err).To(BeNil()) Expect(resp.StatusCode()).To(Equal(http.StatusOK)) }) + + g.Specify("GET request to sha512 manifest URL (digest) should yield 200 response", func() { + SkipIfDisabled(push) + req := client.NewRequest(reggie.GET, "/v2//manifests/", reggie.WithDigest(testManifests["sha512"].Digest)). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + }) }) g.Context("Teardown", func() { @@ -394,6 +468,16 @@ var test02Push = func() { ), Equal(http.StatusMethodNotAllowed), )) + req = client.NewRequest(reggie.DELETE, "/v2//manifests/", reggie.WithDigest(testManifests["sha512"].Digest)) + resp, err = client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAny( + SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300), + ), + Equal(http.StatusMethodNotAllowed), + )) if emptyLayerManifestRef != "" { req = client.NewRequest(reggie.DELETE, "/v2//manifests/", reggie.WithReference(emptyLayerManifestDigest)) resp, err = client.Do(req) @@ -440,6 +524,24 @@ var test02Push = func() { )) }) + for _, blob := range testBlobs["sha512"] { + g.Specify("Delete sha512 blob created in setup", func() { + SkipIfDisabled(push) + RunOnlyIf(runPushSetup) + req := client.NewRequest(reggie.DELETE, "/v2//blobs/", reggie.WithDigest(blob.Digest)) + resp, err := client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAny( + SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300), + ), + Equal(http.StatusNotFound), + Equal(http.StatusMethodNotAllowed), + )) + }) + } + if !deleteManifestBeforeBlobs { g.Specify("Delete manifest created in tests", func() { SkipIfDisabled(push) @@ -454,6 +556,16 @@ var test02Push = func() { ), Equal(http.StatusMethodNotAllowed), )) + req = client.NewRequest(reggie.DELETE, "/v2//manifests/", reggie.WithDigest(testManifests["sha512"].Digest)) + resp, err = client.Do(req) + Expect(err).To(BeNil()) + Expect(resp.StatusCode()).To(SatisfyAny( + SatisfyAll( + BeNumerically(">=", 200), + BeNumerically("<", 300), + ), + Equal(http.StatusMethodNotAllowed), + )) if emptyLayerManifestRef != "" { req = client.NewRequest(reggie.DELETE, "/v2//manifests/", reggie.WithReference(emptyLayerManifestDigest)) resp, err = client.Do(req) diff --git a/conformance/README.md b/conformance/README.md index 55036215..d0100c35 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -86,6 +86,9 @@ to content already present in the registry: OCI_MANIFEST_DIGEST= OCI_TAG_NAME= OCI_BLOB_DIGEST= +OCI_MANIFEST512_DIGEST= +OCI_TAG512_NAME= +OCI_BLOB512_DIGEST= ``` ##### Push diff --git a/conformance/setup.go b/conformance/setup.go index 350369aa..efdc9b64 100644 --- a/conformance/setup.go +++ b/conformance/setup.go @@ -68,8 +68,11 @@ const ( envVarContentManagement = "OCI_TEST_CONTENT_MANAGEMENT" envVarPushEmptyLayer = "OCI_SKIP_EMPTY_LAYER_PUSH_TEST" envVarBlobDigest = "OCI_BLOB_DIGEST" + envVarBlob512Digest = "OCI_BLOB512_DIGEST" envVarManifestDigest = "OCI_MANIFEST_DIGEST" + envVarManifest512Digest = "OCI_MANIFEST512_DIGEST" envVarTagName = "OCI_TAG_NAME" + envVarTag512Name = "OCI_TAG512_NAME" envVarTagList = "OCI_TAG_LIST" envVarHideSkippedWorkflows = "OCI_HIDE_SKIPPED_WORKFLOWS" envVarAuthScope = "OCI_AUTH_SCOPE" @@ -79,7 +82,6 @@ const ( envVarReportDir = "OCI_REPORT_DIR" emptyLayerTestTag = "emptylayer" - testTagName = "tagtest0" titlePull = "Pull" titlePush = "Push" @@ -105,6 +107,8 @@ var ( envVarContentDiscovery: contentDiscovery, envVarContentManagement: contentManagement, } + testTagName = "tagtest0" + testTag512Name = "tagtest512" testBlobA []byte testBlobALength string @@ -128,6 +132,8 @@ var ( testBlobBChunk2Range string testAnnotationKey string testAnnotationValues map[string]string + testBlobs map[string][]*TestBlob + testManifests map[string]*TestBlob client *reggie.Client crossmountNamespace string dummyDigest string @@ -159,6 +165,7 @@ var ( testsToRun int suiteDescription string runPullSetup bool + runPull512Setup bool runPushSetup bool runContentDiscoverySetup bool runContentManagementSetup bool @@ -252,6 +259,65 @@ func init() { layerBlobDigest = layerBlobDigestRaw.String() layerBlobContentLength = fmt.Sprintf("%d", len(layerBlobData)) + testBlobs = map[string][]*TestBlob{} + testManifests = map[string]*TestBlob{} + layer512Dig := godigest.SHA512.FromBytes(layerBlobData) + testBlobs["sha512"] = []*TestBlob{ + { + Content: layerBlobData, + ContentLength: fmt.Sprintf("%d", len(layerBlobData)), + Digest: layer512Dig.String(), + }, + } + imgConf := image{ + Architecture: "amd64", + OS: "linux", + RootFS: rootFS{ + Type: "layers", + DiffIDs: []godigest.Digest{ + layer512Dig, + }, + }, + Author: randomString(16), + } + configBytes, err := json.MarshalIndent(imgConf, "", "\t") + if err != nil { + log.Fatal(err) + } + configDig := godigest.SHA512.FromBytes(configBytes) + testBlobs["sha512"] = append(testBlobs["sha512"], &TestBlob{ + Content: configBytes, + ContentLength: fmt.Sprintf("%d", len(configBytes)), + Digest: configDig.String(), + }) + imgMan := manifest{ + SchemaVersion: 2, + MediaType: "application/vnd.oci.image.manifest.v1+json", + Config: descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDig, + Size: int64(len(configBytes)), + Data: configBytes, // must be the config content. + NewUnspecifiedField: []byte("hello world"), // content doesn't matter. + }, + Layers: []descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + Size: int64(len(layerBlobData)), + Digest: layer512Dig, + }, + }, + } + imgManBytes, err := json.MarshalIndent(imgMan, "", "\t") + if err != nil { + log.Fatal(err) + } + testManifests["sha512"] = &TestBlob{ + Content: imgManBytes, + ContentLength: fmt.Sprintf("%d", imgManBytes), + Digest: godigest.SHA512.FromBytes(imgManBytes).String(), + } + layers := []descriptor{{ MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", Size: int64(len(layerBlobData)), @@ -544,6 +610,7 @@ func init() { } runPullSetup = true + runPull512Setup = true runPushSetup = true runContentDiscoverySetup = true runContentManagementSetup = true @@ -553,6 +620,16 @@ func init() { os.Getenv(envVarManifestDigest) != "" && os.Getenv(envVarBlobDigest) != "" { runPullSetup = false + testTagName = os.Getenv(envVarTagName) + } + + if os.Getenv(envVarTag512Name) != "" && + os.Getenv(envVarManifest512Digest) != "" && + os.Getenv(envVarBlob512Digest) != "" { + runPull512Setup = false + testTag512Name = os.Getenv(envVarTag512Name) + testManifests["sha512"].Digest = os.Getenv(envVarManifest512Digest) + testBlobs["sha512"][0].Digest = os.Getenv(envVarBlob512Digest) } if os.Getenv(envVarTagList) != "" { diff --git a/spec.md b/spec.md index 7a3a7aeb..f12aff96 100644 --- a/spec.md +++ b/spec.md @@ -224,6 +224,8 @@ When a manifest is rejected for this reason, it MUST result in one or more `MANI There are two ways to push blobs: chunked or monolithic. +In each implementation, if the client provided digest is invalid or uses an unsupported algorithm, the registry SHOULD respond with a response code `400 Bad Request`. + ##### Pushing a blob monolithically There are two ways to push a blob monolithically: @@ -329,6 +331,12 @@ The process remains unchanged for chunked upload, except that the post request M Content-Length: 0 ``` +When pushing a blob with a digest algorithm other than `sha256`, the post request SHOULD include the `digest-algorithm` parameter: + +`/v2//blobs/uploads/?digest-algorithm=` [end-4c](#endpoints) + +Here, `` is the algorithm the registry should use for the blob, e.g. `digest-algorithm=sha512`. + If the registry has a minimum chunk size, the `POST` response SHOULD include the following header, where `` is the size in bytes (see the blob `PATCH` definition for usage): ``` @@ -449,7 +457,7 @@ This indicates that the upload session has begun and that the client MAY proceed ##### Pushing Manifests -To push a manifest, perform a `PUT` request to a path in the following format, and with the following headers and body: `/v2//manifests/` [end-7](#endpoints) +To push a manifest, perform a `PUT` request to a path in the following format, and with the following headers and body: `/v2//manifests/` [end-7a](#endpoints) Clients SHOULD set the `Content-Type` header to the type of the manifest being pushed. The client SHOULD NOT include parameters on the `Content-Type` header (see [RFC7231](https://www.rfc-editor.org/rfc/rfc7231#section-3.1.1.1)). @@ -470,6 +478,12 @@ Manifest byte stream: `` is the namespace of the repository, and the `` MUST be either a) a digest or b) a tag. +When `` is a tag, the client may also provide the digest of the content via a `PUT` request in the following format: + +`/v2//manifests/?digest=` [end-7b](#endpoints) + +`` is the value of the tag and `` is the value of the digest being pushed. + The uploaded manifest MUST reference any blobs that make up the object. However, the list of blobs MAY be empty. @@ -483,6 +497,7 @@ Location: The `` is a pullable manifest URL. The Docker-Content-Digest header returns the canonical digest of the uploaded blob, and MUST be equal to the client provided digest. Clients MAY ignore the value but if it is used, the client SHOULD verify the value against the uploaded blob data. +If the client provided digest is invalid or uses an unsupported algorithm, the registry SHOULD respond with a response code `400 Bad Request`. An attempt to pull a nonexistent repository MUST return response code `404 Not Found`. @@ -761,9 +776,11 @@ This endpoint MAY be used for authentication/authorization purposes, but this is | end-3 | `GET` / `HEAD` | `/v2//manifests/` | `200` | `404` | | end-4a | `POST` | `/v2//blobs/uploads/` | `202` | `404` | | end-4b | `POST` | `/v2//blobs/uploads/?digest=` | `201`/`202` | `404`/`400` | +| end-4c | `POST` | `/v2//blobs/uploads/?digest-algorithm=` | `201`/`202` | `404`/`400` | | end-5 | `PATCH` | `/v2//blobs/uploads/` | `202` | `404`/`416` | | end-6 | `PUT` | `/v2//blobs/uploads/?digest=` | `201` | `404`/`400` | -| end-7 | `PUT` | `/v2//manifests/` | `201` | `404` | +| end-7a | `PUT` | `/v2//manifests/` | `201` | `404`/`400`/`413` | +| end-7b | `PUT` | `/v2//manifests/?digest=` | `201` | `404`/`400`/`413` | | end-8a | `GET` | `/v2//tags/list` | `200` | `404` | | end-8b | `GET` | `/v2//tags/list?n=&last=` | `200` | `404` | | end-9 | `DELETE` | `/v2//manifests/` | `202` | `404`/`400`/`405` |