diff --git a/docs/containers-registries.conf.5.md b/docs/containers-registries.conf.5.md index 928387b69a..d68d60d69a 100644 --- a/docs/containers-registries.conf.5.md +++ b/docs/containers-registries.conf.5.md @@ -108,16 +108,24 @@ Each TOML table in the `mirror` array can contain the following fields, with the as if specified in the `[[registry]]` TOML table directly: - `location` - `insecure` +- `digest-only` `mirror-by-digest-only` : `true` or `false`. If `true`, mirrors will only be used during pulling if the image reference includes a digest. +Note that if this is `true`, images referenced by a tag will only use the primary +registry, failing if that registry is not accessible. + +`digest-only`: `true` or `false`. +: if `true` an individual mirror will only be used during pulling if the image reference includes a digest. +Note that digest-only will be overridden if mirror-by-digest is set for the entire registry. +Also note that if all mirrors in the `mirror` array have `digest-only = true`, images referenced by a tag will only use the primary +registry, failing if that registry is not accessible. + Referencing an image by digest ensures that the same is always used (whereas referencing an image by a tag may cause different registries to return different images if the tag mapping is out of sync). -Note that if this is `true`, images referenced by a tag will only use the primary -registry, failing if that registry is not accessible. *Note*: Redirection and mirrors are currently processed only when reading images, not when pushing to a registry; that may change in the future. diff --git a/pkg/sysregistriesv2/system_registries_v2.go b/pkg/sysregistriesv2/system_registries_v2.go index c8a603c4ef..a7f2007130 100644 --- a/pkg/sysregistriesv2/system_registries_v2.go +++ b/pkg/sysregistriesv2/system_registries_v2.go @@ -53,6 +53,12 @@ type Endpoint struct { // If true, certs verification will be skipped and HTTP (non-TLS) // connections will be allowed. Insecure bool `toml:"insecure,omitempty"` + // If true, the mirror will only be used for digest pulls. Pulling images by + // tag can potentially yield different images, depending on which endpoint + // we pull from. Forcing digest-pulls for mirrors avoids that issue. + // This only applies to the mirror of the primary registry. + // If mirror-by-digest-only is set for the primary registry, mirror-by-digest-only is honored. + DigestOnly bool `toml:"digest-only,omitempty"` } // userRegistriesFile is the path to the per user registry configuration file. @@ -130,16 +136,27 @@ type PullSource struct { // reference. func (r *Registry) PullSourcesFromReference(ref reference.Named) ([]PullSource, error) { var endpoints []Endpoint - + _, isDigested := ref.(reference.Canonical) if r.MirrorByDigestOnly { - // Only use mirrors when the reference is a digest one. - if _, isDigested := ref.(reference.Canonical); isDigested { + // Only use mirrors when the reference is a digested one. + if isDigested { endpoints = append(r.Mirrors, r.Endpoint) } else { endpoints = []Endpoint{r.Endpoint} } } else { - endpoints = append(r.Mirrors, r.Endpoint) + // check digest-only for each mirror if non mirror-by-digest-only set, since mirror-by-digest-only is honored + for _, mirror := range r.Mirrors { + if mirror.DigestOnly { + if isDigested { + // Only use the mirror when the reference is a digested one. + endpoints = append(endpoints, mirror) + } + } else { + endpoints = append(endpoints, mirror) + } + } + endpoints = append(endpoints, r.Endpoint) } sources := []PullSource{} @@ -919,15 +936,19 @@ func loadConfigFile(path string, forceV2 bool) (*parsedConfig, error) { res.shortNameMode = types.ShortNameModeInvalid } - // Valid wildcarded prefixes must be in the format: *.example.com - // FIXME: Move to postProcessRegistries - // https://github.com/containers/image/pull/1191#discussion_r610623829 for i := range res.partialV2.Registries { + // Valid wildcarded prefixes must be in the format: *.example.com + // FIXME: Move to postProcessRegistries + // https://github.com/containers/image/pull/1191#discussion_r610623829 prefix := res.partialV2.Registries[i].Prefix if strings.HasPrefix(prefix, "*.") && strings.ContainsAny(prefix, "/@:") { msg := fmt.Sprintf("Wildcarded prefix should be in the format: *.example.com. Current prefix %q is incorrectly formatted", prefix) return nil, &InvalidRegistries{s: msg} } + // validate the mirror usage settings does not apply to primary registry + if res.partialV2.Registries[i].DigestOnly { + return nil, fmt.Errorf("digest-only does not apply to primary registry %s", prefix) + } } // Parse and validate short-name aliases. diff --git a/pkg/sysregistriesv2/system_registries_v2_test.go b/pkg/sysregistriesv2/system_registries_v2_test.go index 0c3f141b0e..c366561a9f 100644 --- a/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/pkg/sysregistriesv2/system_registries_v2_test.go @@ -675,6 +675,72 @@ func TestPullSourcesFromReference(t *testing.T) { assert.Equal(t, 1, len(pullSources)) } +func TestPullSourcesMirrorFromReference(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/pull-sources-mirror-reference.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + registries, err := GetRegistries(sys) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) + + for _, tc := range []struct { + registry string + digestRef string + digestSources int + tagRef string + tagSources int + }{ + // Registry A has mirrors allow any kind of pull + { + "registry-a.com/foo/image:latest", + "registry-a.com/foo/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + 3, + "registry-a.com/foo/image:aaa", + 3, + }, + // Registry B has mirrors allow digests pull only + { + "registry-b.com/foo/image:latest", + "registry-b.com/foo/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + 3, + "registry-b.com/foo/image:aaa", + 1, + }, + // Registry C has a mirror allows digest pull only and a mirror allows any kind of pull + { + "registry-c.com/foo/image:latest", + "registry-c.com/foo/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + 3, + "registry-c.com/foo/image:aaa", + 2, + }, + // Registry D set mirror-by-digest-only, honor this configuration for registry + // Regsitry D has no digest-only set for mirrors table + { + "registry-d.com/foo/image:latest", + "registry-d.com/foo/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + 3, + "registry-d.com/foo/image:aaa", + 1, + }, + } { + registry, err := FindRegistry(sys, tc.registry) + assert.Nil(t, err) + assert.NotNil(t, registry) + // Digest + referenceDigest := toNamedRef(t, tc.digestRef) + pullSource, err := registry.PullSourcesFromReference(referenceDigest) + assert.Nil(t, err) + assert.Equal(t, tc.digestSources, len(pullSource)) + // Tag + referenceTag := toNamedRef(t, tc.tagRef) + pullSource, err = registry.PullSourcesFromReference(referenceTag) + assert.Nil(t, err) + assert.Equal(t, tc.tagSources, len(pullSource)) + } +} + func TestTryUpdatingCache(t *testing.T) { ctx := &types.SystemContext{ SystemRegistriesConfPath: "testdata/try-update-cache-valid.conf", diff --git a/pkg/sysregistriesv2/testdata/pull-sources-mirror-reference.conf b/pkg/sysregistriesv2/testdata/pull-sources-mirror-reference.conf new file mode 100644 index 0000000000..3682cb2921 --- /dev/null +++ b/pkg/sysregistriesv2/testdata/pull-sources-mirror-reference.conf @@ -0,0 +1,44 @@ +[[registry]] +prefix = "registry-a.com/foo" +location = "registry-a.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true + +[[registry]] +prefix = "registry-b.com/foo" +location = "registry-b.com/bar" + +[[registry.mirror]] +digest-only = true +location = "mirror-1.registry-b.com" + +[[registry.mirror]] +digest-only = true +location = "mirror-2.registry-b.com" + +[[registry]] +prefix = "registry-c.com/foo" +location = "registry-c.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-c.com" + +[[registry.mirror]] +digest-only = true +location = "mirror-2.registry-c.com" + +[[registry]] +prefix = "registry-d.com/foo" +location = "registry-d.com/bar" +mirror-by-digest-only = true + +[[registry.mirror]] +location = "mirror-1.registry-d.com" + +[[registry.mirror]] +location = "mirror-2.registry-d.com"