0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -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
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

View file

@ -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

3
go.mod
View file

@ -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

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 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=

View file

@ -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()

View file

@ -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
}

View file

@ -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")
}

View file

@ -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")
}

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,
) 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
}

View file

@ -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," +

View file

@ -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{},

View file

@ -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")

View file

@ -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{},

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
//
)

View file

@ -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,

View file

@ -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

View file

@ -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
}

View file

@ -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)
})
})
}

View file

@ -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)
}

View file

@ -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{},

View file

@ -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

View file

@ -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 <<EOF
first artifact with subject
@test "push OCI artifact references with regclient" {
run regctl artifact put localhost:8080/manifest-ref:demo <<EOF
test artifact
EOF
[ "$status" -eq 0 ]
run regctl artifact put --refers localhost:8080/test-regclient <<EOF
second artifact with subject
run regctl artifact list localhost:8080/manifest-ref:demo --format raw-body
[ "$status" -eq 0 ]
[ $(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
[ "$status" -eq 0 ]
# list OCI artifacts of an image
run regctl artifact list localhost:8080/test-regclient --format raw-body
# with artifact media-type
run regctl artifact put localhost:8080/artifact-ref:demo <<EOF
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 ]
[ $(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 ]
}