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

GatewaySpec Listener CEL block empty Hostname if similar Listeners also present #2369

Closed
rainest opened this issue Aug 31, 2023 · 7 comments · Fixed by #2370
Closed

GatewaySpec Listener CEL block empty Hostname if similar Listeners also present #2369

rainest opened this issue Aug 31, 2023 · 7 comments · Fixed by #2370
Labels
help wanted Denotes an issue that needs help from a contributor. Must meet "help wanted" guidelines. kind/bug Categorizes issue or PR as related to a bug. needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. triage/accepted Indicates an issue or PR is ready to be actively worked on.

Comments

@rainest
Copy link
Contributor

rainest commented Aug 31, 2023

What happened:

0.8.0 does not allow a Gateway with two same Port, same Protocol Listeners where one Listener has Hostname set and the other does not. This violates a CEL rule that ensures Listeners have a unique Port/Protocol/Hostname combination.

What you expected to happen:

The Gateway passes validation. Per the above spec comment:

one Listener within a group may omit Hostname, in which case this Listener matches when no other Listener matches.

How to reproduce it (as minimally and precisely as possible):

With 0.8.0 installed:

$ echo "apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway                                                             
metadata:    
  name: prod-web
spec:           
  gatewayClassName: acme-lb
  listeners:               
  - protocol: HTTP
    port: 80      
    name: http
  - protocol: HTTP
    port: 80      
    name: httphostname
    hostname: http.example" | kubectl create -f -
The Gateway "prod-web" is invalid: spec.listeners: Invalid value: "array": Combination of port, protocol and hostname must be unique for each listener

Anything else we need to know?:

It looks like

(has(l1.hostname) && has(l2.hostname) ? l1.hostname == l2.hostname : true

requires a set value to compare if Hostname is equal.

Setting Hostname to "" or "*" does not work around this, as these values violate Hostname's '^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' validation regex.

@rainest rainest added the kind/bug Categorizes issue or PR as related to a bug. label Aug 31, 2023
@shaneutt shaneutt added the needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. label Aug 31, 2023
@robscott
Copy link
Member

robscott commented Aug 31, 2023

Edit: I was wrong here, the spec clearly states that an empty hostname can be combined with a specified hostname. Recommend skipping this and my next comment.

In my opinion this example should be invalid. The spec currently states that the combination of Hostname, Protocol, and Port must be unique:

// Each listener in a Gateway must have a unique combination of Hostname,
// Port, and Protocol.

Unfortunately the webhook validation did allow this to happen:

// validateHostnameProtocolPort validates that the combination of port, protocol, and hostname are
// unique for each listener.
func validateHostnameProtocolPort(listeners []gatewayv1b1.Listener, path *field.Path) field.ErrorList {
var errs field.ErrorList
hostnameProtocolPortSets := sets.Set[string]{}
for i, listener := range listeners {
hostname := new(gatewayv1b1.Hostname)
if listener.Hostname != nil {
hostname = listener.Hostname
}
protocol := listener.Protocol
port := listener.Port
hostnameProtocolPort := fmt.Sprintf("%s:%s:%d", *hostname, protocol, port)
if hostnameProtocolPortSets.Has(hostnameProtocolPort) {
errs = append(errs, field.Duplicate(path.Index(i), "combination of port, protocol, and hostname must be unique for each listener"))
} else {
hostnameProtocolPortSets.Insert(hostnameProtocolPort)
}
}
return errs
}

For completeness, here's the corresponding CEL validation:

// +kubebuilder:validation:XValidation:message="Combination of port, protocol and hostname must be unique for each listener",rule="self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname == l2.hostname : true)))"

To me, this is/was a bug in validation. I recognize that if a bug exists long enough, it becomes a feature. I'm not sure if we've already crossed that line here. Would be interested in what others think.

/cc @shaneutt @youngnick

@robscott
Copy link
Member

This feels conceptually similar to ParentRefs where we're also saying that for ParentRefs to be distinct, either SectionName or Port needs to be set to non-empty + distinct values, x-ref #2350. To avoid confusion here, I think we should do what we can to make sure the definitions of distinct here are as consistent as possible.

@rainest
Copy link
Contributor Author

rainest commented Aug 31, 2023

Considering this invalid would severely limit the ability to use a catch-all wildcard hostname, since you couldn't use it in combination with any other Listeners on the same Port+Protocol. I don't think we'd want that, and the prose indicates that we do want the 0.7 behavior--it's "one Listener within a group [port and protocol combination] may omit Hostname" rather than "a Listener may omit Hostname and match any Hostname if it is the only Listener in the group)".

We could require an explicit non-nil value for the catch-all (either "*" or ""), but it'd be a breaking change (the webhook used the same regex validation previously, and the spec said to omit it).

IMO empty is fine so long as we can check it and confirm that there are no two Listeners both using it on the same Protocol+Port. Rather, for Hostname alone, the list ["foo.example", "*.example", nil] has no duplicates, but ["foo.example", "*.example", nil, nil] does.

Testing that in CEL may be another story, but I assume you can somehow work in a failure condition for !has(l1.hostname) && !has(l2.hostname) (from brief testing, you cannot directly compare l1.hostname == l2.hostname if one is nil).

@frankbu
Copy link
Contributor

frankbu commented Aug 31, 2023

This seems like a valid configuration to me. The missing hostname is unique, it essentially behaves like hostname: *, right? So http.example will match the second listener, everything else will go to the first. It seems no different than if the first listener had hostname: *.example, which would be considered a valid unique hostname combination.

@robscott
Copy link
Member

Yep, I think you're all right. I missed @rainest's reference to the spec in the original issue that very much settles this:

// 3. As a special case, one Listener within a group may omit Hostname,
// in which case this Listener matches when no other Listener
// matches.

Sorry for the noise here, this is indeed a bug in the CEL validation and we should fix it ASAP.

/triage accepted
/help

@k8s-ci-robot
Copy link
Contributor

@robscott:
This request has been marked as needing help from a contributor.

Guidelines

Please ensure that the issue body includes answers to the following questions:

  • Why are we solving this issue?
  • To address this issue, are there any code changes? If there are code changes, what needs to be done in the code and what places can the assignee treat as reference points?
  • Does this issue have zero to low barrier of entry?
  • How can the assignee reach out to you for help?

For more details on the requirements of such an issue, please see here and ensure that they are met.

If this request no longer meets these requirements, the label can be removed
by commenting with the /remove-help command.

In response to this:

Yep, I think you're all right. I missed @rainest's reference to the spec in the original issue that very much settles this:

// 3. As a special case, one Listener within a group may omit Hostname,
// in which case this Listener matches when no other Listener
// matches.

Sorry for the noise here, this is indeed a bug in the CEL validation and we should fix it ASAP.

/triage accepted
/help

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@k8s-ci-robot k8s-ci-robot added triage/accepted Indicates an issue or PR is ready to be actively worked on. help wanted Denotes an issue that needs help from a contributor. Must meet "help wanted" guidelines. labels Aug 31, 2023
@frankbu
Copy link
Contributor

frankbu commented Aug 31, 2023

I'm no CEL expert, but it seems that the rule just needs to remove the has() part, so it will just check for duplicate hostnames, nil or not.

 // +kubebuilder:validation:XValidation:message="Combination of port, protocol and hostname must be unique for each listener",rule="self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol == l2.protocol && l1.hostname == l2.hostname))"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Denotes an issue that needs help from a contributor. Must meet "help wanted" guidelines. kind/bug Categorizes issue or PR as related to a bug. needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. triage/accepted Indicates an issue or PR is ready to be actively worked on.
Projects
No open projects
Development

Successfully merging a pull request may close this issue.

5 participants