distribution/registry/storage/schema2manifesthandler_test.go
James Hewitt c40c4b289a
Enable configuration of index dependency validation
Enable configuration options that can selectively disable validation
that dependencies exist within the registry before the image index
is uploaded.

This enables sparse indexes, where a registry holds a manifest index that
could be signed (so the digest must not change) but does not hold every
referenced image in the index. The use case for this is when a registry
mirror does not need to mirror all platforms, but does need to maintain
the digests of all manifests either because they are signed or because
they are pulled by digest.

The registry administrator can also select specific image architectures
that must exist in the registry, enabling a registry operator to select
only the platforms they care about and ensure all image indexes uploaded
to the registry are valid for those platforms.

Signed-off-by: James Hewitt <james.hewitt@uk.ibm.com>
2024-05-28 09:56:14 +01:00

321 lines
6.2 KiB
Go

package storage
import (
"regexp"
"strings"
"testing"
"github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/internal/dcontext"
"github.com/distribution/distribution/v3/manifest"
"github.com/distribution/distribution/v3/manifest/schema2"
"github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
"github.com/opencontainers/go-digest"
)
func TestVerifyManifestForeignLayer(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver,
ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")),
ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope")),
EnableValidateImageIndexImagesExist,
)
repo := makeRepository(t, registry, "test")
manifestService := makeManifestService(t, repo)
config, err := repo.Blobs(ctx).Put(ctx, schema2.MediaTypeImageConfig, nil)
if err != nil {
t.Fatal(err)
}
layer, err := repo.Blobs(ctx).Put(ctx, schema2.MediaTypeLayer, nil)
if err != nil {
t.Fatal(err)
}
foreignLayer := distribution.Descriptor{
Digest: "sha256:463435349086340864309863409683460843608348608934092322395278926a",
Size: 6323,
MediaType: schema2.MediaTypeForeignLayer,
}
emptyLayer := distribution.Descriptor{
Digest: "",
}
template := schema2.Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: schema2.MediaTypeManifest,
},
Config: config,
}
type testcase struct {
BaseLayer distribution.Descriptor
URLs []string
Err error
}
cases := []testcase{
{
foreignLayer,
nil,
errMissingURL,
},
{
// regular layers may have foreign urls
layer,
[]string{"http://foo/bar"},
nil,
},
{
foreignLayer,
[]string{"file:///local/file"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://foo/bar#baz"},
errInvalidURL,
},
{
foreignLayer,
[]string{""},
errInvalidURL,
},
{
foreignLayer,
[]string{"https://foo/bar", ""},
errInvalidURL,
},
{
foreignLayer,
[]string{"", "https://foo/bar"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://nope/bar"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://foo/nope"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://foo/bar"},
nil,
},
{
foreignLayer,
[]string{"https://foo/bar"},
nil,
},
{
emptyLayer,
[]string{"https://foo/empty"},
digest.ErrDigestInvalidFormat,
},
{
emptyLayer,
[]string{},
digest.ErrDigestInvalidFormat,
},
}
for _, c := range cases {
m := template
l := c.BaseLayer
l.URLs = c.URLs
m.Layers = []distribution.Descriptor{l}
dm, err := schema2.FromStruct(m)
if err != nil {
t.Error(err)
continue
}
_, err = manifestService.Put(ctx, dm)
if verr, ok := err.(distribution.ErrManifestVerification); ok {
// Extract the first error
if len(verr) == 2 {
if _, ok = verr[1].(distribution.ErrManifestBlobUnknown); ok {
err = verr[0]
}
}
}
if err != c.Err {
t.Errorf("%#v: expected %v, got %v", l, c.Err, err)
}
}
}
func TestVerifyManifestBlobLayerAndConfig(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver,
ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")),
ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope")),
EnableValidateImageIndexImagesExist,
)
repo := makeRepository(t, registry, strings.ToLower(t.Name()))
manifestService := makeManifestService(t, repo)
config, err := repo.Blobs(ctx).Put(ctx, schema2.MediaTypeImageConfig, nil)
if err != nil {
t.Fatal(err)
}
layer, err := repo.Blobs(ctx).Put(ctx, schema2.MediaTypeLayer, nil)
if err != nil {
t.Fatal(err)
}
template := schema2.Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: schema2.MediaTypeManifest,
},
}
checkFn := func(m schema2.Manifest, rerr error) {
dm, err := schema2.FromStruct(m)
if err != nil {
t.Error(err)
return
}
_, err = manifestService.Put(ctx, dm)
if verr, ok := err.(distribution.ErrManifestVerification); ok {
// Extract the first error
if len(verr) == 2 {
if _, ok = verr[1].(distribution.ErrManifestBlobUnknown); ok {
err = verr[0]
}
} else if len(verr) == 1 {
err = verr[0]
}
}
if err != rerr {
t.Errorf("%#v: expected %v, got %v", m, rerr, err)
}
}
type testcase struct {
Desc distribution.Descriptor
URLs []string
Err error
}
layercases := []testcase{
// empty media type
{
distribution.Descriptor{},
[]string{"http://foo/bar"},
digest.ErrDigestInvalidFormat,
},
{
distribution.Descriptor{},
nil,
digest.ErrDigestInvalidFormat,
},
// unknown media type, but blob is present
{
distribution.Descriptor{
Digest: layer.Digest,
},
nil,
nil,
},
{
distribution.Descriptor{
Digest: layer.Digest,
},
[]string{"http://foo/bar"},
nil,
},
// gzip layer, but invalid digest
{
distribution.Descriptor{
MediaType: schema2.MediaTypeLayer,
},
nil,
digest.ErrDigestInvalidFormat,
},
{
distribution.Descriptor{
MediaType: schema2.MediaTypeLayer,
},
[]string{"https://foo/bar"},
digest.ErrDigestInvalidFormat,
},
{
distribution.Descriptor{
MediaType: schema2.MediaTypeLayer,
Digest: digest.Digest("invalid"),
},
nil,
digest.ErrDigestInvalidFormat,
},
// normal uploaded gzip layer
{
layer,
nil,
nil,
},
{
layer,
[]string{"https://foo/bar"},
nil,
},
}
for _, c := range layercases {
m := template
m.Config = config
l := c.Desc
l.URLs = c.URLs
m.Layers = []distribution.Descriptor{l}
checkFn(m, c.Err)
}
configcases := []testcase{
// valid config
{
config,
nil,
nil,
},
// invalid digest
{
distribution.Descriptor{
MediaType: schema2.MediaTypeImageConfig,
},
[]string{"https://foo/bar"},
digest.ErrDigestInvalidFormat,
},
{
distribution.Descriptor{
MediaType: schema2.MediaTypeImageConfig,
Digest: digest.Digest("invalid"),
},
nil,
digest.ErrDigestInvalidFormat,
},
}
for _, c := range configcases {
m := template
m.Config = c.Desc
m.Config.URLs = c.URLs
checkFn(m, c.Err)
}
}