diff --git a/docs/containers-registries.conf.5.md b/docs/containers-registries.conf.5.md index 9a884e3d56..cb72debde1 100644 --- a/docs/containers-registries.conf.5.md +++ b/docs/containers-registries.conf.5.md @@ -34,10 +34,15 @@ Given an image name, a single `[[registry]]` TOML table is chosen based on its ` - _host_[`:`_port_]`/`_namespace_[`/`_namespace_…] - _host_[`:`_port_]`/`_namespace_[`/`_namespace_…]`/`_repo_ - _host_[`:`_port_]`/`_namespace_[`/`_namespace_…]`/`_repo_(`:`_tag|`@`_digest_) + - [`*.`]_host_ The user-specified image name must start with the specified `prefix` (and continue with the appropriate separator) for a particular `[[registry]]` TOML table to be - considered; (only) the TOML table with the longest match is used. + considered; (only) the TOML table with the longest match is used. It can + also include wildcarded subdomains in the format `*.example.com` along as mentioned + above. The wildcard should only be present at the beginning as shown in the formats + above. Other cases will not work. For example, `*.example.com` is valid but + `example.*.com`, `*.example.com/foo` and `*.example.com:5000/foo/bar:baz` are not. As a special case, the `prefix` field can be missing; if so, it defaults to the value of the `location` field (described below). @@ -77,6 +82,19 @@ internet without having to change `Dockerfile`s, or to add redundancy). requests for the image `example.com/foo/myimage:latest` will actually work with the `internal-registry-for-example.net/bar/myimage:latest` image. + With a `prefix` containing a wildcard in the format: "*.example.com" for subdomain matching, + the location can be empty. In such a case, + prefix matching will occur, but no reference rewrite will occur. The + original requested image string will be used as-is. But other settings like + `insecure` / `blocked` / `mirrors` will be applied to matching images. + + Example: Given + ``` + prefix = "*.example.com" + ``` + requests for the image `blah.example.com/foo/myimage:latest` will be used + as-is. But other settings like insecure/blocked/mirrors will be applied to matching images + `mirror` : An array of TOML tables specifying (possibly-partial) mirrors for the `prefix`-rooted namespace. diff --git a/pkg/sysregistriesv2/system_registries_v2.go b/pkg/sysregistriesv2/system_registries_v2.go index b5fd3e8c60..880f8c8710 100644 --- a/pkg/sysregistriesv2/system_registries_v2.go +++ b/pkg/sysregistriesv2/system_registries_v2.go @@ -44,7 +44,10 @@ const AuthenticationFileHelper = "containers-auth.json" // Endpoint describes a remote location of a registry. type Endpoint struct { - // The endpoint's remote location. + // The endpoint's remote location. Can be empty iff Prefix contains + // wildcard in the format: "*.example.com" for subdomain matching. + // Please refer to FindRegistry / PullSourcesFromReference instead + // of accessing/interpreting `Location` directly. Location string `toml:"location,omitempty"` // If true, certs verification will be skipped and HTTP (non-TLS) // connections will be allowed. @@ -62,11 +65,26 @@ var userRegistriesDir = filepath.FromSlash(".config/containers/registries.conf.d // The function errors if the newly created reference is not parsable. func (e *Endpoint) rewriteReference(ref reference.Named, prefix string) (reference.Named, error) { refString := ref.String() - if !refMatchesPrefix(refString, prefix) { + var newNamedRef string + // refMatchingPrefix returns the length of the match. Everything that + // follows the match gets appended to registries location. + prefixLen := refMatchingPrefix(refString, prefix) + if prefixLen == -1 { return nil, fmt.Errorf("invalid prefix '%v' for reference '%v'", prefix, refString) } - - newNamedRef := strings.Replace(refString, prefix, e.Location, 1) + // In the case of an empty `location` field, simply return the original + // input ref as-is. + // + // FIXME: already validated in postProcessRegistries, so check can probably + // be dropped. + // https://github.com/containers/image/pull/1191#discussion_r610621608 + if e.Location == "" { + if prefix[:2] != "*." { + return nil, fmt.Errorf("invalid prefix '%v' for empty location, should be in the format: *.example.com", prefix) + } + return ref, nil + } + newNamedRef = e.Location + refString[prefixLen:] newParsedRef, err := reference.ParseNamed(newNamedRef) if err != nil { return nil, errors.Wrapf(err, "error rewriting reference") @@ -82,6 +100,11 @@ type Registry struct { // and we pull from "example.com/bar/myimage:latest", the image will // effectively be pulled from "example.com/foo/bar/myimage:latest". // If no Prefix is specified, it defaults to the specified location. + // Prefix can also be in the format: "*.example.com" for matching + // subdomains. The wildcard should only be in the beginning and should also + // not contain any namespaces or special characters: "/", "@" or ":". + // Please refer to FindRegistry / PullSourcesFromReference instead + // of accessing/interpreting `Prefix` directly. Prefix string `toml:"prefix"` // A registry is an Endpoint too Endpoint @@ -225,9 +248,15 @@ func (e *InvalidRegistries) Error() string { func parseLocation(input string) (string, error) { trimmed := strings.TrimRight(input, "/") - if trimmed == "" { - return "", &InvalidRegistries{s: "invalid location: cannot be empty"} - } + // FIXME: This check needs to exist but fails for empty Location field with + // wildcarded prefix. Removal of this check "only" allows invalid input in, + // and does not prevent correct operation. + // https://github.com/containers/image/pull/1191#discussion_r610122617 + // + // if trimmed == "" { + // return "", &InvalidRegistries{s: "invalid location: cannot be empty"} + // } + // if strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://") { msg := fmt.Sprintf("invalid location '%s': URI schemes are not supported", input) @@ -306,12 +335,20 @@ func (config *V2RegistriesConf) postProcessRegistries() error { } if reg.Prefix == "" { + if reg.Location == "" { + return &InvalidRegistries{s: "invalid condition: both location and prefix are unset"} + } reg.Prefix = reg.Location } else { reg.Prefix, err = parseLocation(reg.Prefix) if err != nil { return err } + // FIXME: allow config authors to always use Prefix. + // https://github.com/containers/image/pull/1191#discussion_r610622495 + if reg.Prefix[:2] != "*." && reg.Location == "" { + return &InvalidRegistries{s: "invalid condition: location is unset and prefix is not in the format: *.example.com"} + } } // make sure mirrors are valid @@ -320,8 +357,19 @@ func (config *V2RegistriesConf) postProcessRegistries() error { if err != nil { return err } + + //FIXME: unqualifiedSearchRegistries now also accepts empty values + //and shouldn't + // https://github.com/containers/image/pull/1191#discussion_r610623216 + if mir.Location == "" { + return &InvalidRegistries{s: "invalid condition: mirror location is unset"} + } + } + if reg.Location == "" { + regMap[reg.Prefix] = append(regMap[reg.Prefix], reg) + } else { + regMap[reg.Location] = append(regMap[reg.Location], reg) } - regMap[reg.Location] = append(regMap[reg.Location], reg) } // Given a registry can be mentioned multiple times (e.g., to have @@ -331,7 +379,13 @@ func (config *V2RegistriesConf) postProcessRegistries() error { // Note: we need to iterate over the registries array to ensure a // deterministic behavior which is not guaranteed by maps. for _, reg := range config.Registries { - others, ok := regMap[reg.Location] + var others []*Registry + var ok bool + if reg.Location == "" { + others, ok = regMap[reg.Prefix] + } else { + others, ok = regMap[reg.Location] + } if !ok { return fmt.Errorf("Internal error in V2RegistriesConf.PostProcess: entry in regMap is missing") } @@ -623,6 +677,8 @@ func tryUpdatingCache(ctx *types.SystemContext, wrapper configWrapper) (*parsedC return config, nil } +// GetRegistries has been deprecated. Use FindRegistry instead. +// // GetRegistries loads and returns the registries specified in the config. // Note the parsed content of registry config files is cached. For reloading, // use `InvalidateCache` and re-call `GetRegistries`. @@ -689,27 +745,63 @@ func CredentialHelpers(sys *types.SystemContext) ([]string, error) { return config.partialV2.CredentialHelpers, nil } -// refMatchesPrefix returns true iff ref, +// refMatchingSubdomainPrefix returns the length of ref +// iff ref, which is a registry, repository namespace, repository or image reference (as formatted by +// reference.Domain(), reference.Named.Name() or reference.Reference.String() +// — note that this requires the name to start with an explicit hostname!), +// matches a Registry.Prefix value containing wildcarded subdomains in the +// format: *.example.com. Wildcards are only accepted at the beginning, so +// other formats like example.*.com will not work. Wildcarded prefixes also +// cannot contain port numbers or namespaces in them. +func refMatchingSubdomainPrefix(ref, prefix string) int { + index := strings.Index(ref, prefix[1:]) + if index == -1 { + return -1 + } + if strings.Contains(ref[:index], "/") { + return -1 + } + index += len(prefix[1:]) + if index == len(ref) { + return index + } + switch ref[index] { + case ':', '/', '@': + return index + default: + return -1 + } +} + +// refMatchingPrefix returns the length of the prefix iff ref, // which is a registry, repository namespace, repository or image reference (as formatted by // reference.Domain(), reference.Named.Name() or reference.Reference.String() // — note that this requires the name to start with an explicit hostname!), // matches a Registry.Prefix value. // (This is split from the caller primarily to make testing easier.) -func refMatchesPrefix(ref, prefix string) bool { +func refMatchingPrefix(ref, prefix string) int { switch { + case prefix[0:2] == "*.": + return refMatchingSubdomainPrefix(ref, prefix) case len(ref) < len(prefix): - return false + return -1 case len(ref) == len(prefix): - return ref == prefix + if ref == prefix { + return len(prefix) + } + return -1 case len(ref) > len(prefix): if !strings.HasPrefix(ref, prefix) { - return false + return -1 } c := ref[len(prefix)] // This allows "example.com:5000" to match "example.com", // which is unintended; that will get fixed eventually, DON'T RELY // ON THE CURRENT BEHAVIOR. - return c == ':' || c == '/' || c == '@' + if c == ':' || c == '/' || c == '@' { + return len(prefix) + } + return -1 default: panic("Internal error: impossible comparison outcome") } @@ -735,7 +827,7 @@ func findRegistryWithParsedConfig(config *parsedConfig, ref string) (*Registry, reg := Registry{} prefixLen := 0 for _, r := range config.partialV2.Registries { - if refMatchesPrefix(ref, r.Prefix) { + if refMatchingPrefix(ref, r.Prefix) != -1 { length := len(r.Prefix) if length > prefixLen { reg = r @@ -804,6 +896,17 @@ 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 { + prefix := res.partialV2.Registries[i].Prefix + if prefix[:2] == "*." && 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} + } + } + // Parse and validate short-name aliases. cache, err := newShortNameAliasCache(path, &res.partialV2.shortNameAliasConf) if err != nil { diff --git a/pkg/sysregistriesv2/system_registries_v2_test.go b/pkg/sysregistriesv2/system_registries_v2_test.go index f300ee55fc..79b41f215e 100644 --- a/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/pkg/sysregistriesv2/system_registries_v2_test.go @@ -83,43 +83,86 @@ func TestMirrors(t *testing.T) { assert.True(t, reg.Mirrors[1].Insecure) } -func TestRefMatchesPrefix(t *testing.T) { +func TestRefMatchingSubdomainPrefix(t *testing.T) { for _, c := range []struct { ref, prefix string - expected bool + expected int + }{ + // Check for subdomain matches + {"docker.io", "*.io", len("docker.io")}, + {"docker.io/foo", "*.com", -1}, + {"example.com/foo", "*.co", -1}, + {"example.com/foo", "*.example.com", -1}, + //FIXME: Port Number matching needs to be revisited. + // https://github.com/containers/image/pull/1191#pullrequestreview-631869416 + //{"example.com:5000", "*.com", len("example.com")}, + //{"example.com:5000/foo", "*.com", len("example.com")}, + //{"sub.example.com:5000/foo", "*.example.com", len("sub.example.com")}, + //{"example.com:5000/foo/bar", "*.com", len("example.com")}, + //{"example.com:5000/foo/bar:baz", "*.com", len("example.com")}, + //{"example.com:5000/foo/bar/bbq:baz", "*.com", len("example.com")}, + //{"example.com:50000/foo", "*.example.com", -1}, + {"example.com/foo", "*.com", len("example.com")}, + {"example.com/foo:bar", "*.com", len("example.com")}, + {"example.com/foo/bar:baz", "*.com", len("example.com")}, + {"yet.another.example.com/foo", "**.example.com", -1}, + {"yet.another.example.com/foo", "***.another.example.com", -1}, + {"yet.another.example.com/foo", "**********.another.example.com", -1}, + {"yet.another.example.com/foo/bar", "**********.another.example.com", -1}, + {"yet.another.example.com/foo/bar", "*.another.example.com", len("yet.another.example.com")}, + {"another.example.com/namespace.com/foo/bar/bbq:baz", "*.example.com", len("another.example.com")}, + {"example.net/namespace-ends-in.com/foo/bar/bbq:baz", "*.com", -1}, + {"another.example.com/namespace.com/foo/bar/bbq:baz", "*.namespace.com", -1}, + {"sub.example.com/foo/bar", "*.com", len("sub.example.com")}, + {"sub.example.com/foo/bar", "*.example.com", len("sub.example.com")}, + {"another.sub.example.com/foo/bar/bbq:baz", "*.example.com", len("another.sub.example.com")}, + {"another.sub.example.com/foo/bar/bbq:baz", "*.sub.example.com", len("another.sub.example.com")}, + {"yet.another.example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "*.example.com", len("yet.another.example.com")}, + {"yet.another.sub.example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "*.sub.example.com", len("yet.another.sub.example.com")}, + } { + refLen := refMatchingSubdomainPrefix(c.ref, c.prefix) + assert.Equal(t, c.expected, refLen, fmt.Sprintf("%s vs. %s", c.ref, c.prefix)) + } +} + +func TestRefMatchingPrefix(t *testing.T) { + for _, c := range []struct { + ref, prefix string + expected int }{ // Prefix is a reference.Domain() value - {"docker.io", "docker.io", true}, - {"docker.io", "example.com", false}, - {"example.com:5000", "example.com:5000", true}, - {"example.com:50000", "example.com:5000", false}, - {"example.com:5000", "example.com", true}, // FIXME FIXME This is unintended and undocumented, don't rely on this behavior - {"example.com/foo", "example.com", true}, - {"example.com/foo/bar", "example.com", true}, - {"example.com/foo/bar:baz", "example.com", true}, - {"example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com", true}, + {"docker.io", "docker.io", len("docker.io")}, + {"docker.io", "example.com", -1}, + {"example.com:5000", "example.com:5000", len("example.com:5000")}, + {"example.com:50000", "example.com:5000", -1}, + {"example.com:5000", "example.com", len("example.com")}, // FIXME FIXME This is unintended and undocumented, don't rely on this behavior + {"example.com/foo", "example.com", len("example.com")}, + {"example.com/foo/bar", "example.com", len("example.com")}, + {"example.com/foo/bar:baz", "example.com", len("example.com")}, + {"example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com", len("example.com")}, // Prefix is a reference.Named.Name() value or a repo namespace - {"docker.io", "docker.io/library", false}, - {"docker.io/library", "docker.io/library", true}, - {"example.com/library", "docker.io/library", false}, - {"docker.io/libraryy", "docker.io/library", false}, - {"docker.io/library/busybox", "docker.io/library", true}, - {"docker.io", "docker.io/library/busybox", false}, - {"docker.io/library/busybox", "docker.io/library/busybox", true}, - {"example.com/library/busybox", "docker.io/library/busybox", false}, - {"docker.io/library/busybox2", "docker.io/library/busybox", false}, + {"docker.io", "docker.io/library", -1}, + {"docker.io/library", "docker.io/library", len("docker.io/library")}, + {"example.com/library", "docker.io/library", -1}, + {"docker.io/libraryy", "docker.io/library", -1}, + {"docker.io/library/busybox", "docker.io/library", len("docker.io/library")}, + {"docker.io", "docker.io/library/busybox", -1}, + {"docker.io/library/busybox", "docker.io/library/busybox", len("docker.io/library/busybox")}, + {"example.com/library/busybox", "docker.io/library/busybox", -1}, + {"docker.io/library/busybox2", "docker.io/library/busybox", -1}, // Prefix is a single image - {"example.com", "example.com/foo:bar", false}, - {"example.com/foo", "example.com/foo:bar", false}, - {"example.com/foo:bar", "example.com/foo:bar", true}, - {"example.com/foo:bar2", "example.com/foo:bar", false}, - {"example.com", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, - {"example.com/foo", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, - {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, - {"example.com/foo@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, + {"example.com", "example.com/foo:bar", -1}, + {"example.com/foo", "example.com/foo:bar", -1}, + {"example.com/foo:bar", "example.com/foo:bar", len("example.com/foo:bar")}, + {"example.com/foo:bar2", "example.com/foo:bar", -1}, + {"example.com", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1}, + {"example.com/foo", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1}, + {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + len("example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, + {"example.com/foo@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1}, } { - res := refMatchesPrefix(c.ref, c.prefix) - assert.Equal(t, c.expected, res, fmt.Sprintf("%s vs. %s", c.ref, c.prefix)) + prefixLen := refMatchingPrefix(c.ref, c.prefix) + assert.Equal(t, c.expected, prefixLen, fmt.Sprintf("%s vs. %s", c.ref, c.prefix)) } } @@ -192,12 +235,12 @@ func TestNewConfigWrapper(t *testing.T) { func TestFindRegistry(t *testing.T) { sys := &types.SystemContext{ SystemRegistriesConfPath: "testdata/find-registry.conf", - SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + SystemRegistriesConfDirPath: "testdata/registries.conf.d", } registries, err := GetRegistries(sys) assert.Nil(t, err) - assert.Equal(t, 5, len(registries)) + assert.Equal(t, 19, len(registries)) reg, err := FindRegistry(sys, "simple-prefix.com/foo/bar:latest") assert.Nil(t, err) @@ -215,6 +258,57 @@ func TestFindRegistry(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, reg) + // subdomain prefix match + reg, err = FindRegistry(sys, "not.so.simple-prefix.com/") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix.com", reg.Location) + + reg, err = FindRegistry(sys, "not.quite.simple-prefix.com/") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-2.com", reg.Location) + + reg, err = FindRegistry(sys, "not.quite.simple-prefix.com:5000/with/path/and/beyond:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-2.com", reg.Location) + + // subdomain prefix match for *.not.quite.simple-prefix.com + // location field overriden by /registries.conf.d/subdomain-override-1.conf + reg, err = FindRegistry(sys, "really.not.quite.simple-prefix.com:5000/with/path/and/beyond:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-1-overridden-by-dropin-location.com", reg.Location) + + // In this case, the override does NOT occur because the dropin + // prefix = "*.docker.com" which is not a match. + reg, err = FindRegistry(sys, "foo.docker.io:5000/omg/wtf/bbq:foo") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-2.com", reg.Location) + + // subdomain prefix match for *.bar.example.com + // location field overriden by /registries.conf.d/subdomain-override-3.conf + reg, err = FindRegistry(sys, "foo.bar.example.com:6000/omg/wtf/bbq@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-3-overridden-by-dropin-location.com", reg.Location) + + // This case first matches with prefix = *.docker.io in find-registry.conf but + // there's a longer match with *.bar.docker.io which gets used + reg, err = FindRegistry(sys, "foo.bar.docker.io:5000/omg/wtf/bbq:foo") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-4.com", reg.Location) + + // This case first matches with prefix = *.example.com in find-registry.conf but + // there's a longer match with foo.bar.example.com:5000 which gets used + reg, err = FindRegistry(sys, "foo.bar.example.com:5000/omg/wtf/bbq:foo") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-5.com", reg.Location) + // invalid match reg, err = FindRegistry(sys, "simple-prefix.comx") assert.Nil(t, err) @@ -277,8 +371,7 @@ func TestInvalidV2Configs(t *testing.T) { for _, c := range []struct{ path, errorSubstring string }{ {"testdata/insecure-conflicts.conf", "registry 'registry.com' is defined multiple times with conflicting 'insecure' setting"}, {"testdata/blocked-conflicts.conf", "registry 'registry.com' is defined multiple times with conflicting 'blocked' setting"}, - {"testdata/missing-registry-location.conf", "invalid location"}, - {"testdata/missing-mirror-location.conf", "invalid location"}, + {"testdata/missing-mirror-location.conf", "invalid condition: mirror location is unset"}, {"testdata/invalid-prefix.conf", "invalid location"}, {"testdata/this-does-not-exist.conf", "no such file or directory"}, } { @@ -429,6 +522,20 @@ func TestRewriteReferenceSuccess(t *testing.T) { {"docker.io/library/image", "docker.io/library", "example.com", "example.com/image"}, {"docker.io/library/image", "docker.io", "example.com", "example.com/library/image"}, {"docker.io/library/prefix/image", "docker.io/library/prefix", "example.com", "example.com/image"}, + // Wildcard prefix examples + {"docker.io/namespace/image", "*.io", "example.com", "example.com/namespace/image"}, + {"docker.io/library/prefix/image", "*.io", "example.com", "example.com/library/prefix/image"}, + {"sub.example.io/library/prefix/image", "*.example.io", "example.com", "example.com/library/prefix/image"}, + {"another.sub.example.io:5000/library/prefix/image:latest", "*.sub.example.io", "example.com", "example.com:5000/library/prefix/image:latest"}, + {"foo.bar.io/ns1/ns2/ns3/ns4", "*.bar.io", "omg.bbq.com/roflmao", "omg.bbq.com/roflmao/ns1/ns2/ns3/ns4"}, + // Empty location with wildcard prefix examples. Essentially, no + // rewrite occurs and original reference is used as-is. + {"abc.internal.registry.com/foo:bar", "*.internal.registry.com", "", "abc.internal.registry.com/foo:bar"}, + {"blah.foo.bar.com/omg:bbq", "*.com", "", "blah.foo.bar.com/omg:bbq"}, + {"alien.vs.predator.foobar.io:5000/omg", "*.foobar.io", "", "alien.vs.predator.foobar.io:5000/omg"}, + {"alien.vs.predator.foobar.io:5000/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "*.foobar.io", "", + "alien.vs.predator.foobar.io:5000/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {"alien.vs.predator.foobar.io:5000/omg:bbq", "*.foobar.io", "", "alien.vs.predator.foobar.io:5000/omg:bbq"}, } { ref := toNamedRef(t, c.inputRef) testEndpoint := Endpoint{Location: c.location} @@ -455,6 +562,9 @@ func TestRewriteReferenceFailedDuringParseNamed(t *testing.T) { {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com/fo", "example.com/foo"}, {"docker.io/library/image", "example.com", "example.com"}, + {"docker.io/library/image", "*.com", "example.com"}, + {"foo.docker.io/library/image", "*.example.com", "example.com/image"}, + {"foo.docker.io/library/image", "*.docker.com", "example.com/image"}, } { ref := toNamedRef(t, c.inputRef) testEndpoint := Endpoint{Location: c.location} @@ -542,8 +652,8 @@ func TestRegistriesConfDirectory(t *testing.T) { assert.NotNil(t, registries) assert.Equal(t, []string{"example-overwrite.com"}, registries.UnqualifiedSearchRegistries) - assert.Equal(t, 3, len(registries.Registries)) - assertRegistryLocationsEqual(t, []string{"1.com", "2.com", "base.com"}, registries.Registries) + assert.Equal(t, 6, len(registries.Registries)) + assertRegistryLocationsEqual(t, []string{"subdomain-prefix-3-overridden-by-dropin-location.com", "subdomain-prefix-2-overridden-by-dropin-location.com", "subdomain-prefix-1-overridden-by-dropin-location.com", "1.com", "2.com", "base.com"}, registries.Registries) reg, err := FindRegistry(ctx, "base.com/test:latest") require.NoError(t, err) diff --git a/pkg/sysregistriesv2/testdata/find-registry.conf b/pkg/sysregistriesv2/testdata/find-registry.conf index 02bfc32934..a46bafb7f9 100644 --- a/pkg/sysregistriesv2/testdata/find-registry.conf +++ b/pkg/sysregistriesv2/testdata/find-registry.conf @@ -16,3 +16,47 @@ location = "no-prefix.com" [[registry]] location = "empty-prefix.com" prefix = "" + +[[registry]] +location = "subdomain-prefix.com" +prefix = "*.so.simple-prefix.com" + +[[registry]] +location = "subdomain-prefix-2.com" +prefix = "*.simple-prefix.com" + +# For subdomain override using dropin registries.conf.d/subdomain-override-1.conf +[[registry]] +location = "subdomain-prefix-1.com" +prefix = "*.not.quite.simple-prefix.com" + +# For subdomain override failure using registries.conf.d/subdomain-override-2.conf +# with unmatched prefix = "*.example.com" +[[registry]] +location = "subdomain-prefix-2.com" +prefix = "*.docker.io" + +# For subdomain override using dropin registries.conf.d/subdomain-override-3.conf +[[registry]] +location = "subdomain-prefix-3.com" +prefix = "*.bar.example.com" + +# For longest wildcarded prefix match in comparison with "*.docker.io" +[[registry]] +location = "subdomain-prefix-4.com" +prefix = "*.bar.docker.io" + +# For longest prefix match in comaprison with *.bar.example.com +[[registry]] +location = "subdomain-prefix-5.com" +prefix = "foo.bar.example.com:5000" + +# For empty location with wildcard prefix +[[registry]] +prefix="*.internal.registry.com" + +[[registry]] +prefix="*.com" + +[[registry]] +prefix="*.foobar.io" diff --git a/pkg/sysregistriesv2/testdata/missing-registry-location.conf b/pkg/sysregistriesv2/testdata/missing-registry-location.conf deleted file mode 100644 index cf97b65c73..0000000000 --- a/pkg/sysregistriesv2/testdata/missing-registry-location.conf +++ /dev/null @@ -1,9 +0,0 @@ -unqualified-search-registries = ["registry-a.com"] - -[[registry]] -location = "registry-a.com" - -[[registry]] -location = "registry-b.com" - -[[registry]] diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-1.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-1.conf new file mode 100644 index 0000000000..b98000c09c --- /dev/null +++ b/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-1.conf @@ -0,0 +1,3 @@ +[[registry]] +location = "subdomain-prefix-1-overridden-by-dropin-location.com" +prefix = "*.not.quite.simple-prefix.com" diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-2.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-2.conf new file mode 100644 index 0000000000..87a10a148f --- /dev/null +++ b/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-2.conf @@ -0,0 +1,3 @@ +[[registry]] +location = "subdomain-prefix-2-overridden-by-dropin-location.com" +prefix = "*.docker.com" diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-3.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-3.conf new file mode 100644 index 0000000000..f7daf16c6f --- /dev/null +++ b/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-3.conf @@ -0,0 +1,3 @@ +[[registry]] +location = "subdomain-prefix-3-overridden-by-dropin-location.com" +prefix = "*.bar.example.com" diff --git a/registries.conf b/registries.conf index 9fb1670163..d85befbf9e 100644 --- a/registries.conf +++ b/registries.conf @@ -24,6 +24,9 @@ # # The "prefix" field is used to choose the relevant [[registry]] TOML table; # # (only) the TOML table with the longest match for the input image name # # (taking into account namespace/repo/tag/digest separators) is used. +# # +# # The prefix can also be of the form: *.example.com for wildcard subdomain +# # matching. # # # # If the prefix field is missing, it defaults to be the same as the "location" field. # prefix = "example.com/foo" @@ -37,7 +40,7 @@ # # # The physical location of the "prefix"-rooted namespace. # # -# # By default, this equal to "prefix" (in which case "prefix" can be omitted +# # By default, this is equal to "prefix" (in which case "prefix" can be omitted # # and the [[registry]] TOML table can only specify "location"). # # # # Example: Given @@ -45,6 +48,10 @@ # # location = "internal-registry-for-example.net/bar" # # requests for the image example.com/foo/myimage:latest will actually work with the # # internal-registry-for-example.net/bar/myimage:latest image. +# +# # The location can be empty iff prefix is in a +# # wildcarded format: "*.example.com". In this case, the input reference will +# # be used as-is without any rewrite. # location = internal-registry-for-example.com/bar" # # # (Possibly-partial) mirrors for the "prefix"-rooted namespace. diff --git a/version/version.go b/version/version.go index 229a365cea..e48f4bc607 100644 --- a/version/version.go +++ b/version/version.go @@ -8,7 +8,7 @@ const ( // VersionMinor is for functionality in a backwards-compatible manner VersionMinor = 11 // VersionPatch is for backwards-compatible bug fixes - VersionPatch = 0 + VersionPatch = 1 // VersionDev indicates development branch. Releases will be empty string. VersionDev = "-dev"