Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable subdomain matching in registries.conf #1191

Merged
merged 3 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs/containers-registries.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
lsm5 marked this conversation as resolved.
Show resolved Hide resolved

As a special case, the `prefix` field can be missing; if so, it defaults to the value
of the `location` field (described below).
Expand Down Expand Up @@ -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.
Expand Down
135 changes: 119 additions & 16 deletions pkg/sysregistriesv2/system_registries_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Copy link
Collaborator

@mtrmac mtrmac Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on user input (prefix=*.foo, location unset)? AFAICS that’s converted into (prefix=*.foo, location=*.foo), and this rewrite function then turns domain.foo/bar into *.foo/bar, breaking the format.

(There should be a test for this as well.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I have re-formatted the above to show asterisks instead of being interpreted as italic.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do, thanks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on user input (prefix=*.foo, location unset)?

So, just to confirm, do you mean this:

# Location unset with wildcarded prefix
[[registry]]
location=""
prefix="*.foo"

So, unset location doesn't seem to be accepted to begin with and make test panics along with a:
Error: Expected nil, but got: error loading registries configuration "testdata/find-registry.conf": invalid location: cannot be empty

I'll look at adding a test for panics if we need it in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err .. let me double check on this ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading more of the RFE, it’s unclear to me whether they actually need the insecure flag or whether it was incidental, and primarily then wanted the allowed/blocked registries. Either way, the “blocked registries” option shows up both in policy.json and registries.conf per https://github.com/openshift/runtime-utils/blob/master/pkg/registries/registries.go and https://github.com/openshift/machine-config-operator/blob/ca8fcb887f153604556f2706cee8a33165879797/pkg/controller/container-runtime-config/helpers.go#L381 , so I think we do need need to have wildcard matching without triggering a remapping.


It feels unlikely to me the primary feature that must get in before the deadline is “remap *.foo/… to bar/…”, but it’s possible; I’m afraid I can’t now review the full history of the related RFEs. Either way prioritization is not up to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your points convince me. Thanks, @mtrmac!

@lsm5. I agree that we need to support the location="" case in which case no remapping will happen (i.e., the provided input will be used) but the matching should work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[[registry]]
prefix="*.internal.registry.com
insecure=true

In that case, a podman pull xxx.internal.registry.com/image:tag would pull from xxx. ... without TLS (insecure).
blocked=true would work implicitly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack, I'll add a check for this and may also need code change I guess.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vrothberg I first added and then removed the check for insecure=true based on @mtrmac's later review comment and @umohnani8 said openshift/mco didn't need it. Current version only checks for empty location and wildcarded prefix.

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 == "" {
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
if prefix[:2] != "*." {
return nil, fmt.Errorf("invalid prefix '%v' for empty location, should be in the format: *.example.com", prefix)
}
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
return ref, nil
}
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
newNamedRef = e.Location + refString[prefixLen:]
newParsedRef, err := reference.ParseNamed(newNamedRef)
if err != nil {
return nil, errors.Wrapf(err, "error rewriting reference")
Expand All @@ -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
Expand Down Expand Up @@ -225,9 +248,15 @@ func (e *InvalidRegistries) Error() string {
func parseLocation(input string) (string, error) {
trimmed := strings.TrimRight(input, "/")

if trimmed == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit too broad — it now allows empty mirrors, or empty entries in unqualifiedSearchRegistries. I’d expect this check to remain, the callers could recognize the various special situations and decide not to parse an empty string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad. changing ..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Earlier there was a reference to a deadline today — if things had to be cut, this removed check “only” allows invalid input in, and does not prevent correct operation.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deadline is tomorrow, so I guess I still have time to do the right thing.

If you don't mind dumbing things down for me, would you prefer that this check remain and I also add a condition to check that it's not a location field? Otherwise anything test that requires a location="" fails currently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, tomorrow is only feature freeze and there's another month for final code freeze, which will give time for bugs and such.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that also makes me wonder if this func should have a name other than parseLocation, because I tend to assume it's only for the Location field. Maybe that's just me though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrmac For now, I left the check commented out along with a note about allowing invalid input and not preventing correct operation. let me know what's the best way to proceed. ^ /cc @vrothberg .

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ideally there should be a parsePrefix (which allows *., and rejects /@: the way the current later check does, and maybe validates against the charset limitations of of reference.DomainRegexp), and a parseLocation (which rejects *., and allows /@:, and allows a reference.DomainRegexp or a reference.NameRegexp, or…).

I think it’s quite fine to defer that till later (especially because too strict validation of non-wildcard locations could break users, and tightening that validation is really a quite separate feature).


would you prefer that this check remain and I also add a condition to check that it's not a location field? Otherwise anything test that requires a location="" fails currently.

This might be a clean way (or I might be missing something):

  • Continue to have parseLocation reject empty input.
  • The mirror/search uses of parseLocation can remain unchanged.
  • The primary Prefix/Location validation/defaulting logic of postProcessRegistries could go something like:
    if reg.Prefix != "" {
        reg.Prefix = parsePrefix(reg.Prefix)
        if reg.Location != "" { // prefix->location remapping
            reg.Location = parseLocation(reg.Location)
        } // else prefix->"", leave Location as "", no remapping
    } else if reg.Location != "" { // ""->location, previously-preferred syntax for no remapping
        reg.Prefix = parseLocation(reg.Location) // must not contain wildcards
        reg.Location = reg.Prefix // Could also be reg.Location = "", we leave both filled for API compatibility with possible older direct readers of Registry
    } else {
        // fail: registry entry with neither Prefix nor Location
    }
    where the second reg.Location != "" check needs to happen anyway for error checking/defaulting, and only the first one exists purely to implement the “location can be empty” situation, exactly in the one case where we allow an empty value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrmac On second thoughts, I have some other commitments this evening and also considering @vrothberg's timezone for tomorrow and such, I would like to defer this part to the bugfix phase.

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)
Expand Down Expand Up @@ -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 == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is consistent with the current documentation and fine for now.

For future fix-ups:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted as FIXME.

return &InvalidRegistries{s: "invalid condition: location is unset and prefix is not in the format: *.example.com"}
}
}

// make sure mirrors are valid
Expand All @@ -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"}
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
}
}
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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the loop below?

AFAICS with a { prefix = *.example; location = ""; insecure = true }; { prefix = *.other; location = ""; blocked = true } the loop will complain about conflicting insecure/blocked settings.

I think regMap should use reg.Prefix if reg.Location is unset.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add this now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrmac PTAL ^ .. hope that looks ok.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err likely not :\

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • regMap[reg.Location] = append(regMap[reg.Prefix], reg) should use `reg.Prefix in both cases
  • The loop below looking up data in regMap needs to consider reg.Prefix as well if reg.Location is empty.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, ack, thanks!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, PTAL. Other comments added as FIXMEs. Thanks.


// Given a registry can be mentioned multiple times (e.g., to have
Expand All @@ -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")
}
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}
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")
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
prefix := res.partialV2.Registries[i].Prefix
if prefix[:2] == "*." && strings.ContainsAny(prefix, "/@:") {
lsm5 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
Loading