From c40c4b289ad1575d450da47476f335a724db370b Mon Sep 17 00:00:00 2001 From: James Hewitt Date: Tue, 15 Aug 2023 14:37:43 +0100 Subject: [PATCH] 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 --- configuration/configuration.go | 83 +++-- configuration/configuration_test.go | 22 ++ docs/content/about/configuration.md | 75 ++++- manifests.go | 2 +- registry/handlers/api_test.go | 2 +- registry/handlers/app.go | 15 + registry/storage/manifestlisthandler.go | 52 ++- registry/storage/manifeststore_test.go | 308 +++++++++++++++--- registry/storage/registry.go | 48 ++- .../storage/schema2manifesthandler_test.go | 8 +- 10 files changed, 520 insertions(+), 95 deletions(-) diff --git a/configuration/configuration.go b/configuration/configuration.go index aa74e3cb4..0dbf68c0d 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -181,25 +181,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 { @@ -366,6 +348,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 @@ -653,6 +642,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"` +} + +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"` + // ImageIndexes configures validation of image indexes + Indexes ValidationIndexes `yaml:"indexes,omitempty"` +} + +type ValidationIndexes struct { + // Platforms configures the validation applies to the platform images included in an image index + Platforms Platforms `yaml:"platforms"` + // PlatformList filters the set of platforms to validate for image existence. + PlatformList []Platform `yaml:"platformlist,omitempty"` +} + +// Platforms configures the validation applies to the platform images included in an image index +// This can be all, none, or list +type Platforms string + +// UnmarshalYAML implements the yaml.Umarshaler interface +// Unmarshals a string into a Platforms option, lowercasing the string and validating that it represents a +// valid option +func (platforms *Platforms) UnmarshalYAML(unmarshal func(interface{}) error) error { + var platformsString string + err := unmarshal(&platformsString) + if err != nil { + return err + } + + platformsString = strings.ToLower(platformsString) + switch platformsString { + case "all", "none", "list": + default: + return fmt.Errorf("invalid platforms option %s Must be one of [all, none, list]", platformsString) + } + + *platforms = Platforms(platformsString) + 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 e3bf1bf1c..7e0a1e22e 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -151,6 +151,13 @@ var configStruct = Configuration{ ReadTimeout: time.Millisecond * 10, WriteTimeout: time.Millisecond * 10, }, + Validation: Validation{ + Manifests: ValidationManifests{ + Indexes: ValidationIndexes{ + Platforms: "none", + }, + }, + }, } // configYamlV0_1 is a Version 0.1 yaml document representing configStruct @@ -206,6 +213,10 @@ redis: dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms +validation: + manifests: + indexes: + platforms: none ` // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory @@ -235,6 +246,10 @@ notifications: http: headers: X-Content-Type-Options: [nosniff] +validation: + manifests: + indexes: + platforms: none ` type ConfigSuite struct { @@ -295,6 +310,7 @@ func (suite *ConfigSuite) TestParseIncomplete() { suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.HTTP.Headers = nil suite.expectedConfig.Redis = Redis{} + suite.expectedConfig.Validation.Manifests.Indexes.Platforms = "" // Note: this also tests that REGISTRY_STORAGE and // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together @@ -566,5 +582,11 @@ func copyConfig(config Configuration) *Configuration { configCopy.Redis = config.Redis + configCopy.Validation = Validation{ + Enabled: config.Validation.Enabled, + Disabled: config.Validation.Disabled, + Manifests: config.Validation.Manifests, + } + return configCopy } diff --git a/docs/content/about/configuration.md b/docs/content/about/configuration.md index e173388ac..d6c508470 100644 --- a/docs/content/about/configuration.md +++ b/docs/content/about/configuration.md @@ -288,6 +288,11 @@ validation: - ^https?://([^/]+\.)*example\.com/ deny: - ^https?://www\.example\.com/ + indexes: + platforms: List + platformlist: + - architecture: amd64 + os: linux ``` In some instances a configuration option is **optional** but it contains child @@ -1160,14 +1165,14 @@ username (such as `batman`) and the password for that username. ```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` @@ -1180,6 +1185,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. @@ -1193,6 +1208,54 @@ 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. +#### `indexes` + +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: + manifests: + indexes: + platforms: [all|none|list] + platformlist: + - os: linux + architecture: amd64 + +Use these settings to configure what validation the registry performs on image +index manifests uploaded to the registry. + +##### `platforms` + +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 `platforms` 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 `platforms` to `list` 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. + +##### `platformlist` + +When `platforms` is set to `list`, set `platformlist` 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 +`platformlist` 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 `platformlist` 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 8f84a220a..f38d3ce8b 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 dc5a57ce1..43d3ae146 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -2514,7 +2514,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 91c56e762..946c415b1 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -255,6 +255,21 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { options = append(options, storage.ManifestURLsDenyRegexp(re)) } } + + switch config.Validation.Manifests.Indexes.Platforms { + case "list": + options = append(options, storage.EnableValidateImageIndexImagesExist) + for _, platform := range config.Validation.Manifests.Indexes.PlatformList { + options = append(options, storage.AddValidateImageIndexImagesExistPlatform(platform.Architecture, platform.OS)) + } + fallthrough + case "none": + dcontext.GetLogger(app).Warn("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 caaf76bb0..a1da46322 100644 --- a/registry/storage/manifestlisthandler.go +++ b/registry/storage/manifestlisthandler.go @@ -13,9 +13,10 @@ 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 + validateImageIndexes validateImageIndexes } var _ ManifestHandler = &manifestListHandler{} @@ -74,24 +75,24 @@ 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 { - // 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. - + // Check if we should be validating the existence of any child images in images indexes + if ms.validateImageIndexes.imagesExist && !skipDependencyVerification { + // Get the manifest service we can use to check for the existence of child images manifestService, err := ms.repository.Manifests(ctx) if err != nil { return err } for _, manifestDescriptor := range mnfst.References() { - exists, err := manifestService.Exists(ctx, manifestDescriptor.Digest) - if err != nil && err != distribution.ErrBlobUnknown { - errs = append(errs, err) - } - if err != nil || !exists { - // On error here, we always append unknown blob errors. - errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: manifestDescriptor.Digest}) + if ms.platformMustExist(manifestDescriptor) { + exists, err := manifestService.Exists(ctx, manifestDescriptor.Digest) + if err != nil && err != distribution.ErrBlobUnknown { + errs = append(errs, err) + } + if err != nil || !exists { + // On error here, we always append unknown blob errors. + errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: manifestDescriptor.Digest}) + } } } } @@ -101,3 +102,24 @@ func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst distrib return nil } + +// platformMustExist checks if a descriptor within an index should be validated as existing before accepting the manifest into the registry. +func (ms *manifestListHandler) platformMustExist(descriptor distribution.Descriptor) bool { + // If there are no image platforms configured to validate, we must check the existence of all child images. + if len(ms.validateImageIndexes.imagePlatforms) == 0 { + return true + } + + imagePlatform := descriptor.Platform + + // If the platform matches a platform that is configured to validate, we must check the existence. + for _, platform := range ms.validateImageIndexes.imagePlatforms { + if imagePlatform.Architecture == platform.architecture && + imagePlatform.OS == platform.os { + return true + } + } + + // If the platform doesn't match a platform configured to validate, we don't need to check the existence. + return false +} diff --git a/registry/storage/manifeststore_test.go b/registry/storage/manifeststore_test.go index 3c1cb9dde..1b071df25 100644 --- a/registry/storage/manifeststore_test.go +++ b/registry/storage/manifeststore_test.go @@ -10,6 +10,7 @@ import ( "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/manifest" + "github.com/distribution/distribution/v3/manifest/manifestlist" "github.com/distribution/distribution/v3/manifest/ocischema" "github.com/distribution/distribution/v3/manifest/schema2" "github.com/distribution/distribution/v3/registry/storage/cache/memory" @@ -54,7 +55,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) { @@ -314,7 +315,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) @@ -322,46 +323,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) - } - - if err := builder.AppendReference(distribution.Descriptor{Digest: dgst, MediaType: v1.MediaTypeImageLayer}); err != nil { - t.Fatalf("%s unexpected error appending references: %v", testname, err) - } + platformSpec := &v1.Platform{ + Architecture: "atari2600", + OS: "CP/M", } - mfst, err := builder.Build(ctx) + mfstDescriptors := []distribution.Descriptor{ + createOciManifestDescriptor(t, testname, mfst, platformSpec), + } + + 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) @@ -379,22 +370,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 { @@ -456,6 +432,244 @@ 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 + + ociPlatformSpec := &v1.Platform{ + Architecture: "atari2600", + OS: "CP/M", + } + + ociManifestDescriptors := []distribution.Descriptor{ + createOciManifestDescriptor(t, t.Name(), manifest, ociPlatformSpec), + } + + imageIndex, err := ociIndexFromDesriptorsWithMediaType(ociManifestDescriptors, 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 OCI image index: %v", err) + } + + // same for a manifest list + + listPlatformSpec := &manifestlist.PlatformSpec{ + Architecture: "atari2600", + OS: "CP/M", + } + + listManifestDescriptors := []manifestlist.ManifestDescriptor{ + createManifestListDescriptor(t, t.Name(), manifest, listPlatformSpec), + } + + list, err := manifestlist.FromDescriptors(listManifestDescriptors) + if err != nil { + t.Fatalf("unexpected error creating manifest list: %v", err) + } + + // We should be able to put the list without having put the image + + _, err = ms.Put(ctx, list) + if err != nil { + t.Fatalf("unexpected error putting sparse manifest list: %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{ + createOciManifestDescriptor(t, t.Name(), amdManifest, amdPlatformSpec), + createOciManifestDescriptor(t, t.Name(), armManifest, armPlatformSpec), + createOciManifestDescriptor(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) + } + + if err := builder.AppendReference(distribution.Descriptor{Digest: dgst, MediaType: v1.MediaTypeImageLayer}); err != nil { + t.Fatalf("%s unexpected error appending references: %v", testname, err) + } + } + + return builder.Build(ctx) +} + +// createOciManifestDescriptor builds a manifest descriptor from a manifest and a platform descriptor +func createOciManifestDescriptor(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, + }, + } +} + +// createManifestListDescriptor builds a manifest descriptor from a manifest and a platform descriptor +func createManifestListDescriptor(t *testing.T, testname string, manifest distribution.Manifest, platformSpec *manifestlist.PlatformSpec) manifestlist.ManifestDescriptor { + manifestMediaType, manifestPayload, err := manifest.Payload() + if err != nil { + t.Fatalf("%s: unexpected error getting manifest payload: %v", testname, err) + } + manifestDigest := digest.FromBytes(manifestPayload) + + return manifestlist.ManifestDescriptor{ + Descriptor: distribution.Descriptor{ + Digest: manifestDigest, + Size: int64(len(manifestPayload)), + MediaType: manifestMediaType, + }, + Platform: manifestlist.PlatformSpec{ + 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 5bc5295c9..ab8fb1f9a 100644 --- a/registry/storage/registry.go +++ b/registry/storage/registry.go @@ -26,8 +26,11 @@ type registry struct { tagLookupConcurrencyLimit int resumableDigestEnabled bool blobDescriptorServiceFactory distribution.BlobDescriptorServiceFactory - manifestURLs manifestURLs driver storagedriver.StorageDriver + + // Validation + manifestURLs manifestURLs + validateImageIndexes validateImageIndexes } // manifestURLs holds regular expressions for controlling manifest URL whitelisting @@ -36,6 +39,20 @@ type manifestURLs struct { deny *regexp.Regexp } +// validateImageIndexImages holds configuration for validation of image indexes +type validateImageIndexes struct { + // exist can be used to disable checking that platform images exist entirely. Default true. + imagesExist bool + // platforms can be used to only validate the existence of images for a set of platforms. The empty array means validate all platforms. + imagePlatforms []platform +} + +// 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 @@ -83,6 +100,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.validateImageIndexes.imagesExist = 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.validateImageIndexes.imagePlatforms = append( + registry.validateImageIndexes.imagePlatforms, + 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 { @@ -240,9 +279,10 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M } manifestListHandler := &manifestListHandler{ - ctx: ctx, - repository: repo, - blobStore: blobStore, + ctx: ctx, + repository: repo, + blobStore: blobStore, + validateImageIndexes: repo.validateImageIndexes, } ms := &manifestStore{ diff --git a/registry/storage/schema2manifesthandler_test.go b/registry/storage/schema2manifesthandler_test.go index 26c19ec12..6b918a807 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)