From c0f93caacbac38c619ec98d05f811659678e59ce Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani <45800463+rchincha@users.noreply.github.com> Date: Tue, 8 Nov 2022 00:38:16 -0800 Subject: [PATCH] feat(artifact): add OCI references support (#936) Thanks @jdolitsky et al for kicking off these changes at: https://github.com/oci-playground/zot/commits/main Thanks @sudo-bmitch for reviewing the patch Signed-off-by: Ramkumar Chinchani --- Makefile | 3 +- README.md | 7 +- go.mod | 3 +- go.sum | 10 +- pkg/api/controller_test.go | 305 +++++++++++++++++- pkg/api/routes.go | 164 ++++++++-- .../gqlplayground/gqlplayground_disabled.go | 2 +- pkg/debug/swagger/swagger_disabled.go | 2 +- pkg/extensions/extension_sync.go | 4 +- pkg/extensions/extension_sync_disabled.go | 2 +- pkg/extensions/search/common/common_test.go | 24 +- pkg/extensions/search/common/oci_layout.go | 2 +- pkg/extensions/search/resolver_test.go | 6 +- pkg/extensions/sync/on_demand.go | 8 +- pkg/extensions/sync/signatures.go | 2 +- pkg/extensions/sync/sync_test.go | 91 +++++- pkg/storage/common.go | 35 +- pkg/storage/constants/constants.go | 26 +- pkg/storage/local/local.go | 141 +++++++- pkg/storage/local/local_test.go | 16 +- pkg/storage/s3/s3.go | 7 +- pkg/storage/s3/s3_test.go | 12 + pkg/storage/storage.go | 4 +- pkg/test/common.go | 31 +- pkg/test/mocks/image_store_mock.go | 21 +- test/blackbox/pushpull.bats | 55 +++- 26 files changed, 865 insertions(+), 118 deletions(-) diff --git a/Makefile b/Makefile index f1a45552..4a1fb045 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ COSIGN := $(TOOLSDIR)/bin/cosign HELM := $(TOOLSDIR)/bin/helm ORAS := $(TOOLSDIR)/bin/oras REGCLIENT := $(TOOLSDIR)/bin/regctl +REGCLIENT_VERSION := v0.4.5 STACKER := $(TOOLSDIR)/bin/stacker BATS := $(TOOLSDIR)/bin/bats TESTDATA := $(TOP_LEVEL)/test/data @@ -121,7 +122,7 @@ $(HELM): $(REGCLIENT): mkdir -p $(TOOLSDIR)/bin - curl -Lo regctl https://github.com/regclient/regclient/releases/download/v0.4.4/regctl-linux-amd64 + curl -Lo regctl https://github.com/regclient/regclient/releases/download/$(REGCLIENT_VERSION)/regctl-linux-amd64 cp regctl $(TOOLSDIR)/bin/regctl chmod +x $(TOOLSDIR)/bin/regctl diff --git a/README.md b/README.md index d94a7da2..fca5e0bd 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,14 @@ The following document refers on the **core dist-spec**, see also the [zot-speci ## [**Why zot?**](COMPARISON.md) ## What's new? -* Support content range for pull requests +* Supports push/pull OCI and ORAS Artifacts +* Supports OCI references +* Supports content range for pull requests * Selectively add extensions on top of minimal build * Supports container image signatures - [cosign](https://github.com/sigstore/cosign) and [notation](https://github.com/notaryproject/notation) * Multi-arch support * Clustering support * Image linting support -* Supports push/pull OCI Artifacts ## [Demos](demos/README.md) @@ -290,7 +291,7 @@ Supports: You can benchmark a zot registry or any other dist-spec conformant registry with `zb`. -## Building `zb`` +## Building `zb` ```console $ make bench diff --git a/go.mod b/go.mod index e5d39715..f173dfaf 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba github.com/olekukonko/tablewriter v0.0.5 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0-rc2 + github.com/opencontainers/image-spec v1.1.0-rc2.0.20221020182949-4df8887994e8 github.com/opencontainers/umoci v0.4.8-0.20210922062158-e60a0cc726e6 github.com/oras-project/artifacts-spec v1.0.0-rc.2 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 @@ -413,7 +413,6 @@ replace ( github.com/containers/image/v5 => github.com/anuvu/image/v5 v5.0.0-20220520105616-e594853d6471 github.com/hashicorp/go-getter => github.com/hashicorp/go-getter v1.6.1 github.com/open-policy-agent/opa => github.com/open-policy-agent/opa v0.44.0 - github.com/opencontainers/image-spec => github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5 github.com/opencontainers/runc => github.com/opencontainers/runc v1.1.2 go.etcd.io/etcd/v3 => go.etcd.io/etcd/v3 v3.5.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.26.1 diff --git a/go.sum b/go.sum index 7d94c1a3..f4052f4f 100644 --- a/go.sum +++ b/go.sum @@ -1839,8 +1839,15 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5 h1:q37d91F6BO4Jp1UqWiun0dUFYaqv6WsKTLTCaWv+8LY= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84/go.mod h1:Qnt1q4cjDNQI9bT832ziho5Iw2BhK8o1KwLOwW56VP4= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221020182949-4df8887994e8 h1:l9vfzobI7tZtG164u1Jf6NqDErHZoqAw8rlvBYQJpVI= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221020182949-4df8887994e8/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opencontainers/runc v1.1.2 h1:2VSZwLx5k/BfsBxMMipG/LYUnmqOD/BPkIVgQUcTlLw= github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -2030,6 +2037,7 @@ github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 1c9a3dbd..7c4c7ae5 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -4060,7 +4060,7 @@ func TestImageSignatures(t *testing.T) { resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) out, err = cmd.CombinedOutput() So(err, ShouldNotBeNil) @@ -4084,7 +4084,7 @@ func TestImageSignatures(t *testing.T) { resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) out, err = cmd.CombinedOutput() So(err, ShouldNotBeNil) @@ -4093,7 +4093,7 @@ func TestImageSignatures(t *testing.T) { }) }) - Convey("GetReferrers", func() { + Convey("GetOrasReferrers", func() { // cover error paths resp, err := resty.R().Get( fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", "badDigest")) @@ -4123,6 +4123,301 @@ func TestImageSignatures(t *testing.T) { }) } +func TestArtifactReferences(t *testing.T) { + Convey("Validate Artifact References", t, func() { + // start a new server + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + dir := t.TempDir() + ctlr.Config.Storage.RootDirectory = dir + go func(controller *api.Controller) { + // this blocks + if err := controller.Run(context.Background()); err != nil { + return + } + }(ctlr) + // wait till ready + for { + _, err := resty.R().Get(baseURL) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + defer func(controller *api.Controller) { + ctx := context.Background() + _ = controller.Server.Shutdown(ctx) + }(ctlr) + + repoName := "artifact-repo" + + // create a blob/layer + resp, err := resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc := test.Location(baseURL, resp) + So(loc, ShouldNotBeEmpty) + + resp, err = resty.R().Get(loc) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 204) + content := []byte("this is a blob") + digest := godigest.FromBytes(content) + So(digest, ShouldNotBeNil) + + // monolithic blob upload: success + resp, err = resty.R().SetQueryParam("digest", digest.String()). + SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + blobLoc := resp.Header().Get("Location") + So(blobLoc, ShouldNotBeEmpty) + So(resp.Header().Get("Content-Length"), ShouldEqual, "0") + So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) + + // upload image config blob + resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = test.Location(baseURL, resp) + cblob, cdigest := test.GetRandomImageConfig() + + resp, err = resty.R(). + SetContentLength(true). + SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", cdigest.String()). + SetBody(cblob). + Put(loc) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // create a manifest + manifest := ispec.Manifest{ + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest, + Size: int64(len(content)), + }, + }, + } + manifest.SchemaVersion = 2 + content, err = json.Marshal(manifest) + 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 + fmt.Sprintf("/v2/%s/manifests/1.0", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + d := resp.Header().Get(constants.DistContentDigestKey) + So(d, ShouldNotBeEmpty) + So(d, ShouldEqual, digest.String()) + + artifactType := "application/vnd.example.icecream.v1" + + Convey("Validate Image Manifest Reference", func() { + resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + // now upload a reference + + // upload image config blob + resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = test.Location(baseURL, resp) + cblob, cdigest := test.GetEmptyImageConfig() + + resp, err = resty.R(). + SetContentLength(true). + SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", cdigest.String()). + SetBody(cblob). + Put(loc) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // create a manifest + manifest := ispec.Manifest{ + Config: ispec.Descriptor{ + MediaType: artifactType, + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest, + Size: int64(len(content)), + }, + }, + Subject: &ispec.Descriptor{ + MediaType: ispec.MediaTypeImageManifest, + Digest: digest, + Size: int64(len(content)), + }, + Annotations: map[string]string{ + "key": "val", + }, + } + manifest.SchemaVersion = 2 + + Convey("Using invalid content", func() { + resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest). + SetBody([]byte("invalid data")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + + resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) + Convey("Using valid content", func() { + content, err = json.Marshal(manifest) + So(err, ShouldBeNil) + resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest). + SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifact": "invalid"}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": "invalid"}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex) + }) + }) + + Convey("Validate Artifact Manifest Reference", func() { + resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + // now upload a reference + + // upload image config blob + resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName)) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = test.Location(baseURL, resp) + cblob, cdigest := test.GetEmptyImageConfig() + + resp, err = resty.R(). + SetContentLength(true). + SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", cdigest.String()). + SetBody(cblob). + Put(loc) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // create a artifact + manifest := ispec.Artifact{ + MediaType: ispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Blobs: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest, + Size: int64(len(content)), + }, + }, + Subject: &ispec.Descriptor{ + MediaType: ispec.MediaTypeImageManifest, + Digest: digest, + Size: int64(len(content)), + }, + Annotations: map[string]string{ + "key": "val", + }, + } + Convey("Using invalid content", func() { + content := []byte("invalid data") + So(err, ShouldBeNil) + mdigest := godigest.FromBytes(content) + So(mdigest, ShouldNotBeNil) + resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeArtifactManifest). + SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, mdigest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + + resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) + Convey("Using valid content", func() { + content, err = json.Marshal(manifest) + So(err, ShouldBeNil) + mdigest := godigest.FromBytes(content) + So(mdigest, ShouldNotBeNil) + resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeArtifactManifest). + SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, mdigest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifact": "invalid"}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": "invalid"}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}). + Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String())) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex) + }) + }) + }) +} + //nolint:dupl // duplicated test code func TestRouteFailures(t *testing.T) { Convey("Make a new controller", t, func() { @@ -4685,7 +4980,7 @@ func TestRouteFailures(t *testing.T) { request = mux.SetURLVars(request, map[string]string{}) response := httptest.NewRecorder() - rthdlr.GetReferrers(response, request) + rthdlr.GetOrasReferrers(response, request) resp := response.Result() defer resp.Body.Close() @@ -4696,7 +4991,7 @@ func TestRouteFailures(t *testing.T) { request = mux.SetURLVars(request, map[string]string{"name": "foo"}) response = httptest.NewRecorder() - rthdlr.GetReferrers(response, request) + rthdlr.GetOrasReferrers(response, request) resp = response.Result() defer resp.Body.Close() diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 30d6e826..901d649b 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -8,6 +8,8 @@ package api import ( + "context" + "encoding/json" "errors" "fmt" "io" @@ -96,6 +98,9 @@ func (rh *RouteHandler) SetupRoutes() { rh.UpdateBlobUpload).Methods("PUT") prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()), rh.DeleteBlobUpload).Methods("DELETE") + // support for OCI artifact references + prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/referrers/{digest}", NameRegexp.String()), + rh.GetReferrers).Methods(allowedMethods("GET")...) prefixedRouter.HandleFunc(constants.ExtCatalogPrefix, rh.ListRepositories).Methods(allowedMethods("GET")...) prefixedRouter.HandleFunc(constants.ExtOciDiscoverPrefix, @@ -104,9 +109,9 @@ func (rh *RouteHandler) SetupRoutes() { rh.CheckVersionSupport).Methods(allowedMethods("GET")...) } - // support for oras artifact reference types (alpha 1) - image signature use case + // support for ORAS artifact reference types (alpha 1) - image signature use case rh.c.Router.HandleFunc(fmt.Sprintf("%s/{name:%s}/manifests/{digest}/referrers", - constants.ArtifactSpecRoutePrefix, NameRegexp.String()), rh.GetReferrers).Methods("GET") + constants.ArtifactSpecRoutePrefix, NameRegexp.String()), rh.GetOrasReferrers).Methods("GET") // swagger debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, rh.c.Log) @@ -310,7 +315,8 @@ func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *htt return } - content, digest, mediaType, err := getImageManifest(rh, imgStore, name, reference) //nolint:contextcheck + content, digest, mediaType, err := getImageManifest(request.Context(), rh, imgStore, + name, reference) //nolint:contextcheck if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain WriteJSON(response, http.StatusNotFound, @@ -375,7 +381,8 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http. return } - content, digest, mediaType, err := getImageManifest(rh, imgStore, name, reference) //nolint: contextcheck + content, digest, mediaType, err := getImageManifest(request.Context(), rh, + imgStore, name, reference) //nolint: contextcheck if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain WriteJSON(response, http.StatusNotFound, @@ -398,6 +405,117 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http. WriteData(response, http.StatusOK, mediaType, content) } +type ImageIndex struct { + ispec.Index +} + +func getReferrers(ctx context.Context, routeHandler *RouteHandler, + imgStore storage.ImageStore, name string, digest godigest.Digest, + artifactType string, +) (ispec.Index, error) { + // first get the subject and then all its referrers + references, err := imgStore.GetReferrers(name, digest, artifactType) + if err != nil { + if routeHandler.c.Config.Extensions != nil && + routeHandler.c.Config.Extensions.Sync != nil && + *routeHandler.c.Config.Extensions.Sync.Enable { + routeHandler.c.Log.Info().Msgf("referrers not found, trying to get referrers to %s:%s by syncing on demand", + name, digest) + + errSync := ext.SyncOneImage(ctx, routeHandler.c.Config, routeHandler.c.StoreController, + name, digest.String(), false, routeHandler.c.Log) + if errSync != nil { + routeHandler.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") + + return ispec.Index{}, err + } + + for _, ref := range references.Manifests { + errSync := ext.SyncOneImage(ctx, routeHandler.c.Config, routeHandler.c.StoreController, + name, ref.Digest.String(), false, routeHandler.c.Log) + if errSync != nil { + routeHandler.c.Log.Error().Err(err).Str("name", name). + Str("digest", ref.Digest.String()).Msg("unable to get references") + + return ispec.Index{}, err + } + } + + references, err = imgStore.GetReferrers(name, digest, artifactType) + } + } + + return references, err +} + +// GetReferrers godoc +// @Summary Get references for a given digest +// @Description Get references given a digest +// @Accept json +// @Produce application/vnd.oci.image.index.v1+json +// @Param name path string true "repository name" +// @Param digest path string true "digest" +// @Success 200 {object} api.ImageIndex +// @Failure 404 {string} string "not found" +// @Failure 500 {string} string "internal server error" +// @Router /v2/{name}/references/{digest} [get]. +func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + + name, ok := vars["name"] + if !ok || name == "" { + response.WriteHeader(http.StatusNotFound) + + return + } + + digestStr, ok := vars["digest"] + digest, err := godigest.Parse(digestStr) + + if !ok || digestStr == "" || err != nil { + response.WriteHeader(http.StatusBadRequest) + + return + } + + // filter by artifact type + artifactType := "" + + artifactTypes, ok := request.URL.Query()["artifactType"] + if ok { + if len(artifactTypes) != 1 { + rh.c.Log.Error().Msg("invalid artifact types") + response.WriteHeader(http.StatusBadRequest) + + return + } + + artifactType = artifactTypes[0] + } + + rh.c.Log.Info().Str("digest", digest.String()).Str("artifactType", artifactType).Msg("getting manifest") + + imgStore := rh.getImageStore(name) + + referrers, err := getReferrers(request.Context(), rh, imgStore, name, digest, artifactType) + if err != nil { + rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") + response.WriteHeader(http.StatusNotFound) + + return + } + + out, err := json.Marshal(referrers) + if err != nil { + rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to marshal json") + response.WriteHeader(http.StatusInternalServerError) + + return + } + + WriteData(response, http.StatusOK, ispec.MediaTypeImageIndex, out) +} + // UpdateManifest godoc // @Summary Update image manifest // @Description Update an image's manifest given a reference or a digest @@ -1458,7 +1576,7 @@ func (rh *RouteHandler) getImageStore(name string) storage.ImageStore { } // will sync on demand if an image is not found, in case sync extensions is enabled. -func getImageManifest(routeHandler *RouteHandler, imgStore storage.ImageStore, name, +func getImageManifest(ctx context.Context, routeHandler *RouteHandler, imgStore storage.ImageStore, name, reference string, ) ([]byte, godigest.Digest, string, error) { content, digest, mediaType, err := imgStore.GetImageManifest(name, reference) @@ -1470,7 +1588,7 @@ func getImageManifest(routeHandler *RouteHandler, imgStore storage.ImageStore, n routeHandler.c.Log.Info().Msgf("image not found, trying to get image %s:%s by syncing on demand", name, reference) - errSync := ext.SyncOneImage(routeHandler.c.Config, routeHandler.c.StoreController, + errSync := ext.SyncOneImage(ctx, routeHandler.c.Config, routeHandler.c.StoreController, name, reference, false, routeHandler.c.Log) if errSync != nil { routeHandler.c.Log.Err(errSync).Msgf("error encounter while syncing image %s:%s", @@ -1488,10 +1606,11 @@ func getImageManifest(routeHandler *RouteHandler, imgStore storage.ImageStore, n } // will sync referrers on demand if they are not found, in case sync extensions is enabled. -func getReferrers(routeHandler *RouteHandler, imgStore storage.ImageStore, name string, digest godigest.Digest, +func getOrasReferrers(ctx context.Context, routeHandler *RouteHandler, + imgStore storage.ImageStore, name string, digest godigest.Digest, artifactType string, ) ([]artifactspec.Descriptor, error) { - refs, err := imgStore.GetReferrers(name, digest, artifactType) + refs, err := imgStore.GetOrasReferrers(name, digest, artifactType) if err != nil { if routeHandler.c.Config.Extensions != nil && routeHandler.c.Config.Extensions.Sync != nil && @@ -1499,7 +1618,7 @@ func getReferrers(routeHandler *RouteHandler, imgStore storage.ImageStore, name routeHandler.c.Log.Info().Msgf("signature not found, trying to get signature %s:%s by syncing on demand", name, digest.String()) - errSync := ext.SyncOneImage(routeHandler.c.Config, routeHandler.c.StoreController, + errSync := ext.SyncOneImage(ctx, routeHandler.c.Config, routeHandler.c.StoreController, name, digest.String(), true, routeHandler.c.Log) if errSync != nil { routeHandler.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") @@ -1507,7 +1626,7 @@ func getReferrers(routeHandler *RouteHandler, imgStore storage.ImageStore, name return []artifactspec.Descriptor{}, err } - refs, err = imgStore.GetReferrers(name, digest, artifactType) + refs, err = imgStore.GetOrasReferrers(name, digest, artifactType) } } @@ -1518,7 +1637,7 @@ type ReferenceList struct { References []artifactspec.Descriptor `json:"references"` } -// GetReferrers godoc +// GetOrasReferrers godoc // @Summary Get references for an image // @Description Get references for an image given a digest and artifact type // @Accept json @@ -1530,7 +1649,7 @@ type ReferenceList struct { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /oras/artifacts/v1/{name:%s}/manifests/{digest}/referrers [get]. -func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http.Request) { +func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) name, ok := vars["name"] @@ -1549,16 +1668,21 @@ func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http return } + // filter by artifact type + artifactType := "" + artifactTypes, ok := request.URL.Query()["artifactType"] - if !ok || len(artifactTypes) != 1 { - rh.c.Log.Error().Msg("invalid artifact types") - response.WriteHeader(http.StatusBadRequest) + if ok { + if len(artifactTypes) != 1 { + rh.c.Log.Error().Msg("invalid artifact types") + response.WriteHeader(http.StatusBadRequest) - return + return + } + + artifactType = artifactTypes[0] } - artifactType := artifactTypes[0] - if artifactType != notreg.ArtifactTypeNotation { rh.c.Log.Error().Str("artifactType", artifactType).Msg("invalid artifact type") response.WriteHeader(http.StatusBadRequest) @@ -1570,10 +1694,10 @@ func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http rh.c.Log.Info().Str("digest", digest.String()).Str("artifactType", artifactType).Msg("getting manifest") - refs, err := getReferrers(rh, imgStore, name, digest, artifactType) //nolint:contextcheck + refs, err := getOrasReferrers(request.Context(), rh, imgStore, name, digest, artifactType) //nolint:contextcheck if err != nil { rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") - response.WriteHeader(http.StatusBadRequest) + response.WriteHeader(http.StatusNotFound) return } diff --git a/pkg/debug/gqlplayground/gqlplayground_disabled.go b/pkg/debug/gqlplayground/gqlplayground_disabled.go index b1cf4367..ba6dfd9a 100644 --- a/pkg/debug/gqlplayground/gqlplayground_disabled.go +++ b/pkg/debug/gqlplayground/gqlplayground_disabled.go @@ -15,6 +15,6 @@ import ( func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController, log log.Logger, ) { - log.Warn().Msg("skipping enabling graphql playground extension because given zot binary" + + log.Warn().Msg("skipping enabling graphql playground extension because given zot binary " + "doesn't include this feature, please build a binary that does so") } diff --git a/pkg/debug/swagger/swagger_disabled.go b/pkg/debug/swagger/swagger_disabled.go index c58fe4f4..4fa187de 100644 --- a/pkg/debug/swagger/swagger_disabled.go +++ b/pkg/debug/swagger/swagger_disabled.go @@ -19,6 +19,6 @@ import ( func SetupSwaggerRoutes(conf *config.Config, router *mux.Router, log log.Logger, ) { // swagger swagger "/swagger/v2/index.html" - log.Warn().Msg("skipping enabling swagger because given zot binary" + + log.Warn().Msg("skipping enabling swagger because given zot binary " + "doesn't include this feature, please build a binary that does so") } diff --git a/pkg/extensions/extension_sync.go b/pkg/extensions/extension_sync.go index 864ba046..40d5e751 100644 --- a/pkg/extensions/extension_sync.go +++ b/pkg/extensions/extension_sync.go @@ -25,12 +25,12 @@ func EnableSyncExtension(ctx context.Context, config *config.Config, wg *goSync. } } -func SyncOneImage(config *config.Config, storeController storage.StoreController, +func SyncOneImage(ctx context.Context, config *config.Config, storeController storage.StoreController, repoName, reference string, isArtifact bool, log log.Logger, ) error { log.Info().Msgf("syncing image %s:%s", repoName, reference) - err := sync.OneImage(*config.Extensions.Sync, storeController, repoName, reference, isArtifact, log) + err := sync.OneImage(ctx, *config.Extensions.Sync, storeController, repoName, reference, isArtifact, log) return err } diff --git a/pkg/extensions/extension_sync_disabled.go b/pkg/extensions/extension_sync_disabled.go index 1ddb2fad..524cf0d9 100644 --- a/pkg/extensions/extension_sync_disabled.go +++ b/pkg/extensions/extension_sync_disabled.go @@ -22,7 +22,7 @@ func EnableSyncExtension(ctx context.Context, } // SyncOneImage ... -func SyncOneImage(config *config.Config, storeController storage.StoreController, +func SyncOneImage(ctx context.Context, config *config.Config, storeController storage.StoreController, repoName, reference string, isArtifact bool, log log.Logger, ) error { log.Warn().Msg("skipping syncing on demand because given zot binary doesn't include this feature," + diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index d7e74746..67fdaa08 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -1101,8 +1101,10 @@ func TestDerivedImageList(t *testing.T) { Convey("Test dependency list for image working", t, func() { // create test images config := ispec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: ispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, @@ -1516,8 +1518,10 @@ func TestBaseImageList(t *testing.T) { Convey("Test base image list for image working", t, func() { // create test images config := ispec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: ispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, @@ -2502,8 +2506,10 @@ func TestImageList(t *testing.T) { WaitTillServerReady(baseURL) config := ispec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: ispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, @@ -2619,8 +2625,10 @@ func TestBuildImageInfo(t *testing.T) { } config := ispec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: ispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index 30371a49..f15e341a 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -201,7 +201,7 @@ func (olu BaseOciLayoutUtils) checkNotarySignature(name string, digest godigest. imageStore := olu.StoreController.GetImageStore(name) mediaType := notreg.ArtifactTypeNotation - _, err := imageStore.GetReferrers(name, digest, mediaType) + _, err := imageStore.GetOrasReferrers(name, digest, mediaType) if err != nil { olu.Log.Info().Err(err).Str("repo", name).Str("digest", digest.String()).Str("mediatype", mediaType).Msg("invalid notary signature") diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 8d8be3fc..b8c4cb80 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -322,8 +322,10 @@ func TestExtractImageDetails(t *testing.T) { testLogger := log.NewLogger("debug", "") layerDigest := godigest.FromBytes(content) config := ispec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: ispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, diff --git a/pkg/extensions/sync/on_demand.go b/pkg/extensions/sync/on_demand.go index abe5eede..f4d22866 100644 --- a/pkg/extensions/sync/on_demand.go +++ b/pkg/extensions/sync/on_demand.go @@ -50,7 +50,7 @@ func (di *demandedImages) delete(key string) { di.syncedMap.Delete(key) } -func OneImage(cfg Config, storeController storage.StoreController, +func OneImage(ctx context.Context, cfg Config, storeController storage.StoreController, repo, reference string, isArtifact bool, log log.Logger, ) error { // guard against multiple parallel requests @@ -73,7 +73,7 @@ func OneImage(cfg Config, storeController storage.StoreController, defer demandedImgs.delete(demandedImage) defer close(imageChannel) - go syncOneImage(imageChannel, cfg, storeController, repo, reference, isArtifact, log) + go syncOneImage(ctx, imageChannel, cfg, storeController, repo, reference, isArtifact, log) err, ok := <-imageChannel if !ok { @@ -83,7 +83,7 @@ func OneImage(cfg Config, storeController storage.StoreController, return err } -func syncOneImage(imageChannel chan error, cfg Config, storeController storage.StoreController, +func syncOneImage(ctx context.Context, imageChannel chan error, cfg Config, storeController storage.StoreController, localRepo, reference string, isArtifact bool, log log.Logger, ) { var credentialsFile CredentialsFile @@ -248,7 +248,7 @@ func syncOneImage(imageChannel chan error, cfg Config, storeController storage.S demandedImageRef, copyErr) time.Sleep(retryOptions.Delay) - if err = retry.RetryIfNecessary(context.Background(), func() error { + if err = retry.RetryIfNecessary(ctx, func() error { _, err := syncRun(regCfg, localRepo, upstreamRepo, reference, syncContextUtils, sig, log) return err diff --git a/pkg/extensions/sync/signatures.go b/pkg/extensions/sync/signatures.go index 9313e19c..8c1b9869 100644 --- a/pkg/extensions/sync/signatures.go +++ b/pkg/extensions/sync/signatures.go @@ -340,7 +340,7 @@ func (sig *signaturesCopier) canSkipNotarySignature(localRepo, digestStr string, // check notary signature already synced if len(refs.References) > 0 { - localRefs, err := imageStore.GetReferrers(localRepo, digest, notreg.ArtifactTypeNotation) + localRefs, err := imageStore.GetOrasReferrers(localRepo, digest, notreg.ArtifactTypeNotation) if err != nil { if errors.Is(err, zerr.ErrManifestNotFound) { return false, nil diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index 93d61d00..7962320c 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -26,7 +26,7 @@ import ( notreg "github.com/notaryproject/notation-go/registry" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + oraspec "github.com/oras-project/artifacts-spec/specs-go/v1" perr "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" @@ -2404,7 +2404,7 @@ func TestPeriodicallySignaturesErr(t *testing.T) { }) Convey("Trigger error on notary signature", func() { - // trigger permission error on cosign signature on upstream + // trigger permission error on notary signature on upstream notaryURLPath := path.Join("/oras/artifacts/v1/", repoName, "manifests", imageManifestDigest.String(), "referrers") // based on image manifest digest get referrers @@ -2422,7 +2422,7 @@ func TestPeriodicallySignaturesErr(t *testing.T) { So(err, ShouldBeNil) // read manifest - var artifactManifest artifactspec.Manifest + var artifactManifest oraspec.Manifest for _, ref := range referrers.References { refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) body, err := os.ReadFile(refPath) @@ -2450,6 +2450,53 @@ func TestPeriodicallySignaturesErr(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 400) }) + + Convey("Trigger error on artifact references", func() { + // trigger permission denied on image manifest + manifestPath := path.Join(srcDir, repoName, "blobs", + string(imageManifestDigest.Algorithm()), imageManifestDigest.Encoded()) + err = os.Chmod(manifestPath, 0o000) + So(err, ShouldBeNil) + + // trigger permission error on upstream + artifactURLPath := path.Join("/v2", repoName, "referrers", imageManifestDigest.String()) + + // based on image manifest digest get referrers + resp, err := resty.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("artifactType", "application/vnd.cncf.icecream"). + Get(srcBaseURL + artifactURLPath) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + + var referrers ispec.Index + + err = json.Unmarshal(resp.Body(), &referrers) + So(err, ShouldBeNil) + + // read manifest + for _, ref := range referrers.Manifests { + refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) + _, err = os.ReadFile(refPath) + So(err, ShouldBeNil) + + // triggers perm denied on artifact blobs + err = os.Chmod(refPath, 0o000) + So(err, ShouldBeNil) + } + + // start downstream server + dctlr, destBaseURL, _, _ := startDownstreamServer(t, false, syncConfig) + defer dctlr.Shutdown() + + time.Sleep(2 * time.Second) + + // should not be synced nor sync on demand + resp, err = resty.R().Get(destBaseURL + artifactURLPath) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 404) + }) }) } @@ -2590,7 +2637,7 @@ func TestSignatures(t *testing.T) { err = os.RemoveAll(path.Join(destDir, repoName)) So(err, ShouldBeNil) - var artifactManifest artifactspec.Manifest + var artifactManifest oraspec.Manifest for _, ref := range referrers.References { refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) body, err := os.ReadFile(refPath) @@ -4405,6 +4452,42 @@ func pushRepo(url, repoName string) godigest.Digest { panic(err) } + // push a referrer artifact + manifest = ispec.Manifest{ + Config: ispec.Descriptor{ + MediaType: "application/vnd.cncf.icecream", + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest, + Size: int64(len(content)), + }, + }, + Subject: &ispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest, + Size: int64(len(content)), + }, + } + + manifest.SchemaVersion = 2 + + content, err = json.Marshal(manifest) + if err != nil { + panic(err) + } + + adigest := godigest.FromBytes(content) + + _, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(content).Put(url + fmt.Sprintf("/v2/%s/manifests/%s", repoName, adigest.String())) + if err != nil { + panic(err) + } + return digest } diff --git a/pkg/storage/common.go b/pkg/storage/common.go index 11486974..d9cc5e45 100644 --- a/pkg/storage/common.go +++ b/pkg/storage/common.go @@ -8,7 +8,7 @@ import ( "github.com/notaryproject/notation-go" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + oras "github.com/oras-project/artifacts-spec/specs-go/v1" "github.com/rs/zerolog" "github.com/sigstore/cosign/pkg/oci/remote" @@ -63,7 +63,8 @@ func ValidateManifest(imgStore ImageStore, repo, reference, mediaType string, bo return "", zerr.ErrBadManifest } - if mediaType == ispec.MediaTypeImageManifest { + switch mediaType { + case ispec.MediaTypeImageManifest: var manifest ispec.Manifest if err := json.Unmarshal(body, &manifest); err != nil { log.Error().Err(err).Msg("unable to unmarshal JSON") @@ -79,13 +80,38 @@ func ValidateManifest(imgStore ImageStore, repo, reference, mediaType string, bo return digest, err } } - } else if mediaType == artifactspec.MediaTypeArtifactManifest { + + if manifest.Subject != nil { + var m ispec.Descriptor + if err := json.Unmarshal(body, &m); err != nil { + log.Error().Err(err).Msg("unable to unmarshal JSON") + + return "", zerr.ErrBadManifest + } + } + case oras.MediaTypeArtifactManifest: var m notation.Descriptor if err := json.Unmarshal(body, &m); err != nil { log.Error().Err(err).Msg("unable to unmarshal JSON") return "", zerr.ErrBadManifest } + case ispec.MediaTypeArtifactManifest: + var artifact ispec.Artifact + if err := json.Unmarshal(body, &artifact); err != nil { + log.Error().Err(err).Msg("unable to unmarshal JSON") + + return "", zerr.ErrBadManifest + } + + if artifact.Subject != nil { + var m ispec.Descriptor + if err := json.Unmarshal(body, &m); err != nil { + log.Error().Err(err).Msg("unable to unmarshal JSON") + + return "", zerr.ErrBadManifest + } + } } return "", nil @@ -423,5 +449,6 @@ func ApplyLinter(imgStore ImageStore, linter Lint, repo string, manifestDesc isp func IsSupportedMediaType(mediaType string) bool { return mediaType == ispec.MediaTypeImageIndex || mediaType == ispec.MediaTypeImageManifest || - mediaType == artifactspec.MediaTypeArtifactManifest + mediaType == ispec.MediaTypeArtifactManifest || + mediaType == oras.MediaTypeArtifactManifest } diff --git a/pkg/storage/constants/constants.go b/pkg/storage/constants/constants.go index d38fd484..f4355a3e 100644 --- a/pkg/storage/constants/constants.go +++ b/pkg/storage/constants/constants.go @@ -6,16 +6,18 @@ import ( const ( // BlobUploadDir defines the upload directory for blob uploads. - BlobUploadDir = ".uploads" - SchemaVersion = 2 - DefaultFilePerms = 0o600 - DefaultDirPerms = 0o700 - RLOCK = "RLock" - RWLOCK = "RWLock" - BlobsCache = "blobs" - DuplicatesBucket = "duplicates" - OriginalBucket = "original" - DBExtensionName = ".db" - DBCacheLockCheckTimeout = 10 * time.Second - BoltdbName = "cache" + BlobUploadDir = ".uploads" + SchemaVersion = 2 + DefaultFilePerms = 0o600 + DefaultDirPerms = 0o700 + RLOCK = "RLock" + RWLOCK = "RWLock" + BlobsCache = "blobs" + DuplicatesBucket = "duplicates" + OriginalBucket = "original" + DBExtensionName = ".db" + DBCacheLockCheckTimeout = 10 * time.Second + BoltdbName = "cache" + ReferrerFilterAnnotation = "org.opencontainers.references.filtersApplied" + // ) diff --git a/pkg/storage/local/local.go b/pkg/storage/local/local.go index b61562cb..838c5ef8 100644 --- a/pkg/storage/local/local.go +++ b/pkg/storage/local/local.go @@ -20,10 +20,11 @@ import ( guuid "github.com/gofrs/uuid" "github.com/minio/sha256-simd" godigest "github.com/opencontainers/go-digest" + imeta "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/umoci" "github.com/opencontainers/umoci/oci/casext" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + oras "github.com/oras-project/artifacts-spec/specs-go/v1" "github.com/rs/zerolog" zerr "zotregistry.io/zot/errors" @@ -37,8 +38,9 @@ import ( ) const ( - DefaultFilePerms = 0o600 - DefaultDirPerms = 0o700 + DefaultFilePerms = 0o600 + DefaultDirPerms = 0o700 + defaultSchemaVersion = 2 ) // ImageStoreLocal provides the image storage operations. @@ -186,8 +188,7 @@ func (is *ImageStoreLocal) initRepo(name string) error { // "index.json" file - create if it doesn't exist indexPath := path.Join(repoDir, "index.json") if _, err := os.Stat(indexPath); err != nil { - index := ispec.Index{} - index.SchemaVersion = 2 + index := ispec.Index{Versioned: imeta.Versioned{SchemaVersion: defaultSchemaVersion}} buf, err := json.Marshal(index) if err != nil { @@ -1339,7 +1340,120 @@ func (is *ImageStoreLocal) DeleteBlob(repo string, digest godigest.Digest) error } func (is *ImageStoreLocal) GetReferrers(repo string, gdigest godigest.Digest, artifactType string, -) ([]artifactspec.Descriptor, error) { +) (ispec.Index, error) { + var lockLatency time.Time + + nilIndex := ispec.Index{} + + if err := gdigest.Validate(); err != nil { + return nilIndex, err + } + + dir := path.Join(is.rootDir, repo) + if !is.DirExists(dir) { + return nilIndex, zerr.ErrRepoNotFound + } + + index, err := storage.GetIndex(is, repo, is.log) + if err != nil { + return nilIndex, err + } + + is.RLock(&lockLatency) + defer is.RUnlock(&lockLatency) + + found := false + + result := []ispec.Descriptor{} + + for _, manifest := range index.Manifests { + if manifest.Digest == gdigest { + continue + } + + p := path.Join(dir, "blobs", manifest.Digest.Algorithm().String(), manifest.Digest.Encoded()) + + buf, err := os.ReadFile(p) + if err != nil { + is.log.Error().Err(err).Str("blob", p).Msg("failed to read manifest") + + if os.IsNotExist(err) { + return nilIndex, zerr.ErrManifestNotFound + } + + return nilIndex, err + } + + if manifest.MediaType == ispec.MediaTypeImageManifest { + var mfst ispec.Manifest + if err := json.Unmarshal(buf, &mfst); err != nil { + return nilIndex, err + } + + if mfst.Subject == nil || mfst.Subject.Digest != gdigest { + continue + } + + // filter by artifact type + if artifactType != "" && mfst.Config.MediaType != artifactType { + continue + } + + result = append(result, ispec.Descriptor{ + MediaType: manifest.MediaType, + ArtifactType: mfst.Config.MediaType, + Size: manifest.Size, + Digest: manifest.Digest, + Annotations: mfst.Annotations, + }) + } else if manifest.MediaType == ispec.MediaTypeArtifactManifest { + var art ispec.Artifact + if err := json.Unmarshal(buf, &art); err != nil { + return nilIndex, err + } + + if art.Subject == nil || art.Subject.Digest != gdigest { + continue + } + + // filter by artifact type + if artifactType != "" && art.ArtifactType != artifactType { + continue + } + + result = append(result, ispec.Descriptor{ + MediaType: manifest.MediaType, + ArtifactType: art.ArtifactType, + Size: manifest.Size, + Digest: manifest.Digest, + Annotations: art.Annotations, + }) + } + + found = true + } + + if !found { + return nilIndex, zerr.ErrManifestNotFound + } + + index = ispec.Index{ + Versioned: imeta.Versioned{SchemaVersion: defaultSchemaVersion}, + MediaType: ispec.MediaTypeImageIndex, + Manifests: result, + Annotations: map[string]string{}, + } + + // response was filtered by artifactType + if artifactType != "" { + index.Annotations[storageConstants.ReferrerFilterAnnotation] = artifactType + } + + return index, nil +} + +func (is *ImageStoreLocal) GetOrasReferrers(repo string, gdigest godigest.Digest, artifactType string, +) ([]oras.Descriptor, error) { var lockLatency time.Time if err := gdigest.Validate(); err != nil { @@ -1361,10 +1475,10 @@ func (is *ImageStoreLocal) GetReferrers(repo string, gdigest godigest.Digest, ar found := false - result := []artifactspec.Descriptor{} + result := []oras.Descriptor{} for _, manifest := range index.Manifests { - if manifest.MediaType != artifactspec.MediaTypeArtifactManifest { + if manifest.MediaType != oras.MediaTypeArtifactManifest { continue } @@ -1381,18 +1495,23 @@ func (is *ImageStoreLocal) GetReferrers(repo string, gdigest godigest.Digest, ar return nil, err } - var artManifest artifactspec.Manifest + var artManifest oras.Manifest if err := json.Unmarshal(buf, &artManifest); err != nil { is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON") return nil, err } - if artifactType != artManifest.ArtifactType || gdigest != artManifest.Subject.Digest { + if artManifest.Subject.Digest != gdigest { continue } - result = append(result, artifactspec.Descriptor{ + // filter by artifact type + if artifactType != "" && artManifest.ArtifactType != artifactType { + continue + } + + result = append(result, oras.Descriptor{ MediaType: manifest.MediaType, ArtifactType: artManifest.ArtifactType, Digest: manifest.Digest, diff --git a/pkg/storage/local/local_test.go b/pkg/storage/local/local_test.go index 9a5b248b..f30f88ed 100644 --- a/pkg/storage/local/local_test.go +++ b/pkg/storage/local/local_test.go @@ -147,14 +147,14 @@ func TestStorageFSAPIs(t *testing.T) { panic(err) } - // invalid GetReferrers - _, err = imgStore.GetReferrers("invalid", "invalid", "invalid") + // invalid GetOrasReferrers + _, err = imgStore.GetOrasReferrers("invalid", "invalid", "invalid") So(err, ShouldNotBeNil) - _, err = imgStore.GetReferrers(repoName, "invalid", "invalid") + _, err = imgStore.GetOrasReferrers(repoName, "invalid", "invalid") So(err, ShouldNotBeNil) - _, err = imgStore.GetReferrers(repoName, digest, "invalid") + _, err = imgStore.GetOrasReferrers(repoName, digest, "invalid") So(err, ShouldNotBeNil) // invalid DeleteImageManifest @@ -175,7 +175,7 @@ func TestStorageFSAPIs(t *testing.T) { }) } -func TestGetReferrers(t *testing.T) { +func TestGetOrasReferrers(t *testing.T) { dir := t.TempDir() log := log.Logger{Logger: zerolog.New(os.Stdout)} @@ -218,7 +218,7 @@ func TestGetReferrers(t *testing.T) { So(err, ShouldBeNil) So(err, ShouldBeNil) - descriptors, err := imgStore.GetReferrers("zot-test", digest, "signature-example") + descriptors, err := imgStore.GetOrasReferrers("zot-test", digest, "signature-example") So(err, ShouldBeNil) So(descriptors, ShouldNotBeEmpty) So(descriptors[0].ArtifactType, ShouldEqual, "signature-example") @@ -982,7 +982,7 @@ func FuzzGetBlobContent(f *testing.F) { }) } -func FuzzGetReferrers(f *testing.F) { +func FuzzGetOrasReferrers(f *testing.F) { f.Fuzz(func(t *testing.T, data string) { log := &log.Logger{Logger: zerolog.New(os.Stdout)} metrics := monitoring.NewMetricsServer(false, *log) @@ -1033,7 +1033,7 @@ func FuzzGetReferrers(f *testing.F) { if err != nil { t.Error(err) } - _, err = imgStore.GetReferrers("zot-test", digest, data) + _, err = imgStore.GetOrasReferrers("zot-test", digest, data) if err != nil { if errors.Is(err, zerr.ErrManifestNotFound) || isKnownErr(err) { return diff --git a/pkg/storage/s3/s3.go b/pkg/storage/s3/s3.go index 8d3aa95a..f52a6786 100644 --- a/pkg/storage/s3/s3.go +++ b/pkg/storage/s3/s3.go @@ -1224,7 +1224,12 @@ func (is *ObjectStorage) GetBlobContent(repo string, digest godigest.Digest) ([] return buf.Bytes(), nil } -func (is *ObjectStorage) GetReferrers(repo string, digest godigest.Digest, mediaType string, +func (is *ObjectStorage) GetReferrers(repo string, digest godigest.Digest, artifactType string, +) (ispec.Index, error) { + return ispec.Index{}, zerr.ErrMethodNotSupported +} + +func (is *ObjectStorage) GetOrasReferrers(repo string, digest godigest.Digest, artifactType string, ) ([]artifactspec.Descriptor, error) { return nil, zerr.ErrMethodNotSupported } diff --git a/pkg/storage/s3/s3_test.go b/pkg/storage/s3/s3_test.go index 8d448f88..baf4ac38 100644 --- a/pkg/storage/s3/s3_test.go +++ b/pkg/storage/s3/s3_test.go @@ -917,6 +917,18 @@ func TestNegativeCasesObjectsStorage(t *testing.T) { So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrMethodNotSupported) }) + + Convey("Test GetOrasReferrers", func(c C) { + imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{ + DeleteFn: func(ctx context.Context, path string) error { + return errS3 + }, + }) + d := godigest.FromBytes([]byte("")) + _, err := imgStore.GetOrasReferrers(testImage, d, "application/image") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrMethodNotSupported) + }) }) } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 29bb7d50..d2d1907a 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -5,6 +5,7 @@ import ( "time" godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" "zotregistry.io/zot/pkg/scheduler" @@ -48,7 +49,8 @@ type ImageStore interface { //nolint:interfacebloat DeleteBlob(repo string, digest godigest.Digest) error GetIndexContent(repo string) ([]byte, error) GetBlobContent(repo string, digest godigest.Digest) ([]byte, error) - GetReferrers(repo string, digest godigest.Digest, mediaType string) ([]artifactspec.Descriptor, error) + GetReferrers(repo string, digest godigest.Digest, artifactType string) (ispec.Index, error) + GetOrasReferrers(repo string, digest godigest.Digest, artifactType string) ([]artifactspec.Descriptor, error) RunGCRepo(repo string) error RunGCPeriodically(interval time.Duration, sch *scheduler.Scheduler) } diff --git a/pkg/test/common.go b/pkg/test/common.go index 5d938838..510cb62b 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -212,8 +212,10 @@ func GetRandomImageConfig() ([]byte, godigest.Digest) { randomAuthor := randomString(maxLen) config := imagespec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: imagespec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: imagespec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, @@ -231,10 +233,25 @@ func GetRandomImageConfig() ([]byte, godigest.Digest) { return configBlobContent, configBlobDigestRaw } +func GetEmptyImageConfig() ([]byte, godigest.Digest) { + config := imagespec.Image{} + + configBlobContent, err := json.MarshalIndent(&config, "", "\t") + if err != nil { + log.Fatal(err) + } + + configBlobDigestRaw := godigest.FromBytes(configBlobContent) + + return configBlobContent, configBlobDigestRaw +} + func GetImageConfig() ([]byte, godigest.Digest) { config := imagespec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: imagespec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: imagespec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, @@ -305,8 +322,10 @@ func GetOciLayoutDigests(imagePath string) (godigest.Digest, godigest.Digest, go func GetImageComponents(layerSize int) (imagespec.Image, [][]byte, imagespec.Manifest, error) { config := imagespec.Image{ - Architecture: "amd64", - OS: "linux", + Platform: imagespec.Platform{ + Architecture: "amd64", + OS: "linux", + }, RootFS: imagespec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, diff --git a/pkg/test/mocks/image_store_mock.go b/pkg/test/mocks/image_store_mock.go index bdf71e4e..dfd504bc 100644 --- a/pkg/test/mocks/image_store_mock.go +++ b/pkg/test/mocks/image_store_mock.go @@ -5,6 +5,7 @@ import ( "time" godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" "zotregistry.io/zot/pkg/scheduler" @@ -39,7 +40,8 @@ type MockedImageStore struct { DeleteBlobFn func(repo string, digest godigest.Digest) error GetIndexContentFn func(repo string) ([]byte, error) GetBlobContentFn func(repo string, digest godigest.Digest) ([]byte, error) - GetReferrersFn func(repo string, digest godigest.Digest, mediaType string) ([]artifactspec.Descriptor, error) + GetReferrersFn func(repo string, digest godigest.Digest, artifactType string) (ispec.Index, error) + GetOrasReferrersFn func(repo string, digest godigest.Digest, artifactType string) ([]artifactspec.Descriptor, error) URLForPathFn func(path string) (string, error) RunGCRepoFn func(repo string) error RunGCPeriodicallyFn func(interval time.Duration, sch *scheduler.Scheduler) @@ -287,12 +289,23 @@ func (is MockedImageStore) GetBlobContent(repo string, digest godigest.Digest) ( } func (is MockedImageStore) GetReferrers( + repo string, digest godigest.Digest, + artifactType string, +) (ispec.Index, error) { + if is.GetReferrersFn != nil { + return is.GetReferrersFn(repo, digest, artifactType) + } + + return ispec.Index{}, nil +} + +func (is MockedImageStore) GetOrasReferrers( repo string, digest godigest.Digest, - mediaType string, + artifactType string, ) ([]artifactspec.Descriptor, error) { - if is.GetReferrersFn != nil { - return is.GetReferrersFn(repo, digest, mediaType) + if is.GetOrasReferrersFn != nil { + return is.GetOrasReferrersFn(repo, digest, artifactType) } return []artifactspec.Descriptor{}, nil diff --git a/test/blackbox/pushpull.bats b/test/blackbox/pushpull.bats index 469986b6..27741723 100644 --- a/test/blackbox/pushpull.bats +++ b/test/blackbox/pushpull.bats @@ -203,23 +203,50 @@ EOF [ "${lines[-1]}" == "this is an artifact" ] } -@test "list OCI artifacts with regclient" { - run regctl artifact list localhost:8080/test-regclient --format raw-body - [ "$status" -eq 0 ] - [ $(echo "${lines[-1]}" | jq '.manifests') == '[]' ] - # push OCI artifacts on an image - run regctl artifact put --refers localhost:8080/test-regclient <