mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
artifacts: initial support for artifacts/notaryv2 spec
https://github.com/oras-project/artifacts-spec https://github.com/notaryproject/notaryproject Fixes issue #264 Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
This commit is contained in:
parent
d1a80ba9b7
commit
e42e42a2cc
12 changed files with 1537 additions and 47 deletions
5
.github/workflows/ci-cd.yml
vendored
5
.github/workflows/ci-cd.yml
vendored
|
@ -11,7 +11,7 @@ on:
|
|||
name: build-test
|
||||
jobs:
|
||||
build-test:
|
||||
name: Build and test ZOT
|
||||
name: Build and test zot
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
s3mock:
|
||||
|
@ -39,6 +39,9 @@ jobs:
|
|||
sudo apt-get install rpm
|
||||
sudo apt install snapd
|
||||
sudo snap install skopeo --edge --devmode
|
||||
curl -Lo notation.tar.gz https://github.com/notaryproject/notation/releases/download/v0.7.1-alpha.1/notation_0.7.1-alpha.1_linux_amd64.tar.gz
|
||||
sudo tar xvzf notation.tar.gz -C /usr/bin notation
|
||||
go get github.com/wadey/gocovmerge
|
||||
|
||||
- name: Run build and test
|
||||
timeout-minutes: 30
|
||||
|
|
|
@ -18,6 +18,7 @@ https://anuvu.github.io/zot/
|
|||
* ```make binary``` builds a zot with all extensions enabled
|
||||
* Uses [OCI image layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) for image storage
|
||||
* Can serve any OCI image layout as a registry
|
||||
* Supports container image signatures - [cosign](https://github.com/sigstore/cosign) and [notation](https://github.com/notaryproject/notation)
|
||||
* Supports [helm charts](https://helm.sh/docs/topics/registries/)
|
||||
* Behavior controlled via [configuration](./examples/README.md)
|
||||
* Supports image deletion by tag
|
||||
|
|
|
@ -44,4 +44,5 @@ var (
|
|||
ErrEmptyRepoList = errors.New("search: no repository found")
|
||||
ErrInvalidRepositoryName = errors.New("routes: not a repository name")
|
||||
ErrSyncMissingCatalog = errors.New("sync: couldn't fetch upstream registry's catalog")
|
||||
ErrMethodNotSupported = errors.New("storage: method not supported")
|
||||
)
|
||||
|
|
4
go.mod
4
go.mod
|
@ -28,15 +28,19 @@ require (
|
|||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mitchellh/mapstructure v1.4.2
|
||||
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
|
||||
github.com/notaryproject/notation v0.7.0-alpha.1
|
||||
github.com/notaryproject/notation-go-lib v1.0.0-alpha-1
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/opencontainers/distribution-spec/specs-go v0.0.0-20211026153258-b3f631f25f1a
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5
|
||||
github.com/opencontainers/umoci v0.4.8-0.20210922062158-e60a0cc726e6
|
||||
github.com/oras-project/artifacts-spec v1.0.0-draft.1
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/rs/zerolog v1.26.0
|
||||
github.com/sigstore/cosign v1.3.1
|
||||
github.com/smartystreets/goconvey v1.7.2
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/viper v1.9.0
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build extended
|
||||
// +build extended
|
||||
|
||||
package api_test
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
@ -30,8 +32,14 @@ import (
|
|||
"github.com/chartmuseum/auth"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
vldap "github.com/nmcclain/ldap"
|
||||
notreg "github.com/notaryproject/notation/pkg/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"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/generate"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/verify"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -2775,6 +2783,318 @@ func TestHardLink(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestImageSignatures(t *testing.T) {
|
||||
Convey("Validate signatures", t, func() {
|
||||
// start a new server
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
c := api.NewController(conf)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}(c)
|
||||
// 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)
|
||||
}(c)
|
||||
|
||||
repoName := "signed-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, 202)
|
||||
loc := 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, 201)
|
||||
blobLoc := resp.Header().Get("Location")
|
||||
So(blobLoc, ShouldNotBeEmpty)
|
||||
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
|
||||
So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
|
||||
|
||||
// create a manifest
|
||||
m := ispec.Manifest{
|
||||
Config: ispec.Descriptor{
|
||||
Digest: digest,
|
||||
Size: int64(len(content)),
|
||||
},
|
||||
Layers: []ispec.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
||||
Digest: digest,
|
||||
Size: int64(len(content)),
|
||||
},
|
||||
},
|
||||
}
|
||||
m.SchemaVersion = 2
|
||||
content, err = json.Marshal(m)
|
||||
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, 201)
|
||||
d := resp.Header().Get(api.DistContentDigestKey)
|
||||
So(d, ShouldNotBeEmpty)
|
||||
So(d, ShouldEqual, digest.String())
|
||||
|
||||
Convey("Validate cosign signatures", func() {
|
||||
cwd, err := os.Getwd()
|
||||
So(err, ShouldBeNil)
|
||||
defer func() { _ = os.Chdir(cwd) }()
|
||||
tdir, err := ioutil.TempDir("", "cosign")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.RemoveAll(tdir)
|
||||
_ = os.Chdir(tdir)
|
||||
|
||||
// generate a keypair
|
||||
os.Setenv("COSIGN_PASSWORD", "")
|
||||
err = generate.GenerateKeyPairCmd(context.TODO(), "", nil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// sign the image
|
||||
err = sign.SignCmd(context.TODO(),
|
||||
sign.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
|
||||
options.RegistryOptions{AllowInsecure: true},
|
||||
map[string]interface{}{"tag": "1.0"},
|
||||
[]string{fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())},
|
||||
"", true, "", false, false, "")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// verify the image
|
||||
a := &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
|
||||
amap, err := a.AnnotationsMap()
|
||||
So(err, ShouldBeNil)
|
||||
v := verify.VerifyCommand{
|
||||
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
||||
CheckClaims: true,
|
||||
KeyRef: path.Join(tdir, "cosign.pub"),
|
||||
Annotations: amap,
|
||||
}
|
||||
err = v.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// verify the image with incorrect tag
|
||||
a = &options.AnnotationOptions{Annotations: []string{"tag=2.0"}}
|
||||
amap, err = a.AnnotationsMap()
|
||||
So(err, ShouldBeNil)
|
||||
v = verify.VerifyCommand{
|
||||
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
||||
CheckClaims: true,
|
||||
KeyRef: path.Join(tdir, "cosign.pub"),
|
||||
Annotations: amap,
|
||||
}
|
||||
err = v.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
// verify the image with incorrect key
|
||||
a = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
|
||||
amap, err = a.AnnotationsMap()
|
||||
So(err, ShouldBeNil)
|
||||
v = verify.VerifyCommand{
|
||||
CheckClaims: true,
|
||||
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
||||
KeyRef: path.Join(tdir, "cosign.key"),
|
||||
Annotations: amap,
|
||||
}
|
||||
err = v.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
// generate another keypair
|
||||
err = os.Remove(path.Join(tdir, "cosign.pub"))
|
||||
So(err, ShouldBeNil)
|
||||
err = os.Remove(path.Join(tdir, "cosign.key"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
os.Setenv("COSIGN_PASSWORD", "")
|
||||
err = generate.GenerateKeyPairCmd(context.TODO(), "", nil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// verify the image with incorrect key
|
||||
a = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
|
||||
amap, err = a.AnnotationsMap()
|
||||
So(err, ShouldBeNil)
|
||||
v = verify.VerifyCommand{
|
||||
CheckClaims: true,
|
||||
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
||||
KeyRef: path.Join(tdir, "cosign.pub"),
|
||||
Annotations: amap,
|
||||
}
|
||||
err = v.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Validate notation signatures", func() {
|
||||
cwd, err := os.Getwd()
|
||||
So(err, ShouldBeNil)
|
||||
defer func() { _ = os.Chdir(cwd) }()
|
||||
tdir, err := ioutil.TempDir("", "notation")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.RemoveAll(tdir)
|
||||
_ = os.Chdir(tdir)
|
||||
|
||||
// "notation" (notaryv2) doesn't yet support exported apis, so use the binary instead
|
||||
notPath, err := exec.LookPath("notation")
|
||||
So(notPath, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
os.Setenv("XDG_CONFIG_HOME", tdir)
|
||||
|
||||
// generate a keypair
|
||||
cmd := exec.Command("notation", "cert", "generate-test", "--trust", "good")
|
||||
err = cmd.Run()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// generate another keypair
|
||||
cmd = exec.Command("notation", "cert", "generate-test", "--trust", "bad")
|
||||
err = cmd.Run()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// sign the image
|
||||
image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")
|
||||
cmd = exec.Command("notation", "sign", "--key", "good", "--plain-http", image)
|
||||
err = cmd.Run()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// verify the image
|
||||
cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image)
|
||||
out, err := cmd.CombinedOutput()
|
||||
So(err, ShouldBeNil)
|
||||
msg := string(out)
|
||||
So(msg, ShouldNotBeEmpty)
|
||||
So(strings.Contains(msg, "verification failure"), ShouldBeFalse)
|
||||
|
||||
// verify the image with incorrect key
|
||||
cmd = exec.Command("notation", "verify", "--cert", "bad", "--plain-http", image)
|
||||
out, err = cmd.CombinedOutput()
|
||||
So(err, ShouldNotBeNil)
|
||||
msg = string(out)
|
||||
So(msg, ShouldNotBeEmpty)
|
||||
So(strings.Contains(msg, "verification failure"), ShouldBeTrue)
|
||||
|
||||
// check unsupported manifest media type
|
||||
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.unsupported.image.manifest.v1+json").
|
||||
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 415)
|
||||
|
||||
// check invalid content with artifact media type
|
||||
resp, err = resty.R().SetHeader("Content-Type", artifactspec.MediaTypeArtifactManifest).
|
||||
SetBody([]byte("bogus")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 400)
|
||||
|
||||
Convey("Validate corrupted signature", func() {
|
||||
// verify with corrupted signature
|
||||
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.StatusOK)
|
||||
var refs api.ReferenceList
|
||||
err = json.Unmarshal(resp.Body(), &refs)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(refs.References), ShouldEqual, 1)
|
||||
err = ioutil.WriteFile(path.Join(dir, repoName, "blobs",
|
||||
strings.ReplaceAll(refs.References[0].Digest.String(), ":", "/")), []byte("corrupt"), 0600)
|
||||
So(err, ShouldBeNil)
|
||||
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)
|
||||
cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image)
|
||||
out, err = cmd.CombinedOutput()
|
||||
So(err, ShouldNotBeNil)
|
||||
msg = string(out)
|
||||
So(msg, ShouldNotBeEmpty)
|
||||
})
|
||||
|
||||
Convey("Validate deleted signature", func() {
|
||||
// verify with corrupted signature
|
||||
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.StatusOK)
|
||||
var refs api.ReferenceList
|
||||
err = json.Unmarshal(resp.Body(), &refs)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(refs.References), ShouldEqual, 1)
|
||||
err = os.Remove(path.Join(dir, repoName, "blobs",
|
||||
strings.ReplaceAll(refs.References[0].Digest.String(), ":", "/")))
|
||||
So(err, ShouldBeNil)
|
||||
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)
|
||||
cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image)
|
||||
out, err = cmd.CombinedOutput()
|
||||
So(err, ShouldNotBeNil)
|
||||
msg = string(out)
|
||||
So(msg, ShouldNotBeEmpty)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("GetReferrers", func() {
|
||||
// cover error paths
|
||||
resp, err := resty.R().Get(
|
||||
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", "badDigest"))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
||||
|
||||
resp, err = resty.R().Get(
|
||||
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, "badDigest"))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = resty.R().Get(
|
||||
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = resty.R().SetQueryParam("artifactType", "badArtifact").Get(
|
||||
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
||||
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", digest.String()))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func getAllBlobs(imagePath string) []string {
|
||||
blobList := make([]string, 0)
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ import (
|
|||
_ "github.com/anuvu/zot/swagger" // as required by swaggo
|
||||
"github.com/gorilla/mux"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/notaryproject/notation-go-lib"
|
||||
notreg "github.com/notaryproject/notation/pkg/registry"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
)
|
||||
|
@ -93,6 +95,11 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
g.HandleFunc("/",
|
||||
rh.CheckVersionSupport).Methods("GET")
|
||||
}
|
||||
|
||||
// support for oras artifact reference types (alpha 1) - image signature use case
|
||||
rh.c.Router.HandleFunc(fmt.Sprintf("/oras/artifacts/v1/{name:%s}/manifests/{digest}/referrers", NameRegexp.String()),
|
||||
rh.GetReferrers).Methods("GET")
|
||||
|
||||
// swagger swagger "/swagger/v2/index.html"
|
||||
rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler)
|
||||
// Setup Extensions Routes
|
||||
|
@ -393,7 +400,7 @@ func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
mediaType := r.Header.Get("Content-Type")
|
||||
if mediaType != ispec.MediaTypeImageManifest {
|
||||
if !storage.IsSupportedMediaType(mediaType) {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
@ -1290,3 +1297,68 @@ func getImageManifest(rh *RouteHandler, is storage.ImageStore, name,
|
|||
|
||||
return content, digest, mediaType, err
|
||||
}
|
||||
|
||||
type ReferenceList struct {
|
||||
References []notation.Descriptor `json:"references"`
|
||||
}
|
||||
|
||||
// GetReferrers godoc
|
||||
// @Summary Get references for an image
|
||||
// @Description Get references for an image given a digest and artifact type
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param digest path string true "image digest"
|
||||
// @Param artifactType query string true "artifact type"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @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(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name, ok := vars["name"]
|
||||
|
||||
if !ok || name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
digest, ok := vars["digest"]
|
||||
if !ok || digest == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
artifactTypes, ok := r.URL.Query()["artifactType"]
|
||||
if !ok || len(artifactTypes) != 1 {
|
||||
rh.c.Log.Error().Msg("invalid artifact types")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
artifactType := artifactTypes[0]
|
||||
|
||||
if artifactType != notreg.ArtifactTypeNotation {
|
||||
rh.c.Log.Error().Str("artifactType", artifactType).Msg("invalid artifact type")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
is := rh.getImageStore(name)
|
||||
|
||||
rh.c.Log.Info().Str("digest", digest).Str("artifactType", artifactType).Msg("getting manifest")
|
||||
|
||||
refs, err := is.GetReferrers(name, digest, artifactType)
|
||||
if err != nil {
|
||||
rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest).Msg("unable to get references")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
rs := ReferenceList{References: refs}
|
||||
|
||||
WriteJSON(w, http.StatusOK, rs)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
|
||||
"testing"
|
||||
|
||||
zerr "github.com/anuvu/zot/errors"
|
||||
"github.com/anuvu/zot/pkg/extensions/monitoring"
|
||||
"github.com/anuvu/zot/pkg/log"
|
||||
"github.com/anuvu/zot/pkg/storage"
|
||||
|
@ -717,4 +718,16 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
|
|||
err := il.DeleteBlob(testImage, d.String())
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test GetReferrers", t, func(c C) {
|
||||
il = createMockStorage(testDir, &StorageDriverMock{
|
||||
deleteFn: func(ctx context.Context, path string) error {
|
||||
return errS3
|
||||
},
|
||||
})
|
||||
d := godigest.FromBytes([]byte(""))
|
||||
_, err := il.GetReferrers(testImage, d.String(), "application/image")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zerr.ErrMethodNotSupported)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
zlog "github.com/anuvu/zot/pkg/log"
|
||||
"github.com/anuvu/zot/pkg/storage"
|
||||
guuid "github.com/gofrs/uuid"
|
||||
"github.com/notaryproject/notation-go-lib"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/rs/zerolog"
|
||||
|
@ -1018,6 +1019,10 @@ func (is *ObjectStorage) GetBlobContent(repo string, digest string) ([]byte, err
|
|||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (is *ObjectStorage) GetReferrers(repo, digest string, mediaType string) ([]notation.Descriptor, error) {
|
||||
return nil, errors.ErrMethodNotSupported
|
||||
}
|
||||
|
||||
func (is *ObjectStorage) GetIndexContent(repo string) ([]byte, error) {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package storage
|
|||
import (
|
||||
"io"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
|
@ -40,4 +41,5 @@ type ImageStore interface {
|
|||
DeleteBlob(repo string, digest string) error
|
||||
GetIndexContent(repo string) ([]byte, error)
|
||||
GetBlobContent(repo, digest string) ([]byte, error)
|
||||
GetReferrers(repo, digest string, mediaType string) ([]notation.Descriptor, error)
|
||||
}
|
||||
|
|
|
@ -15,11 +15,14 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
|
||||
|
||||
"github.com/anuvu/zot/errors"
|
||||
"github.com/anuvu/zot/pkg/extensions/monitoring"
|
||||
zlog "github.com/anuvu/zot/pkg/log"
|
||||
apexlog "github.com/apex/log"
|
||||
guuid "github.com/gofrs/uuid"
|
||||
"github.com/notaryproject/notation-go-lib"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/umoci"
|
||||
|
@ -437,7 +440,7 @@ func (is *ImageStoreFS) PutImageManifest(repo string, reference string, mediaTyp
|
|||
return "", err
|
||||
}
|
||||
|
||||
if mediaType != ispec.MediaTypeImageManifest {
|
||||
if !IsSupportedMediaType(mediaType) {
|
||||
is.log.Debug().Interface("actual", mediaType).
|
||||
Interface("expected", ispec.MediaTypeImageManifest).Msg("bad manifest media type")
|
||||
return "", errors.ErrBadManifest
|
||||
|
@ -448,6 +451,7 @@ func (is *ImageStoreFS) PutImageManifest(repo string, reference string, mediaTyp
|
|||
return "", errors.ErrBadManifest
|
||||
}
|
||||
|
||||
if mediaType == ispec.MediaTypeImageManifest {
|
||||
var m ispec.Manifest
|
||||
if err := json.Unmarshal(body, &m); err != nil {
|
||||
is.log.Error().Err(err).Msg("unable to unmarshal JSON")
|
||||
|
@ -469,6 +473,13 @@ func (is *ImageStoreFS) PutImageManifest(repo string, reference string, mediaTyp
|
|||
return digest.String(), errors.ErrBlobNotFound
|
||||
}
|
||||
}
|
||||
} else if mediaType == artifactspec.MediaTypeArtifactManifest {
|
||||
var m notation.Descriptor
|
||||
if err := json.Unmarshal(body, &m); err != nil {
|
||||
is.log.Error().Err(err).Msg("unable to unmarshal JSON")
|
||||
return "", errors.ErrBadManifest
|
||||
}
|
||||
}
|
||||
|
||||
mDigest := godigest.FromBytes(body)
|
||||
refIsDigest := false
|
||||
|
@ -1264,6 +1275,90 @@ func (is *ImageStoreFS) DeleteBlob(repo string, digest string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (is *ImageStoreFS) GetReferrers(repo, digest string, mediaType string) ([]notation.Descriptor, error) {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
if !is.DirExists(dir) {
|
||||
return nil, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
gdigest, err := godigest.Parse(digest)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("digest", digest).Msg("failed to parse digest")
|
||||
return nil, errors.ErrBadBlobDigest
|
||||
}
|
||||
|
||||
is.RLock()
|
||||
defer is.RUnlock()
|
||||
|
||||
buf, err := ioutil.ReadFile(path.Join(dir, "index.json"))
|
||||
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("failed to read index.json")
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var index ispec.Index
|
||||
if err := json.Unmarshal(buf, &index); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
result := []notation.Descriptor{}
|
||||
|
||||
for _, m := range index.Manifests {
|
||||
if m.MediaType != artifactspec.MediaTypeArtifactManifest {
|
||||
continue
|
||||
}
|
||||
|
||||
p := path.Join(dir, "blobs", m.Digest.Algorithm().String(), m.Digest.Encoded())
|
||||
|
||||
buf, err = ioutil.ReadFile(p)
|
||||
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", p).Msg("failed to read manifest")
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errors.ErrManifestNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var manifest artifactspec.Manifest
|
||||
if err := json.Unmarshal(buf, &manifest); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mediaType != manifest.ArtifactType || gdigest != manifest.Subject.Digest {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, notation.Descriptor{MediaType: m.MediaType,
|
||||
Digest: m.Digest, Size: m.Size, Annotations: m.Annotations})
|
||||
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, errors.ErrManifestNotFound
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func IsSupportedMediaType(mediaType string) bool {
|
||||
return mediaType == ispec.MediaTypeImageManifest ||
|
||||
mediaType == artifactspec.MediaTypeArtifactManifest
|
||||
}
|
||||
|
||||
// garbage collection
|
||||
|
||||
// Scrub will clean up all unreferenced blobs.
|
||||
|
|
|
@ -122,6 +122,16 @@ func TestStorageFSAPIs(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
// invalid GetReferrers
|
||||
_, err = il.GetReferrers("invalid", "invalid", "invalid")
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
_, err = il.GetReferrers(repoName, "invalid", "invalid")
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
_, err = il.GetReferrers(repoName, d.String(), "invalid")
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
// invalid DeleteImageManifest
|
||||
indexPath := path.Join(il.RootDir(), repoName, "index.json")
|
||||
err = os.Chmod(indexPath, 0000)
|
||||
|
|
Loading…
Reference in a new issue