0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-13 22:50:38 -05:00

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 <rchincha@cisco.com>
This commit is contained in:
Ramkumar Chinchani 2022-11-08 00:38:16 -08:00 committed by GitHub
parent eb722905cb
commit c0f93caacb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 865 additions and 118 deletions

View file

@ -16,6 +16,7 @@ COSIGN := $(TOOLSDIR)/bin/cosign
HELM := $(TOOLSDIR)/bin/helm HELM := $(TOOLSDIR)/bin/helm
ORAS := $(TOOLSDIR)/bin/oras ORAS := $(TOOLSDIR)/bin/oras
REGCLIENT := $(TOOLSDIR)/bin/regctl REGCLIENT := $(TOOLSDIR)/bin/regctl
REGCLIENT_VERSION := v0.4.5
STACKER := $(TOOLSDIR)/bin/stacker STACKER := $(TOOLSDIR)/bin/stacker
BATS := $(TOOLSDIR)/bin/bats BATS := $(TOOLSDIR)/bin/bats
TESTDATA := $(TOP_LEVEL)/test/data TESTDATA := $(TOP_LEVEL)/test/data
@ -121,7 +122,7 @@ $(HELM):
$(REGCLIENT): $(REGCLIENT):
mkdir -p $(TOOLSDIR)/bin 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 cp regctl $(TOOLSDIR)/bin/regctl
chmod +x $(TOOLSDIR)/bin/regctl chmod +x $(TOOLSDIR)/bin/regctl

View file

@ -16,13 +16,14 @@ The following document refers on the **core dist-spec**, see also the [zot-speci
## [**Why zot?**](COMPARISON.md) ## [**Why zot?**](COMPARISON.md)
## What's new? ## 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 * 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) * Supports container image signatures - [cosign](https://github.com/sigstore/cosign) and [notation](https://github.com/notaryproject/notation)
* Multi-arch support * Multi-arch support
* Clustering support * Clustering support
* Image linting support * Image linting support
* Supports push/pull OCI Artifacts
## [Demos](demos/README.md) ## [Demos](demos/README.md)
@ -290,7 +291,7 @@ Supports:
You can benchmark a zot registry or any other dist-spec conformant registry with `zb`. You can benchmark a zot registry or any other dist-spec conformant registry with `zb`.
## Building `zb`` ## Building `zb`
```console ```console
$ make bench $ make bench

3
go.mod
View file

@ -29,7 +29,7 @@ require (
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0 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/opencontainers/umoci v0.4.8-0.20210922062158-e60a0cc726e6
github.com/oras-project/artifacts-spec v1.0.0-rc.2 github.com/oras-project/artifacts-spec v1.0.0-rc.2
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 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/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/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/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 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.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 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.26.1

10
go.sum
View file

@ -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-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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 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-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 h1:2VSZwLx5k/BfsBxMMipG/LYUnmqOD/BPkIVgQUcTlLw=
github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= 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= 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/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/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.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.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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

View file

@ -4060,7 +4060,7 @@ func TestImageSignatures(t *testing.T) {
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
So(err, ShouldBeNil) 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) cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image)
out, err = cmd.CombinedOutput() out, err = cmd.CombinedOutput()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
@ -4084,7 +4084,7 @@ func TestImageSignatures(t *testing.T) {
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
So(err, ShouldBeNil) 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) cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image)
out, err = cmd.CombinedOutput() out, err = cmd.CombinedOutput()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
@ -4093,7 +4093,7 @@ func TestImageSignatures(t *testing.T) {
}) })
}) })
Convey("GetReferrers", func() { Convey("GetOrasReferrers", func() {
// cover error paths // cover error paths
resp, err := resty.R().Get( resp, err := resty.R().Get(
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", "badDigest")) 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 //nolint:dupl // duplicated test code
func TestRouteFailures(t *testing.T) { func TestRouteFailures(t *testing.T) {
Convey("Make a new controller", t, func() { Convey("Make a new controller", t, func() {
@ -4685,7 +4980,7 @@ func TestRouteFailures(t *testing.T) {
request = mux.SetURLVars(request, map[string]string{}) request = mux.SetURLVars(request, map[string]string{})
response := httptest.NewRecorder() response := httptest.NewRecorder()
rthdlr.GetReferrers(response, request) rthdlr.GetOrasReferrers(response, request)
resp := response.Result() resp := response.Result()
defer resp.Body.Close() defer resp.Body.Close()
@ -4696,7 +4991,7 @@ func TestRouteFailures(t *testing.T) {
request = mux.SetURLVars(request, map[string]string{"name": "foo"}) request = mux.SetURLVars(request, map[string]string{"name": "foo"})
response = httptest.NewRecorder() response = httptest.NewRecorder()
rthdlr.GetReferrers(response, request) rthdlr.GetOrasReferrers(response, request)
resp = response.Result() resp = response.Result()
defer resp.Body.Close() defer resp.Body.Close()

View file

@ -8,6 +8,8 @@
package api package api
import ( import (
"context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -96,6 +98,9 @@ func (rh *RouteHandler) SetupRoutes() {
rh.UpdateBlobUpload).Methods("PUT") rh.UpdateBlobUpload).Methods("PUT")
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()),
rh.DeleteBlobUpload).Methods("DELETE") 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, prefixedRouter.HandleFunc(constants.ExtCatalogPrefix,
rh.ListRepositories).Methods(allowedMethods("GET")...) rh.ListRepositories).Methods(allowedMethods("GET")...)
prefixedRouter.HandleFunc(constants.ExtOciDiscoverPrefix, prefixedRouter.HandleFunc(constants.ExtOciDiscoverPrefix,
@ -104,9 +109,9 @@ func (rh *RouteHandler) SetupRoutes() {
rh.CheckVersionSupport).Methods(allowedMethods("GET")...) 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", 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 // swagger
debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, rh.c.Log) 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 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 err != nil {
if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
WriteJSON(response, http.StatusNotFound, WriteJSON(response, http.StatusNotFound,
@ -375,7 +381,8 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.
return 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 err != nil {
if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
WriteJSON(response, http.StatusNotFound, WriteJSON(response, http.StatusNotFound,
@ -398,6 +405,117 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.
WriteData(response, http.StatusOK, mediaType, content) 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 // UpdateManifest godoc
// @Summary Update image manifest // @Summary Update image manifest
// @Description Update an image's manifest given a reference or a digest // @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. // 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, reference string,
) ([]byte, godigest.Digest, string, error) { ) ([]byte, godigest.Digest, string, error) {
content, digest, mediaType, err := imgStore.GetImageManifest(name, reference) 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", routeHandler.c.Log.Info().Msgf("image not found, trying to get image %s:%s by syncing on demand",
name, reference) 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) name, reference, false, routeHandler.c.Log)
if errSync != nil { if errSync != nil {
routeHandler.c.Log.Err(errSync).Msgf("error encounter while syncing image %s:%s", 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. // 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, artifactType string,
) ([]artifactspec.Descriptor, error) { ) ([]artifactspec.Descriptor, error) {
refs, err := imgStore.GetReferrers(name, digest, artifactType) refs, err := imgStore.GetOrasReferrers(name, digest, artifactType)
if err != nil { if err != nil {
if routeHandler.c.Config.Extensions != nil && if routeHandler.c.Config.Extensions != nil &&
routeHandler.c.Config.Extensions.Sync != 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", routeHandler.c.Log.Info().Msgf("signature not found, trying to get signature %s:%s by syncing on demand",
name, digest.String()) 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) name, digest.String(), true, routeHandler.c.Log)
if errSync != nil { if errSync != nil {
routeHandler.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") 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 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"` References []artifactspec.Descriptor `json:"references"`
} }
// GetReferrers godoc // GetOrasReferrers godoc
// @Summary Get references for an image // @Summary Get references for an image
// @Description Get references for an image given a digest and artifact type // @Description Get references for an image given a digest and artifact type
// @Accept json // @Accept json
@ -1530,7 +1649,7 @@ type ReferenceList struct {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /oras/artifacts/v1/{name:%s}/manifests/{digest}/referrers [get]. // @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) vars := mux.Vars(request)
name, ok := vars["name"] name, ok := vars["name"]
@ -1549,15 +1668,20 @@ func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http
return return
} }
// filter by artifact type
artifactType := ""
artifactTypes, ok := request.URL.Query()["artifactType"] artifactTypes, ok := request.URL.Query()["artifactType"]
if !ok || len(artifactTypes) != 1 { if ok {
if len(artifactTypes) != 1 {
rh.c.Log.Error().Msg("invalid artifact types") rh.c.Log.Error().Msg("invalid artifact types")
response.WriteHeader(http.StatusBadRequest) response.WriteHeader(http.StatusBadRequest)
return return
} }
artifactType := artifactTypes[0] artifactType = artifactTypes[0]
}
if artifactType != notreg.ArtifactTypeNotation { if artifactType != notreg.ArtifactTypeNotation {
rh.c.Log.Error().Str("artifactType", artifactType).Msg("invalid artifact type") rh.c.Log.Error().Str("artifactType", artifactType).Msg("invalid artifact type")
@ -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") 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 { if err != nil {
rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") 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 return
} }

View file

@ -15,6 +15,6 @@ import (
func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router, func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router,
storeController storage.StoreController, log log.Logger, 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") "doesn't include this feature, please build a binary that does so")
} }

View file

@ -19,6 +19,6 @@ import (
func SetupSwaggerRoutes(conf *config.Config, router *mux.Router, log log.Logger, func SetupSwaggerRoutes(conf *config.Config, router *mux.Router, log log.Logger,
) { ) {
// swagger swagger "/swagger/v2/index.html" // 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") "doesn't include this feature, please build a binary that does so")
} }

View file

@ -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, repoName, reference string, isArtifact bool, log log.Logger,
) error { ) error {
log.Info().Msgf("syncing image %s:%s", repoName, reference) 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 return err
} }

View file

@ -22,7 +22,7 @@ func EnableSyncExtension(ctx context.Context,
} }
// SyncOneImage ... // 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, repoName, reference string, isArtifact bool, log log.Logger,
) error { ) error {
log.Warn().Msg("skipping syncing on demand because given zot binary doesn't include this feature," + log.Warn().Msg("skipping syncing on demand because given zot binary doesn't include this feature," +

View file

@ -1101,8 +1101,10 @@ func TestDerivedImageList(t *testing.T) {
Convey("Test dependency list for image working", t, func() { Convey("Test dependency list for image working", t, func() {
// create test images // create test images
config := ispec.Image{ config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: ispec.RootFS{ RootFS: ispec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},
@ -1516,8 +1518,10 @@ func TestBaseImageList(t *testing.T) {
Convey("Test base image list for image working", t, func() { Convey("Test base image list for image working", t, func() {
// create test images // create test images
config := ispec.Image{ config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: ispec.RootFS{ RootFS: ispec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},
@ -2502,8 +2506,10 @@ func TestImageList(t *testing.T) {
WaitTillServerReady(baseURL) WaitTillServerReady(baseURL)
config := ispec.Image{ config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: ispec.RootFS{ RootFS: ispec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},
@ -2619,8 +2625,10 @@ func TestBuildImageInfo(t *testing.T) {
} }
config := ispec.Image{ config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: ispec.RootFS{ RootFS: ispec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},

View file

@ -201,7 +201,7 @@ func (olu BaseOciLayoutUtils) checkNotarySignature(name string, digest godigest.
imageStore := olu.StoreController.GetImageStore(name) imageStore := olu.StoreController.GetImageStore(name)
mediaType := notreg.ArtifactTypeNotation mediaType := notreg.ArtifactTypeNotation
_, err := imageStore.GetReferrers(name, digest, mediaType) _, err := imageStore.GetOrasReferrers(name, digest, mediaType)
if err != nil { if err != nil {
olu.Log.Info().Err(err).Str("repo", name).Str("digest", olu.Log.Info().Err(err).Str("repo", name).Str("digest",
digest.String()).Str("mediatype", mediaType).Msg("invalid notary signature") digest.String()).Str("mediatype", mediaType).Msg("invalid notary signature")

View file

@ -322,8 +322,10 @@ func TestExtractImageDetails(t *testing.T) {
testLogger := log.NewLogger("debug", "") testLogger := log.NewLogger("debug", "")
layerDigest := godigest.FromBytes(content) layerDigest := godigest.FromBytes(content)
config := ispec.Image{ config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: ispec.RootFS{ RootFS: ispec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},

View file

@ -50,7 +50,7 @@ func (di *demandedImages) delete(key string) {
di.syncedMap.Delete(key) 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, repo, reference string, isArtifact bool, log log.Logger,
) error { ) error {
// guard against multiple parallel requests // guard against multiple parallel requests
@ -73,7 +73,7 @@ func OneImage(cfg Config, storeController storage.StoreController,
defer demandedImgs.delete(demandedImage) defer demandedImgs.delete(demandedImage)
defer close(imageChannel) 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 err, ok := <-imageChannel
if !ok { if !ok {
@ -83,7 +83,7 @@ func OneImage(cfg Config, storeController storage.StoreController,
return err 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, localRepo, reference string, isArtifact bool, log log.Logger,
) { ) {
var credentialsFile CredentialsFile var credentialsFile CredentialsFile
@ -248,7 +248,7 @@ func syncOneImage(imageChannel chan error, cfg Config, storeController storage.S
demandedImageRef, copyErr) demandedImageRef, copyErr)
time.Sleep(retryOptions.Delay) 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) _, err := syncRun(regCfg, localRepo, upstreamRepo, reference, syncContextUtils, sig, log)
return err return err

View file

@ -340,7 +340,7 @@ func (sig *signaturesCopier) canSkipNotarySignature(localRepo, digestStr string,
// check notary signature already synced // check notary signature already synced
if len(refs.References) > 0 { 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 err != nil {
if errors.Is(err, zerr.ErrManifestNotFound) { if errors.Is(err, zerr.ErrManifestNotFound) {
return false, nil return false, nil

View file

@ -26,7 +26,7 @@ import (
notreg "github.com/notaryproject/notation-go/registry" notreg "github.com/notaryproject/notation-go/registry"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" 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" perr "github.com/pkg/errors"
"github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/options"
@ -2404,7 +2404,7 @@ func TestPeriodicallySignaturesErr(t *testing.T) {
}) })
Convey("Trigger error on notary signature", func() { 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") notaryURLPath := path.Join("/oras/artifacts/v1/", repoName, "manifests", imageManifestDigest.String(), "referrers")
// based on image manifest digest get referrers // based on image manifest digest get referrers
@ -2422,7 +2422,7 @@ func TestPeriodicallySignaturesErr(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
// read manifest // read manifest
var artifactManifest artifactspec.Manifest var artifactManifest oraspec.Manifest
for _, ref := range referrers.References { for _, ref := range referrers.References {
refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded())
body, err := os.ReadFile(refPath) body, err := os.ReadFile(refPath)
@ -2450,6 +2450,53 @@ func TestPeriodicallySignaturesErr(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400) 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)) err = os.RemoveAll(path.Join(destDir, repoName))
So(err, ShouldBeNil) So(err, ShouldBeNil)
var artifactManifest artifactspec.Manifest var artifactManifest oraspec.Manifest
for _, ref := range referrers.References { for _, ref := range referrers.References {
refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded())
body, err := os.ReadFile(refPath) body, err := os.ReadFile(refPath)
@ -4405,6 +4452,42 @@ func pushRepo(url, repoName string) godigest.Digest {
panic(err) 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 return digest
} }

View file

@ -8,7 +8,7 @@ import (
"github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" 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/rs/zerolog"
"github.com/sigstore/cosign/pkg/oci/remote" "github.com/sigstore/cosign/pkg/oci/remote"
@ -63,7 +63,8 @@ func ValidateManifest(imgStore ImageStore, repo, reference, mediaType string, bo
return "", zerr.ErrBadManifest return "", zerr.ErrBadManifest
} }
if mediaType == ispec.MediaTypeImageManifest { switch mediaType {
case ispec.MediaTypeImageManifest:
var manifest ispec.Manifest var manifest ispec.Manifest
if err := json.Unmarshal(body, &manifest); err != nil { if err := json.Unmarshal(body, &manifest); err != nil {
log.Error().Err(err).Msg("unable to unmarshal JSON") 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 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 var m notation.Descriptor
if err := json.Unmarshal(body, &m); err != nil { if err := json.Unmarshal(body, &m); err != nil {
log.Error().Err(err).Msg("unable to unmarshal JSON") log.Error().Err(err).Msg("unable to unmarshal JSON")
return "", zerr.ErrBadManifest 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 return "", nil
@ -423,5 +449,6 @@ func ApplyLinter(imgStore ImageStore, linter Lint, repo string, manifestDesc isp
func IsSupportedMediaType(mediaType string) bool { func IsSupportedMediaType(mediaType string) bool {
return mediaType == ispec.MediaTypeImageIndex || return mediaType == ispec.MediaTypeImageIndex ||
mediaType == ispec.MediaTypeImageManifest || mediaType == ispec.MediaTypeImageManifest ||
mediaType == artifactspec.MediaTypeArtifactManifest mediaType == ispec.MediaTypeArtifactManifest ||
mediaType == oras.MediaTypeArtifactManifest
} }

View file

@ -18,4 +18,6 @@ const (
DBExtensionName = ".db" DBExtensionName = ".db"
DBCacheLockCheckTimeout = 10 * time.Second DBCacheLockCheckTimeout = 10 * time.Second
BoltdbName = "cache" BoltdbName = "cache"
ReferrerFilterAnnotation = "org.opencontainers.references.filtersApplied"
//
) )

View file

@ -20,10 +20,11 @@ import (
guuid "github.com/gofrs/uuid" guuid "github.com/gofrs/uuid"
"github.com/minio/sha256-simd" "github.com/minio/sha256-simd"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
imeta "github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/umoci" "github.com/opencontainers/umoci"
"github.com/opencontainers/umoci/oci/casext" "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" "github.com/rs/zerolog"
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
@ -39,6 +40,7 @@ import (
const ( const (
DefaultFilePerms = 0o600 DefaultFilePerms = 0o600
DefaultDirPerms = 0o700 DefaultDirPerms = 0o700
defaultSchemaVersion = 2
) )
// ImageStoreLocal provides the image storage operations. // 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 // "index.json" file - create if it doesn't exist
indexPath := path.Join(repoDir, "index.json") indexPath := path.Join(repoDir, "index.json")
if _, err := os.Stat(indexPath); err != nil { if _, err := os.Stat(indexPath); err != nil {
index := ispec.Index{} index := ispec.Index{Versioned: imeta.Versioned{SchemaVersion: defaultSchemaVersion}}
index.SchemaVersion = 2
buf, err := json.Marshal(index) buf, err := json.Marshal(index)
if err != nil { 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, 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 var lockLatency time.Time
if err := gdigest.Validate(); err != nil { if err := gdigest.Validate(); err != nil {
@ -1361,10 +1475,10 @@ func (is *ImageStoreLocal) GetReferrers(repo string, gdigest godigest.Digest, ar
found := false found := false
result := []artifactspec.Descriptor{} result := []oras.Descriptor{}
for _, manifest := range index.Manifests { for _, manifest := range index.Manifests {
if manifest.MediaType != artifactspec.MediaTypeArtifactManifest { if manifest.MediaType != oras.MediaTypeArtifactManifest {
continue continue
} }
@ -1381,18 +1495,23 @@ func (is *ImageStoreLocal) GetReferrers(repo string, gdigest godigest.Digest, ar
return nil, err return nil, err
} }
var artManifest artifactspec.Manifest var artManifest oras.Manifest
if err := json.Unmarshal(buf, &artManifest); err != nil { if err := json.Unmarshal(buf, &artManifest); err != nil {
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON") is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
return nil, err return nil, err
} }
if artifactType != artManifest.ArtifactType || gdigest != artManifest.Subject.Digest { if artManifest.Subject.Digest != gdigest {
continue continue
} }
result = append(result, artifactspec.Descriptor{ // filter by artifact type
if artifactType != "" && artManifest.ArtifactType != artifactType {
continue
}
result = append(result, oras.Descriptor{
MediaType: manifest.MediaType, MediaType: manifest.MediaType,
ArtifactType: artManifest.ArtifactType, ArtifactType: artManifest.ArtifactType,
Digest: manifest.Digest, Digest: manifest.Digest,

View file

@ -147,14 +147,14 @@ func TestStorageFSAPIs(t *testing.T) {
panic(err) panic(err)
} }
// invalid GetReferrers // invalid GetOrasReferrers
_, err = imgStore.GetReferrers("invalid", "invalid", "invalid") _, err = imgStore.GetOrasReferrers("invalid", "invalid", "invalid")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
_, err = imgStore.GetReferrers(repoName, "invalid", "invalid") _, err = imgStore.GetOrasReferrers(repoName, "invalid", "invalid")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
_, err = imgStore.GetReferrers(repoName, digest, "invalid") _, err = imgStore.GetOrasReferrers(repoName, digest, "invalid")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
// invalid DeleteImageManifest // invalid DeleteImageManifest
@ -175,7 +175,7 @@ func TestStorageFSAPIs(t *testing.T) {
}) })
} }
func TestGetReferrers(t *testing.T) { func TestGetOrasReferrers(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
log := log.Logger{Logger: zerolog.New(os.Stdout)} log := log.Logger{Logger: zerolog.New(os.Stdout)}
@ -218,7 +218,7 @@ func TestGetReferrers(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
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(err, ShouldBeNil)
So(descriptors, ShouldNotBeEmpty) So(descriptors, ShouldNotBeEmpty)
So(descriptors[0].ArtifactType, ShouldEqual, "signature-example") 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) { f.Fuzz(func(t *testing.T, data string) {
log := &log.Logger{Logger: zerolog.New(os.Stdout)} log := &log.Logger{Logger: zerolog.New(os.Stdout)}
metrics := monitoring.NewMetricsServer(false, *log) metrics := monitoring.NewMetricsServer(false, *log)
@ -1033,7 +1033,7 @@ func FuzzGetReferrers(f *testing.F) {
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
_, err = imgStore.GetReferrers("zot-test", digest, data) _, err = imgStore.GetOrasReferrers("zot-test", digest, data)
if err != nil { if err != nil {
if errors.Is(err, zerr.ErrManifestNotFound) || isKnownErr(err) { if errors.Is(err, zerr.ErrManifestNotFound) || isKnownErr(err) {
return return

View file

@ -1224,7 +1224,12 @@ func (is *ObjectStorage) GetBlobContent(repo string, digest godigest.Digest) ([]
return buf.Bytes(), nil 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) { ) ([]artifactspec.Descriptor, error) {
return nil, zerr.ErrMethodNotSupported return nil, zerr.ErrMethodNotSupported
} }

View file

@ -917,6 +917,18 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrMethodNotSupported) 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)
})
}) })
} }

View file

@ -5,6 +5,7 @@ import (
"time" "time"
godigest "github.com/opencontainers/go-digest" 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" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
"zotregistry.io/zot/pkg/scheduler" "zotregistry.io/zot/pkg/scheduler"
@ -48,7 +49,8 @@ type ImageStore interface { //nolint:interfacebloat
DeleteBlob(repo string, digest godigest.Digest) error DeleteBlob(repo string, digest godigest.Digest) error
GetIndexContent(repo string) ([]byte, error) GetIndexContent(repo string) ([]byte, error)
GetBlobContent(repo string, digest godigest.Digest) ([]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 RunGCRepo(repo string) error
RunGCPeriodically(interval time.Duration, sch *scheduler.Scheduler) RunGCPeriodically(interval time.Duration, sch *scheduler.Scheduler)
} }

View file

@ -212,8 +212,10 @@ func GetRandomImageConfig() ([]byte, godigest.Digest) {
randomAuthor := randomString(maxLen) randomAuthor := randomString(maxLen)
config := imagespec.Image{ config := imagespec.Image{
Platform: imagespec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: imagespec.RootFS{ RootFS: imagespec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},
@ -231,10 +233,25 @@ func GetRandomImageConfig() ([]byte, godigest.Digest) {
return configBlobContent, configBlobDigestRaw 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) { func GetImageConfig() ([]byte, godigest.Digest) {
config := imagespec.Image{ config := imagespec.Image{
Platform: imagespec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: imagespec.RootFS{ RootFS: imagespec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, 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) { func GetImageComponents(layerSize int) (imagespec.Image, [][]byte, imagespec.Manifest, error) {
config := imagespec.Image{ config := imagespec.Image{
Platform: imagespec.Platform{
Architecture: "amd64", Architecture: "amd64",
OS: "linux", OS: "linux",
},
RootFS: imagespec.RootFS{ RootFS: imagespec.RootFS{
Type: "layers", Type: "layers",
DiffIDs: []godigest.Digest{}, DiffIDs: []godigest.Digest{},

View file

@ -5,6 +5,7 @@ import (
"time" "time"
godigest "github.com/opencontainers/go-digest" 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" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
"zotregistry.io/zot/pkg/scheduler" "zotregistry.io/zot/pkg/scheduler"
@ -39,7 +40,8 @@ type MockedImageStore struct {
DeleteBlobFn func(repo string, digest godigest.Digest) error DeleteBlobFn func(repo string, digest godigest.Digest) error
GetIndexContentFn func(repo string) ([]byte, error) GetIndexContentFn func(repo string) ([]byte, error)
GetBlobContentFn func(repo string, digest godigest.Digest) ([]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) URLForPathFn func(path string) (string, error)
RunGCRepoFn func(repo string) error RunGCRepoFn func(repo string) error
RunGCPeriodicallyFn func(interval time.Duration, sch *scheduler.Scheduler) 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( 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, repo string,
digest godigest.Digest, digest godigest.Digest,
mediaType string, artifactType string,
) ([]artifactspec.Descriptor, error) { ) ([]artifactspec.Descriptor, error) {
if is.GetReferrersFn != nil { if is.GetOrasReferrersFn != nil {
return is.GetReferrersFn(repo, digest, mediaType) return is.GetOrasReferrersFn(repo, digest, artifactType)
} }
return []artifactspec.Descriptor{}, nil return []artifactspec.Descriptor{}, nil

View file

@ -203,23 +203,50 @@ EOF
[ "${lines[-1]}" == "this is an artifact" ] [ "${lines[-1]}" == "this is an artifact" ]
} }
@test "list OCI artifacts with regclient" { @test "push OCI artifact references with regclient" {
run regctl artifact list localhost:8080/test-regclient --format raw-body run regctl artifact put localhost:8080/manifest-ref:demo <<EOF
[ "$status" -eq 0 ] test artifact
[ $(echo "${lines[-1]}" | jq '.manifests') == '[]' ]
# push OCI artifacts on an image
run regctl artifact put --refers localhost:8080/test-regclient <<EOF
first artifact with subject
EOF EOF
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
run regctl artifact list localhost:8080/manifest-ref:demo --format raw-body
run regctl artifact put --refers localhost:8080/test-regclient <<EOF [ "$status" -eq 0 ]
second artifact with subject [ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 0 ]
run regctl artifact put --annotation demo=true --annotation format=oci --artifact-type "application/vnd.example.icecream.v1" --subject localhost:8080/manifest-ref:demo << EOF
test reference
EOF EOF
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
# with artifact media-type
# list OCI artifacts of an image run regctl artifact put localhost:8080/artifact-ref:demo <<EOF
run regctl artifact list localhost:8080/test-regclient --format raw-body test artifact
EOF
[ "$status" -eq 0 ]
run regctl artifact list localhost:8080/artifact-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 0 ]
run regctl artifact put --media-type "application/vnd.oci.artifact.manifest.v1+json" --annotation demo=true --annotation format=oci --artifact-type "application/vnd.example.icecream.v1" --subject localhost:8080/artifact-ref:demo << EOF
test reference
EOF
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 2 ] }
@test "pull OCI artifact references with regclient" {
run regctl artifact list localhost:8080/manifest-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 1 ]
run regctl artifact list --filter-artifact-type "application/vnd.example.icecream.v1" localhost:8080/manifest-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 1 ]
run regctl artifact list --filter-artifact-type "application/invalid" localhost:8080/manifest-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 0 ]
# with artifact media-type
run regctl artifact list localhost:8080/artifact-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 1 ]
run regctl artifact list --filter-artifact-type "application/vnd.example.icecream.v1" localhost:8080/artifact-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 1 ]
run regctl artifact list --filter-artifact-type "application/invalid" localhost:8080/artifact-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(echo "${lines[-1]}" | jq '.manifests | length') -eq 0 ]
} }