Skip to content

Conversation

@timflannagan
Copy link
Member

@timflannagan timflannagan commented Nov 3, 2025

Description

This change fixes an issue where patching Kubernetes Services (e.g. changing appProtocol or ports) did not dynamically propagate to the xDS config, and required restarting the kgw container to get the desired functional behavior.

The root cause was the HttpRouteIR.rulesEqual() implementation didn't compare the BackendObject fields when checking backend equality. It was only comparing ClusterName and Weight, missing changes to the underlying Service configuration like appProtocol.

Implements an Equals() method for the HttpBackendOrDelegate type to fix this edge case and cleanup the overall implementation to be consistent with the rest of the codebase and avoid harcoding the conditional checks related to Equals-based collection checking.

Fixes #12576.

Change Type

/kind fix

Changelog

NONE

Additional Notes

In order to reproduce locally:

``yaml

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: test-gateway
namespace: default
spec:
gatewayClassName: kgateway
listeners:
- name: http
protocol: HTTP
port: 8080

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: test-route
namespace: default
spec:
parentRefs:
- name: test-gateway
hostnames:
- "test.example.com"
rules:
- backendRefs:
- name: echo-svc
port: 80

apiVersion: v1
kind: Service
metadata:
name: echo-svc
namespace: default
spec:
selector:
app: echo-server
ports:
- protocol: TCP
port: 80
targetPort: 8080
# appProtocol: kubernetes.io/ws

apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-server
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: echo-server
template:
metadata:
labels:
app: echo-server
spec:
containers:
- name: echo
image: jmalloc/echo-server:latest
ports:
- containerPort: 8080

k -n default port-forward deploy/test-gateway 8080:8080 &

curl -v -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" -H "Host: test.example.com" http://localhost:8080


</details>

Deploying that manifest, validating that the curl command fails, and then uncommenting the appProtocol change + re-validating the curl command should return a 101 response code.

Copilot AI review requested due to automatic review settings November 3, 2025 19:44
@github-actions github-actions bot added kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. release-note-none kind/fix Categorizes issue or PR as related to a bug. and removed kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. labels Nov 3, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the equality comparison logic for HTTP backend objects to properly include AppProtocol field comparisons. The key changes centralize backend equality checks and make the ResourceName field public for better API consistency.

Key Changes

  • Refactored HttpBackendOrDelegate.Equals() method to properly delegate equality checks to constituent types
  • Updated BackendObjectIR.Equals() to include AppProtocol comparison
  • Made BackendObjectIR.ResourceName field public and removed redundant accessor method
  • Added comprehensive test coverage for AppProtocol differences in backend equality

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/pluginsdk/ir/backend.go Made ResourceName public, removed accessor method, added AppProtocol to equality check
pkg/pluginsdk/ir/gw.go Added HttpBackendOrDelegate.Equals() method and moved type definition
pkg/pluginsdk/ir/routes.go Refactored to use new HttpBackendOrDelegate.Equals() method
pkg/pluginsdk/ir/routes_test.go Added test coverage for AppProtocol differences in backends
pkg/pluginsdk/ir/model.go Updated to use public ResourceName field
internal/kgateway/krtcollections/policy.go Updated to use public ResourceName field
internal/kgateway/extensions2/plugins/inferenceextension/endpointpicker/collections.go Updated to use public ResourceName field
internal/kgateway/extensions2/plugins/inferenceextension/endpointpicker/backends.go Updated to use public ResourceName field
internal/kgateway/extensions2/plugins/backendtlspolicy/plugin.go Removed explicit type parameter from kubeclient.Register call

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@shashankram shashankram left a comment

Choose a reason for hiding this comment

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

It would be nice to have a test where we can change the appProtocol or an equivalent field and verify that it triggers a translation update on the route

}

func (h HttpBackendOrDelegate) Equals(other HttpBackendOrDelegate) bool {
if !h.AttachedPolicies.Equals(other.AttachedPolicies) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: order comparision in the order of fields declared in the struct

return false
}
} else {
if !backend.Equals(backendsb[j]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Were we intentionally restricting Backend comparison to specific fields before or was this an oversight?

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 think the latter is very likely. If you go through the git history for this file, you'll reach the massive refactor PR that Yuval did during the early days of the donation that predated my time: 5d4e603#diff-5b7e43b2f30d5fce0ff29295ccad60d0882ce08abb34ba7e6f9464b942e02a6f. @lgadban @yuval-k if you can confirm

Copy link
Contributor

Choose a reason for hiding this comment

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

it is intentional - there's no need to propagate changes like app protocol for routes - i believe it is a no-op in envoy

Copy link
Contributor

Choose a reason for hiding this comment

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

i.e. i don't care about backend properties that don't end up on the envoy route object

Copy link
Contributor

Choose a reason for hiding this comment

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

@timflannagan do you know if something about the app protcol impacts the envoy route?

Copy link
Member Author

Choose a reason for hiding this comment

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

the problem is that appProtocol, at least for websockets, does contribute to envoy config:

if back := backend.Backend.BackendObject; back != nil && back.AppProtocol == ir.WebSocketAppProtocol {
// add websocket upgrade if not already present
if !slices.ContainsFunc(action.GetUpgradeConfigs(), func(uc *envoyroutev3.RouteAction_UpgradeConfig) bool {
return uc.GetUpgradeType() == webSocketUpgradeType
}) {
action.UpgradeConfigs = append(action.GetUpgradeConfigs(), &envoyroutev3.RouteAction_UpgradeConfig{
UpgradeType: webSocketUpgradeType,
})
}
}
. the issue is that before this logic, we weren't comparing the Backend.BackendObject, which meant updates to Services that change the appProtocol weren't reflected in the system.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW for the BackendObjectIR's Equals() not checking the AppProtocol field. I think we're all set due to

func versionEquals(a, b metav1.Object) bool {
var versionEquals bool
if a.GetGeneration() != 0 && b.GetGeneration() != 0 {
versionEquals = a.GetGeneration() == b.GetGeneration()
} else {
versionEquals = a.GetResourceVersion() == b.GetResourceVersion()
}
return versionEquals && a.GetUID() == b.GetUID()
}
. As an aside, that function implementation looks like it wouldn't work with fields that are derived from annotations (e.g. traffic distribution, disable Istio auto mtls) and require those fields to explicitly contribute to the Equals implementation vs. piggybacking off the object check.

So the issue here is solely due to the lack of explicit backend.Backend equals checking.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, we are automatically add an upgrade to the route based on the backend app protocol. then yea the app protocol is effectivly a route policy in this case, so it should be in the equals

}

return objEq && objVersionEq && objIrEq && polEq && nameEq && disableIstioAutoMTLSEq
return objEq && objVersionEq && objIrEq && polEq && nameEq && disableIstioAutoMTLSEq && c.AppProtocol == in.AppProtocol
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Can we collapse L173-177 into L187 for consistency

@timflannagan timflannagan added the work in progress Indicates that a PR should not merge because it is a work in progress label Nov 3, 2025
@timflannagan
Copy link
Member Author

Going to look into an IR-based linter in parallel. Throwing the WIP label depending on the level of effort for that.

@timflannagan timflannagan removed the work in progress Indicates that a PR should not merge because it is a work in progress label Nov 5, 2025
@timflannagan
Copy link
Member Author

It would be nice to have a test where we can change the appProtocol or an equivalent field and verify that it triggers a translation update on the route

SG. I'll look into either a setup test || e2e test here in addition to the unit test added.

This change fixes an issue where patching Kubernetes Services (e.g.
changing appProtocol or ports) did not dynamically propagate to
the xDS config, and required restarting the kgw container.

The root cause was HttpRouteIR.rulesEqual() not comparing
BackendObject fields when checking backend equality. It was only
comparing ClusterName and Weight, missing changes to the underlying
Service configuration like appProtocol.

Implements an Equals() method for the HttpBackendOrDelegate type to
fix this edge case and cleanup the overall implementation to be
consistent with the rest of the codebase and avoid harcoding the
conditional checks related to Equals-based collection checking.

Fixes kgateway-dev#12576.

Signed-off-by: timflannagan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/fix Categorizes issue or PR as related to a bug. release-note-none

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Websocket Support

3 participants