diff --git a/configuration/configuration.go b/configuration/configuration.go index e4d4311d5f4..166354d25c0 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -175,25 +175,7 @@ type Configuration struct { Proxy Proxy `yaml:"proxy,omitempty"` // Validation configures validation options for the registry. - Validation struct { - // Enabled enables the other options in this section. This field is - // deprecated in favor of Disabled. - Enabled bool `yaml:"enabled,omitempty"` - // Disabled disables the other options in this section. - Disabled bool `yaml:"disabled,omitempty"` - // Manifests configures manifest validation. - Manifests struct { - // URLs configures validation for URLs in pushed manifests. - URLs struct { - // Allow specifies regular expressions (https://godoc.org/regexp/syntax) - // that URLs in pushed manifests must match. - Allow []string `yaml:"allow,omitempty"` - // Deny specifies regular expressions (https://godoc.org/regexp/syntax) - // that URLs in pushed manifests must not match. - Deny []string `yaml:"deny,omitempty"` - } `yaml:"urls,omitempty"` - } `yaml:"manifests,omitempty"` - } `yaml:"validation,omitempty"` + Validation Validation `yaml:"validation,omitempty"` // Policy configures registry policy options. Policy struct { @@ -360,6 +342,13 @@ type Health struct { } `yaml:"storagedriver,omitempty"` } +type Platform struct { + // Architecture is the architecture for this platform + Architecture string `yaml:"architecture,omitempty"` + // OS is the operating system for this platform + OS string `yaml:"os,omitempty"` +} + // v0_1Configuration is a Version 0.1 Configuration struct // This is currently aliased to Configuration, as it is the current version type v0_1Configuration Configuration @@ -630,6 +619,62 @@ type Proxy struct { TTL *time.Duration `yaml:"ttl,omitempty"` } +type Validation struct { + // Enabled enables the other options in this section. This field is + // deprecated in favor of Disabled. + Enabled bool `yaml:"enabled,omitempty"` + // Disabled disables the other options in this section. + Disabled bool `yaml:"disabled,omitempty"` + // Manifests configures manifest validation. + Manifests ValidationManifests `yaml:"manifests,omitempty"` + // ImageIndexes configures validation of image indexes + ImageIndexes ValidationImageIndexes `yaml:"imageindexes,omitempty"` +} + +type ValidationManifests struct { + // URLs configures validation for URLs in pushed manifests. + URLs struct { + // Allow specifies regular expressions (https://godoc.org/regexp/syntax) + // that URLs in pushed manifests must match. + Allow []string `yaml:"allow,omitempty"` + // Deny specifies regular expressions (https://godoc.org/regexp/syntax) + // that URLs in pushed manifests must not match. + Deny []string `yaml:"deny,omitempty"` + } `yaml:"urls,omitempty"` +} + +type ValidationImageIndexes struct { + // PlatformsExist configures the validation applies to the platform images included in an image index + PlatformsExist PlatformsExist `yaml:"platformsexist"` + // SelectedPlatforms filters the set of platforms to validate for image existence. + SelectedPlatforms []Platform `yaml:"selectedplatforms,omitempty"` +} + +// PlatformsExist configures the validation applies to the platform images included in an image index +// This can be all, none, or selected +type PlatformsExist string + +// UnmarshalYAML implements the yaml.Umarshaler interface +// Unmarshals a string into a PlatformsExist option, lowercasing the string and validating that it represents a +// valid option +func (platformsexist *PlatformsExist) UnmarshalYAML(unmarshal func(interface{}) error) error { + var platformsExistString string + err := unmarshal(&platformsExistString) + if err != nil { + return err + } + + platformsExistString = strings.ToLower(platformsExistString) + switch platformsExistString { + case "all", "none", "selected": + default: + return fmt.Errorf("invalid platformsexist option %s Must be one of [all, none, selected]", platformsExistString) + } + + *platformsexist = PlatformsExist(platformsExistString) + return nil +} + // Parse parses an input configuration yaml document into a Configuration struct // This should generally be capable of handling old configuration format versions // diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 0ac8dbdcac3..95a8325cd0a 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -144,6 +144,11 @@ var configStruct = Configuration{ ReadTimeout: time.Millisecond * 10, WriteTimeout: time.Millisecond * 10, }, + Validation: Validation{ + ImageIndexes: ValidationImageIndexes{ + PlatformsExist: "none", + }, + }, } // configYamlV0_1 is a Version 0.1 yaml document representing configStruct @@ -197,6 +202,9 @@ redis: dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms +validation: + imageindexes: + platformsexist: none ` // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory @@ -226,6 +234,9 @@ notifications: http: headers: X-Content-Type-Options: [nosniff] +validation: + imageindexes: + platformsexist: none ` type ConfigSuite struct { @@ -284,6 +295,7 @@ func (suite *ConfigSuite) TestParseIncomplete(c *check.C) { suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.HTTP.Headers = nil suite.expectedConfig.Redis = Redis{} + suite.expectedConfig.Validation.ImageIndexes.PlatformsExist = "" // Note: this also tests that REGISTRY_STORAGE and // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together @@ -551,5 +563,12 @@ func copyConfig(config Configuration) *Configuration { configCopy.Redis = config.Redis + configCopy.Validation = Validation{ + Enabled: config.Validation.Enabled, + Disabled: config.Validation.Disabled, + Manifests: config.Validation.Manifests, + ImageIndexes: config.Validation.ImageIndexes, + } + return configCopy } diff --git a/docs/configuration.md b/docs/configuration.md index d5e04ba37b3..b959a7d5a47 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -283,6 +283,11 @@ validation: - ^https?://([^/]+\.)*example\.com/ deny: - ^https?://www\.example\.com/ + imageindex: + platformsexist: Selected + platforms: + - architecture: amd64 + os: linux ``` In some instances a configuration option is **optional** but it contains child @@ -1110,16 +1115,16 @@ username (such as `batman`) and the password for that username. ## `validation` -```none +```yaml validation: - manifests: - urls: - allow: - - ^https?://([^/]+\.)*example\.com/ - deny: - - ^https?://www\.example\.com/ + disabled: false ``` +Use these settings to configure what validation the registry performs on content. + +Validation is performed when content is uploaded to the registry. Changing these +settings will not validate content that has already been accepting into the registry. + ### `disabled` The `disabled` flag disables the other options in the `validation` @@ -1132,6 +1137,16 @@ Use the `manifests` subsection to configure validation of manifests. If #### `urls` +```yaml +validation: + manifests: + urls: + allow: + - ^https?://([^/]+\.)*example\.com/ + deny: + - ^https?://www\.example\.com/ +``` + The `allow` and `deny` options are each a list of [regular expressions](https://pkg.go.dev/regexp/syntax) that restrict the URLs in pushed manifests. @@ -1145,6 +1160,53 @@ one of the `allow` regular expressions **and** one of the following holds: 2. `deny` is set but no URLs within the manifest match any of the `deny` regular expressions. +### `imageindexes` + +By default the registry will validate that all platform images exist when an image +index is uploaded to the registry. Disabling this validatation is experimental +because other tooling that uses the registry may expect the image index to be complete. + +validation: + imageindexes: + platformsexist: [All|None|Selected] + selectedplatforms: + - os: linux + architecture: amd64 + +Use these settings to configure what validation the registry performs on image +index manifests uploaded to the registry. + +#### `platformsexist` + +Set `platformexist` to `all` (the default) to validate all platform images exist. +The registry will validate that the images referenced by the index exist in the +registry before accepting the image index. + +Set `platformsexist` to `none` to disable all validation that images exist when an +image index manifest is uploaded. This allows image lists to be uploaded to the +registry without their associated images. This setting is experimental because +other tooling that uses the registry may expect the image index to be complete. + +Set `platformsexist` to `selected` to selectively validate the existence of platforms +within image index manifests. This setting is experimental because other tooling +that uses the registry may expect the image index to be complete. + +#### `selectedplatforms` + +When `platformsexist` is set to `selected`, set `selectedplatforms` to an array of +platforms to validate. If a platform is included in this the array and in the images +contained within an index, the registry will validate that the platform specific image +exists in the registry before accepting the index. The registry will not validate the +existence of platform specific images in the index that do not appear in the +`selectedplatforms` array. + +This parameter does not validate that the configured platforms are included in every +index. If an image index does not include one of the platform specific images configured +in the `selectedplatforms` array, it may still be accepted by the registry. + +Each platform is a map with two keys, `os` and `architecture`, as defined in the +[OCI Image Index specification](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions). + ## Example: Development configuration You can use this simple example for local development: diff --git a/manifests.go b/manifests.go index 8f84a220a97..f38d3ce8b15 100644 --- a/manifests.go +++ b/manifests.go @@ -47,7 +47,7 @@ type ManifestBuilder interface { AppendReference(dependency Describable) error } -// ManifestService describes operations on image manifests. +// ManifestService describes operations on manifests. type ManifestService interface { // Exists returns true if the manifest exists. Exists(ctx context.Context, dgst digest.Digest) (bool, error) diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go index ae0c67cf64e..78eb4401111 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -2432,7 +2432,7 @@ func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLB func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) { if resp.StatusCode != expectedStatus { - t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus) + t.Logf("unexpected status %s: expected %v, got %v", msg, resp.StatusCode, expectedStatus) maybeDumpResponse(t, resp) t.FailNow() } diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 8efdaf85e3c..e18368f9c98 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -234,6 +234,21 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { options = append(options, storage.ManifestURLsDenyRegexp(re)) } } + + switch config.Validation.ImageIndexes.PlatformsExist { + case "selected": + options = append(options, storage.EnableValidateImageIndexImagesExist) + for _, platform := range config.Validation.ImageIndexes.SelectedPlatforms { + options = append(options, storage.AddValidateImageIndexImagesExistPlatform(platform.Architecture, platform.OS)) + } + fallthrough + case "none": + dcontext.GetLogger(app).Warnf("Image index completeness validation has been disabled, which is an experimental option because other container tooling might expect all image indexes to be complete") + case "all": + fallthrough + default: + options = append(options, storage.EnableValidateImageIndexImagesExist) + } } // configure storage caches diff --git a/registry/storage/manifestlisthandler.go b/registry/storage/manifestlisthandler.go index 1fc7aac7a0e..27dab0e32ed 100644 --- a/registry/storage/manifestlisthandler.go +++ b/registry/storage/manifestlisthandler.go @@ -13,9 +13,11 @@ import ( // manifestListHandler is a ManifestHandler that covers schema2 manifest lists. type manifestListHandler struct { - repository distribution.Repository - blobStore distribution.BlobStore - ctx context.Context + repository distribution.Repository + blobStore distribution.BlobStore + ctx context.Context + validateImagesExist bool + validateImagesExistPlatforms []platform // [] = All platforms } var _ ManifestHandler = &manifestListHandler{} @@ -74,7 +76,7 @@ func (ms *manifestListHandler) Put(ctx context.Context, manifestList distributio func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst distribution.Manifest, skipDependencyVerification bool) error { var errs distribution.ErrManifestVerification - if !skipDependencyVerification { + if ms.validateImagesExist && !skipDependencyVerification { // This manifest service is different from the blob service // returned by Blob. It uses a linked blob store to ensure that // only manifests are accessible. @@ -85,6 +87,10 @@ func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst distrib } for _, manifestDescriptor := range mnfst.References() { + if !ms.platformMustExist(manifestDescriptor) { + continue + } + exists, err := manifestService.Exists(ctx, manifestDescriptor.Digest) if err != nil && err != distribution.ErrBlobUnknown { errs = append(errs, err) @@ -101,3 +107,20 @@ func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst distrib return nil } + +func (ms *manifestListHandler) platformMustExist(descriptor distribution.Descriptor) bool { + if len(ms.validateImagesExistPlatforms) == 0 { + return true + } + + imagePlatform := descriptor.Platform + + for _, platform := range ms.validateImagesExistPlatforms { + if imagePlatform.Architecture == platform.architecture && + imagePlatform.OS == platform.os { + return true + } + } + + return false +} diff --git a/registry/storage/manifeststore_test.go b/registry/storage/manifeststore_test.go index 82a6c97dbe2..d8b6a091283 100644 --- a/registry/storage/manifeststore_test.go +++ b/registry/storage/manifeststore_test.go @@ -54,7 +54,7 @@ func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string, opt } func TestManifestStorage(t *testing.T) { - testManifestStorage(t, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect) + testManifestStorage(t, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect, EnableValidateImageIndexImagesExist) } func testManifestStorage(t *testing.T, options ...RegistryOption) { @@ -312,7 +312,7 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo repoName, _ := reference.WithName("foo/bar") env := newManifestStoreTestEnv(t, repoName, "thetag", BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), - EnableDelete, EnableRedirect) + EnableDelete, EnableRedirect, EnableValidateImageIndexImagesExist) ctx := context.Background() ms, err := env.repository.Manifests(ctx) @@ -320,44 +320,36 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo t.Fatal(err) } - // Build a manifest and store it and its layers in the registry + // Build a manifest and store its layers in the registry blobStore := env.repository.Blobs(ctx) - builder := ocischema.NewManifestBuilder(blobStore, []byte{}, map[string]string{}) - err = builder.(*ocischema.Builder).SetMediaType(imageMediaType) + mfst, err := createRandomImage(t, testname, imageMediaType, blobStore) if err != nil { - t.Fatal(err) + t.Fatalf("%s: unexpected error generating random image: %v", testname, err) } - // Add some layers - for i := 0; i < 2; i++ { - rs, dgst, err := testutil.CreateRandomTarFile() - if err != nil { - t.Fatalf("%s: unexpected error generating test layer file", testname) - } + // create an image index - wr, err := env.repository.Blobs(env.ctx).Create(env.ctx) - if err != nil { - t.Fatalf("%s: unexpected error creating test upload: %v", testname, err) - } - - if _, err := io.Copy(wr, rs); err != nil { - t.Fatalf("%s: unexpected error copying to upload: %v", testname, err) - } - - if _, err := wr.Commit(env.ctx, distribution.Descriptor{Digest: dgst}); err != nil { - t.Fatalf("%s: unexpected error finishing upload: %v", testname, err) - } + platformSpec := &v1.Platform{ + Architecture: "atari2600", + OS: "CP/M", + } - builder.AppendReference(distribution.Descriptor{Digest: dgst}) + mfstDescriptors := []distribution.Descriptor{ + createManifestDescriptor(t, testname, mfst, platformSpec), } - mfst, err := builder.Build(ctx) + imageIndex, err := ociIndexFromDesriptorsWithMediaType(mfstDescriptors, indexMediaType) if err != nil { - t.Fatalf("%s: unexpected error generating manifest: %v", testname, err) + t.Fatalf("%s: unexpected error creating image index: %v", testname, err) } - // before putting the manifest test for proper handling of SchemaVersion + _, err = ms.Put(ctx, imageIndex) + if err == nil { + t.Fatalf("%s: expected error putting image index without child manifests in the registry: %v", testname, err) + } + + // Test for proper handling of SchemaVersion for the image if mfst.(*ocischema.DeserializedManifest).Manifest.SchemaVersion != 2 { t.Fatalf("%s: unexpected error generating default version for oci manifest", testname) @@ -375,22 +367,7 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo } } - // Also create an image index that contains the manifest - - descriptor, err := env.registry.BlobStatter().Stat(ctx, manifestDigest) - if err != nil { - t.Fatalf("%s: unexpected error getting manifest descriptor", testname) - } - descriptor.MediaType = v1.MediaTypeImageManifest - descriptor.Platform = &v1.Platform{ - Architecture: "atari2600", - OS: "CP/M", - } - - imageIndex, err := ociIndexFromDesriptorsWithMediaType([]distribution.Descriptor{descriptor}, indexMediaType) - if err != nil { - t.Fatalf("%s: unexpected error creating image index: %v", testname, err) - } + // We can now push the index var indexDigest digest.Digest if indexDigest, err = ms.Put(ctx, imageIndex); err != nil { @@ -452,6 +429,198 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo } } +func TestIndexManifestStorageWithoutImageCheck(t *testing.T) { + imageMediaType := v1.MediaTypeImageManifest + indexMediaType := v1.MediaTypeImageIndex + + repoName, _ := reference.WithName("foo/bar") + env := newManifestStoreTestEnv(t, repoName, "thetag", + BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), + EnableDelete, EnableRedirect) + + ctx := context.Background() + ms, err := env.repository.Manifests(ctx) + if err != nil { + t.Fatal(err) + } + + // Build a manifest and store its layers in the registry + + blobStore := env.repository.Blobs(ctx) + manifest, err := createRandomImage(t, t.Name(), imageMediaType, blobStore) + if err != nil { + t.Fatalf("unexpected error generating random image: %v", err) + } + + // create an image index + + platformSpec := &v1.Platform{ + Architecture: "atari2600", + OS: "CP/M", + } + + manifestDescriptors := []distribution.Descriptor{ + createManifestDescriptor(t, t.Name(), manifest, platformSpec), + } + + imageIndex, err := ociIndexFromDesriptorsWithMediaType(manifestDescriptors, indexMediaType) + if err != nil { + t.Fatalf("unexpected error creating image index: %v", err) + } + + // We should be able to put the index without having put the image + + _, err = ms.Put(ctx, imageIndex) + if err != nil { + t.Fatalf("unexpected error putting sparse image index: %v", err) + } +} + +func TestIndexManifestStorageWithSelectivePlatforms(t *testing.T) { + imageMediaType := v1.MediaTypeImageManifest + indexMediaType := v1.MediaTypeImageIndex + + repoName, _ := reference.WithName("foo/bar") + env := newManifestStoreTestEnv(t, repoName, "thetag", + BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), + EnableDelete, EnableRedirect, EnableValidateImageIndexImagesExist, + AddValidateImageIndexImagesExistPlatform("amd64", "linux")) + + ctx := context.Background() + ms, err := env.repository.Manifests(ctx) + if err != nil { + t.Fatal(err) + } + + // Build a manifests their layers in the registry + + blobStore := env.repository.Blobs(ctx) + amdManifest, err := createRandomImage(t, t.Name(), imageMediaType, blobStore) + if err != nil { + t.Fatalf("%s: unexpected error generating random image: %v", t.Name(), err) + } + armManifest, err := createRandomImage(t, t.Name(), imageMediaType, blobStore) + if err != nil { + t.Fatalf("%s: unexpected error generating random image: %v", t.Name(), err) + } + atariManifest, err := createRandomImage(t, t.Name(), imageMediaType, blobStore) + if err != nil { + t.Fatalf("%s: unexpected error generating random image: %v", t.Name(), err) + } + + // create an image index + + amdPlatformSpec := &v1.Platform{ + Architecture: "amd64", + OS: "linux", + } + armPlatformSpec := &v1.Platform{ + Architecture: "arm", + OS: "plan9", + } + atariPlatformSpec := &v1.Platform{ + Architecture: "atari2600", + OS: "CP/M", + } + + manifestDescriptors := []distribution.Descriptor{ + createManifestDescriptor(t, t.Name(), amdManifest, amdPlatformSpec), + createManifestDescriptor(t, t.Name(), armManifest, armPlatformSpec), + createManifestDescriptor(t, t.Name(), atariManifest, atariPlatformSpec), + } + + imageIndex, err := ociIndexFromDesriptorsWithMediaType(manifestDescriptors, indexMediaType) + if err != nil { + t.Fatalf("unexpected error creating image index: %v", err) + } + + // Test we can't push with no image manifests existing in the registry + + _, err = ms.Put(ctx, imageIndex) + if err == nil { + t.Fatalf("expected error putting image index without existing images: %v", err) + } + + // Test we can't push with a manifest but not the right one + + _, err = ms.Put(ctx, atariManifest) + if err != nil { + t.Fatalf("unexpected error putting manifest: %v", err) + } + + _, err = ms.Put(ctx, imageIndex) + if err == nil { + t.Fatalf("expected error putting image index without correct existing images: %v", err) + } + + // Test we can push with the right manifest + + _, err = ms.Put(ctx, amdManifest) + if err != nil { + t.Fatalf("unexpected error putting manifest: %v", err) + } + + _, err = ms.Put(ctx, imageIndex) + if err != nil { + t.Fatalf("unexpected error putting image index: %v", err) + } +} + +// createRandomImage builds an image manifest and store it and its layers in the registry +func createRandomImage(t *testing.T, testname string, imageMediaType string, blobStore distribution.BlobStore) (distribution.Manifest, error) { + builder := ocischema.NewManifestBuilder(blobStore, []byte{}, map[string]string{}) + err := builder.(*ocischema.Builder).SetMediaType(imageMediaType) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + + // Add some layers + for i := 0; i < 2; i++ { + rs, dgst, err := testutil.CreateRandomTarFile() + if err != nil { + t.Fatalf("%s: unexpected error generating test layer file", testname) + } + + wr, err := blobStore.Create(ctx) + if err != nil { + t.Fatalf("%s: unexpected error creating test upload: %v", testname, err) + } + + if _, err := io.Copy(wr, rs); err != nil { + t.Fatalf("%s: unexpected error copying to upload: %v", testname, err) + } + + if _, err := wr.Commit(ctx, distribution.Descriptor{Digest: dgst}); err != nil { + t.Fatalf("%s: unexpected error finishing upload: %v", testname, err) + } + + builder.AppendReference(distribution.Descriptor{Digest: dgst}) + } + + return builder.Build(ctx) +} + +// createManifestDescriptor builds a manifest descriptor from a manifest and a platform descriptor +func createManifestDescriptor(t *testing.T, testname string, manifest distribution.Manifest, platformSpec *v1.Platform) distribution.Descriptor { + manifestMediaType, manifestPayload, err := manifest.Payload() + if err != nil { + t.Fatalf("%s: unexpected error getting manifest payload: %v", testname, err) + } + manifestDigest := digest.FromBytes(manifestPayload) + + return distribution.Descriptor{ + Digest: manifestDigest, + Size: int64(len(manifestPayload)), + MediaType: manifestMediaType, + Platform: &v1.Platform{ + Architecture: platformSpec.Architecture, + OS: platformSpec.OS, + }, + } +} + // TestLinkPathFuncs ensures that the link path functions behavior are locked // down and implemented as expected. func TestLinkPathFuncs(t *testing.T) { diff --git a/registry/storage/registry.go b/registry/storage/registry.go index 49b604f2120..8890e38fb38 100644 --- a/registry/storage/registry.go +++ b/registry/storage/registry.go @@ -20,8 +20,12 @@ type registry struct { deleteEnabled bool resumableDigestEnabled bool blobDescriptorServiceFactory distribution.BlobDescriptorServiceFactory - manifestURLs manifestURLs driver storagedriver.StorageDriver + + // Validation + manifestURLs manifestURLs + validateImageIndexImagesExist bool + validateImageIndexImagesExistPlatforms []platform // [] = All platforms } // manifestURLs holds regular expressions for controlling manifest URL whitelisting @@ -30,6 +34,12 @@ type manifestURLs struct { deny *regexp.Regexp } +// platform represents a platform to validate exists in the +type platform struct { + architecture string + os string +} + // RegistryOption is the type used for functional options for NewRegistry. type RegistryOption func(*registry) error @@ -70,6 +80,28 @@ func ManifestURLsDenyRegexp(r *regexp.Regexp) RegistryOption { } } +// EnableValidateImageIndexImagesExist is a functional option for NewRegistry. It enables +// validation that references exist before an image index is accepted. +func EnableValidateImageIndexImagesExist(registry *registry) error { + registry.validateImageIndexImagesExist = true + return nil +} + +// AddValidateImageIndexImagesExistPlatform returns a functional option for NewRegistry. +// It adds a platform to check for existence before an image index is accepted. +func AddValidateImageIndexImagesExistPlatform(architecture string, os string) RegistryOption { + return func(registry *registry) error { + registry.validateImageIndexImagesExistPlatforms = append( + registry.validateImageIndexImagesExistPlatforms, + platform{ + architecture: architecture, + os: os, + }, + ) + return nil + } +} + // BlobDescriptorServiceFactory returns a functional option for NewRegistry. It sets the // factory to create BlobDescriptorServiceFactory middleware. func BlobDescriptorServiceFactory(factory distribution.BlobDescriptorServiceFactory) RegistryOption { @@ -222,9 +254,11 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M } manifestListHandler := &manifestListHandler{ - ctx: ctx, - repository: repo, - blobStore: blobStore, + ctx: ctx, + repository: repo, + blobStore: blobStore, + validateImagesExist: repo.validateImageIndexImagesExist, + validateImagesExistPlatforms: repo.validateImageIndexImagesExistPlatforms, } ms := &manifestStore{ diff --git a/registry/storage/schema2manifesthandler_test.go b/registry/storage/schema2manifesthandler_test.go index 908f8c137a4..f670919dada 100644 --- a/registry/storage/schema2manifesthandler_test.go +++ b/registry/storage/schema2manifesthandler_test.go @@ -18,7 +18,9 @@ func TestVerifyManifestForeignLayer(t *testing.T) { inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver, ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")), - ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope"))) + ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope")), + EnableValidateImageIndexImagesExist, + ) repo := makeRepository(t, registry, "test") manifestService := makeManifestService(t, repo) @@ -156,7 +158,9 @@ func TestVerifyManifestBlobLayerAndConfig(t *testing.T) { inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver, ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")), - ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope"))) + ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope")), + EnableValidateImageIndexImagesExist, + ) repo := makeRepository(t, registry, strings.ToLower(t.Name())) manifestService := makeManifestService(t, repo)