Skip to content

Commit 978d069

Browse files
committed
ir: Fix Service updates not propagating through translation
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 #12576. Signed-off-by: timflannagan <[email protected]>
1 parent 7a9e2af commit 978d069

File tree

6 files changed

+266
-58
lines changed

6 files changed

+266
-58
lines changed

internal/kgateway/extensions2/plugins/backendtlspolicy/plugin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (d *backendTlsPolicy) Equals(in any) bool {
7070
}
7171

7272
func registerTypes() {
73-
kubeclient.Register[*gwv1.BackendTLSPolicy](
73+
kubeclient.Register(
7474
backendTlsPolicyGvr,
7575
backendTlsPolicyGroupKind,
7676
func(c kubeclient.ClientGetter, namespace string, o metav1.ListOptions) (runtime.Object, error) {

internal/kgateway/setup/setup_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,116 @@ spec:
322322
})
323323
}
324324

325+
func TestServiceAppProtocolUpdate(t *testing.T) {
326+
st, err := envtestutil.BuildSettings()
327+
if err != nil {
328+
t.Fatalf("can't get settings %v", err)
329+
}
330+
setupEnvTestAndRun(t, st, func(t *testing.T, ctx context.Context, kdbg *krt.DebugHandler, client istiokube.CLIClient, xdsPort, _ int) {
331+
client.Kube().CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "gwtest"}}, metav1.CreateOptions{})
332+
333+
err = client.ApplyYAMLContents("gwtest", `kind: Gateway
334+
apiVersion: gateway.networking.k8s.io/v1
335+
metadata:
336+
name: http-gw
337+
namespace: gwtest
338+
spec:
339+
gatewayClassName: kgateway
340+
listeners:
341+
- protocol: HTTP
342+
port: 8080
343+
name: http
344+
allowedRoutes:
345+
namespaces:
346+
from: All`, `apiVersion: gateway.networking.k8s.io/v1
347+
kind: HTTPRoute
348+
metadata:
349+
name: ws-route
350+
namespace: gwtest
351+
spec:
352+
parentRefs:
353+
- name: http-gw
354+
hostnames:
355+
- "ws.example.com"
356+
rules:
357+
- backendRefs:
358+
- name: test-service
359+
port: 8080
360+
matches:
361+
- path:
362+
type: PathPrefix
363+
value: /`, `apiVersion: v1
364+
kind: Service
365+
metadata:
366+
name: test-service
367+
namespace: gwtest
368+
spec:
369+
selector:
370+
app: reviews
371+
ports:
372+
- protocol: TCP
373+
port: 8080
374+
targetPort: 8080
375+
`)
376+
if err != nil {
377+
t.Fatalf("failed to apply initial resources: %v", err)
378+
}
379+
380+
time.Sleep(time.Second * 2)
381+
t.Cleanup(func() {
382+
if t.Failed() {
383+
logKrtState(t, fmt.Sprintf("krt state for failed test: %s", t.Name()), kdbg)
384+
} else if os.Getenv("KGW_DUMP_KRT_ON_SUCCESS") == "true" {
385+
logKrtState(t, fmt.Sprintf("krt state for successful test: %s", t.Name()), kdbg)
386+
}
387+
})
388+
initialDump, err := dumpXdsConfig(t, ctx, xdsPort, "http-gw")
389+
if err != nil {
390+
t.Fatalf("failed to get initial xDS dump: %v", err)
391+
}
392+
hasWebsocketUpgrade := hasWebsocketUpgradeConfig(initialDump)
393+
if hasWebsocketUpgrade {
394+
t.Fatalf("expected no websocket upgrade in initial state, but found one")
395+
}
396+
397+
err = client.ApplyYAMLContents("gwtest", `apiVersion: v1
398+
kind: Service
399+
metadata:
400+
name: test-service
401+
namespace: gwtest
402+
spec:
403+
selector:
404+
app: reviews
405+
ports:
406+
- protocol: TCP
407+
appProtocol: kubernetes.io/ws
408+
port: 8080
409+
targetPort: 8080
410+
`)
411+
if err != nil {
412+
t.Fatalf("failed to update service with appProtocol: %v", err)
413+
}
414+
415+
time.Sleep(time.Second * 2)
416+
417+
updatedDump, err := dumpXdsConfig(t, ctx, xdsPort, "http-gw")
418+
if err != nil {
419+
t.Fatalf("failed to get updated xDS dump: %v", err)
420+
}
421+
if len(updatedDump.Routes) > 0 {
422+
hasWebsocketUpgrade := hasWebsocketUpgradeConfig(updatedDump)
423+
if !hasWebsocketUpgrade {
424+
t.Fatalf("expected websocket upgrade configuration after updating appProtocol, but found none")
425+
}
426+
t.Logf("SUCCESS: Found websocket upgrade configuration after updating appProtocol")
427+
} else {
428+
t.Fatalf("No routes found in updated dump")
429+
}
430+
431+
t.Logf("%s finished", t.Name())
432+
})
433+
}
434+
325435
func runScenario(t *testing.T, scenarioDir string, globalSettings *apisettings.Settings) {
326436
setupEnvTestAndRun(t, globalSettings, func(t *testing.T, ctx context.Context, kdbg *krt.DebugHandler, client istiokube.CLIClient, xdsPort, _ int) {
327437
// list all yamls in test data
@@ -1023,3 +1133,29 @@ func getroutesnames(l *envoylistenerv3.Listener) []string {
10231133
}
10241134
return routes
10251135
}
1136+
1137+
// dumpXdsConfig dumps the xDS config for the given gateway name. helper function to
1138+
// ensure the dumper is closed after the dump is complete.
1139+
func dumpXdsConfig(t *testing.T, ctx context.Context, xdsPort int, gwName string) (xdsDump, error) {
1140+
dumper := newXdsDumper(t, ctx, xdsPort, gwName)
1141+
defer dumper.Close()
1142+
return dumper.Dump(t, ctx)
1143+
}
1144+
1145+
// hasWebsocketUpgradeConfig checks if any route in the xDS dump has websocket upgrade configuration
1146+
func hasWebsocketUpgradeConfig(dump xdsDump) bool {
1147+
for _, route := range dump.Routes {
1148+
for _, vhost := range route.GetVirtualHosts() {
1149+
for _, r := range vhost.GetRoutes() {
1150+
if routeAction := r.GetRoute(); routeAction != nil {
1151+
for _, upgradeConfig := range routeAction.GetUpgradeConfigs() {
1152+
if upgradeConfig.GetUpgradeType() == "websocket" {
1153+
return true
1154+
}
1155+
}
1156+
}
1157+
}
1158+
}
1159+
}
1160+
return false
1161+
}

pkg/pluginsdk/ir/backend.go

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,10 @@ type BackendObjectIR struct {
142142

143143
// Errors is a list of errors, if any, encountered while constructing this BackendObject
144144
// Not added to Equals() as it is derived from the inner ObjIr, which is already evaluated
145-
// +krtEqualsTodo decide whether construction errors should impact equality
145+
// +noKrtEquals
146146
Errors []error
147147

148-
// Name is the pre-calculated resource name. used as the krt resource name.
148+
// resourceName is the pre-calculated resource name. used as the krt resource name.
149149
resourceName string
150150

151151
// TrafficDistribution is the desired traffic distribution for the backend.
@@ -167,10 +167,6 @@ func NewBackendObjectIR(objSource ObjectSource, port int32, extraKey string) Bac
167167
}
168168
}
169169

170-
func (c BackendObjectIR) ResourceName() string {
171-
return c.resourceName
172-
}
173-
174170
func BackendResourceName(objSource ObjectSource, port int32, extraKey string) string {
175171
var sb strings.Builder
176172
sb.WriteString(objSource.ResourceName())
@@ -183,22 +179,32 @@ func BackendResourceName(objSource ObjectSource, port int32, extraKey string) st
183179
return sb.String()
184180
}
185181

182+
// ResourceName returns the pre-calculated resource name for this backend.
183+
// This method is required to implement the krt.Named interface.
184+
func (c BackendObjectIR) ResourceName() string {
185+
return c.resourceName
186+
}
187+
186188
func (c BackendObjectIR) Equals(in BackendObjectIR) bool {
187-
objEq := c.ObjectSource.Equals(in.ObjectSource)
188-
objVersionEq := versionEquals(c.Obj, in.Obj)
189-
polEq := c.AttachedPolicies.Equals(in.AttachedPolicies)
190-
nameEq := c.resourceName == in.resourceName
191-
disableIstioAutoMTLSEq := c.DisableIstioAutoMTLS == in.DisableIstioAutoMTLS
192-
193-
// objIr may currently be nil in the case of k8s Services
194-
// TODO: add an IR for Services to avoid the need for this
195-
// see: internal/kgateway/extensions2/plugins/kubernetes/k8s.go
196-
objIrEq := true
197-
if c.ObjIr != nil {
198-
objIrEq = c.ObjIr.Equals(in.ObjIr)
189+
if !c.ObjectSource.Equals(in.ObjectSource) {
190+
return false
199191
}
200-
201-
return objEq && objVersionEq && objIrEq && polEq && nameEq && disableIstioAutoMTLSEq
192+
if !versionEquals(c.Obj, in.Obj) {
193+
return false
194+
}
195+
if c.ObjIr != nil && !c.ObjIr.Equals(in.ObjIr) {
196+
return false
197+
}
198+
if !c.AttachedPolicies.Equals(in.AttachedPolicies) {
199+
return false
200+
}
201+
if c.resourceName != in.resourceName {
202+
return false
203+
}
204+
if c.DisableIstioAutoMTLS != in.DisableIstioAutoMTLS {
205+
return false
206+
}
207+
return true
202208
}
203209

204210
func (c BackendObjectIR) ClusterName() string {
@@ -349,20 +355,28 @@ func (c Gateway) Equals(in Gateway) bool {
349355
c.DeniedListenerSets.Equals(in.DeniedListenerSets)
350356
}
351357

358+
type BackendRefIR struct {
359+
// TODO: remove cluster name from here, it's redundant.
360+
ClusterName string
361+
Weight uint32
362+
363+
// backend could be nil if not found or no ref grant
364+
BackendObject *BackendObjectIR
365+
// if nil, error might say why
366+
Err error
367+
}
368+
352369
// Equals returns true if the two BackendRefIR instances are equal in cluster name, weight, backend object equality, and error.
353370
func (a BackendRefIR) Equals(b BackendRefIR) bool {
354371
if a.ClusterName != b.ClusterName || a.Weight != b.Weight {
355372
return false
356373
}
357-
358374
if !backendObjectEqual(a.BackendObject, b.BackendObject) {
359375
return false
360376
}
361-
362377
if !errorsEqual(a.Err, b.Err) {
363378
return false
364379
}
365-
366380
return true
367381
}
368382

pkg/pluginsdk/ir/gw.go

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -254,23 +254,6 @@ func (l AttachedPolicies) MarshalJSON() ([]byte, error) {
254254
return json.Marshal(m)
255255
}
256256

257-
type BackendRefIR struct {
258-
// TODO: remove cluster name from here, it's redundant.
259-
ClusterName string
260-
Weight uint32
261-
262-
// backend could be nil if not found or no ref grant
263-
BackendObject *BackendObjectIR
264-
// if nil, error might say why
265-
Err error
266-
}
267-
268-
type HttpBackendOrDelegate struct {
269-
Backend *BackendRefIR
270-
Delegate *ObjectSource
271-
AttachedPolicies AttachedPolicies
272-
}
273-
274257
type HttpRouteRuleIR struct {
275258
ExtensionRefs AttachedPolicies
276259
AttachedPolicies AttachedPolicies
@@ -282,3 +265,28 @@ type HttpRouteRuleIR struct {
282265
// that should be propagated through to translation to any derived ir.HttpRouteRuleMatchIRs
283266
Err error
284267
}
268+
269+
type HttpBackendOrDelegate struct {
270+
AttachedPolicies AttachedPolicies
271+
Backend *BackendRefIR
272+
Delegate *ObjectSource
273+
}
274+
275+
func (h HttpBackendOrDelegate) Equals(other HttpBackendOrDelegate) bool {
276+
if !h.AttachedPolicies.Equals(other.AttachedPolicies) {
277+
return false
278+
}
279+
if h.Backend != nil && other.Backend != nil {
280+
return h.Backend.Equals(*other.Backend)
281+
}
282+
if h.Backend == nil && other.Backend == nil {
283+
if h.Delegate == nil && other.Delegate == nil {
284+
return true
285+
}
286+
if h.Delegate != nil && other.Delegate != nil {
287+
return h.Delegate.Equals(*other.Delegate)
288+
}
289+
return false
290+
}
291+
return false
292+
}

pkg/pluginsdk/ir/routes.go

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type Route interface {
1919
GetSourceObject() metav1.Object
2020
}
2121

22+
var _ Route = &HttpRouteIR{}
23+
2224
// this is 1:1 with httproute, and is a krt type
2325
// maybe move this to krtcollections package?
2426
type HttpRouteIR struct {
@@ -54,7 +56,6 @@ func (c HttpRouteIR) ResourceName() string {
5456
return c.ObjectSource.ResourceName()
5557
}
5658

57-
// get hostnames
5859
func (c *HttpRouteIR) GetHostnames() []string {
5960
if c == nil {
6061
return nil
@@ -99,21 +100,7 @@ func (c HttpRouteIR) rulesEqual(in HttpRouteIR) bool {
99100
return false
100101
}
101102
for j, backend := range backendsa {
102-
otherbackend := backendsb[j]
103-
if backend.Backend == nil && otherbackend.Backend == nil {
104-
continue
105-
}
106-
if backend.Backend != nil && otherbackend.Backend != nil {
107-
if backend.Backend.ClusterName != otherbackend.Backend.ClusterName {
108-
return false
109-
}
110-
if backend.Backend.Weight != otherbackend.Backend.Weight {
111-
return false
112-
}
113-
if !backend.AttachedPolicies.Equals(otherbackend.AttachedPolicies) {
114-
return false
115-
}
116-
} else {
103+
if !backend.Equals(backendsb[j]) {
117104
return false
118105
}
119106
}

0 commit comments

Comments
 (0)