From 8ab2d105a1adc68fae8765f1db7ad6932af2e632 Mon Sep 17 00:00:00 2001 From: yekta Date: Thu, 5 Nov 2026 12:38:49 +0000 Subject: [PATCH] operator: give UDP gateway listeners their own name so TCP can share the port L4 listeners were all named l4-. So a service exposing TCP and UDP on the same port produced two Gateway listeners with the same name, and the two routes pointed at the same section. UDP listeners are now named l4-udp-. TCP listeners keep their existing l4- name so current gateways and TCPRoutes don't change. --- .../resourcebuilder/networking/gateway.go | 10 ++++- .../networking/networking_test.go | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/apps/operator/internal/resourcebuilder/networking/gateway.go b/apps/operator/internal/resourcebuilder/networking/gateway.go index 9ccf803d..786a9425 100644 --- a/apps/operator/internal/resourcebuilder/networking/gateway.go +++ b/apps/operator/internal/resourcebuilder/networking/gateway.go @@ -100,8 +100,8 @@ func (p gatewayProvider) BuildRoutes(in RouteInput) ([]client.Object, error) { // Raw L4 ports: one TCP/UDP listener + matching route per port. for _, port := range l4Ports { - listenerName := gwapiv1.SectionName(fmt.Sprintf("l4-%d", *port.NodePort)) proto := portProtocol(port) + listenerName := l4ListenerName(port, proto) listenerProto := gwapiv1.TCPProtocolType routeKind := gwapiv1.Kind("TCPRoute") if proto == "UDP" { @@ -234,6 +234,14 @@ func hostListenerName(host v1.HostSpec) gwapiv1.SectionName { return gwapiv1.SectionName("https-" + hostnameKey(host)) } +// TCP listeners keep the name they had before UDP could share their port +func l4ListenerName(port v1.PortSpec, proto string) gwapiv1.SectionName { + if proto == "UDP" { + return gwapiv1.SectionName(fmt.Sprintf("l4-udp-%d", *port.NodePort)) + } + return gwapiv1.SectionName(fmt.Sprintf("l4-%d", *port.NodePort)) +} + func (p gatewayProvider) parentRef(gateway string, section *gwapiv1.SectionName) gwapiv1.ParentReference { return gwapiv1.ParentReference{ Group: ptr.To(gwapiv1.Group("gateway.networking.k8s.io")), diff --git a/apps/operator/internal/resourcebuilder/networking/networking_test.go b/apps/operator/internal/resourcebuilder/networking/networking_test.go index 2248680f..76a8a5c4 100644 --- a/apps/operator/internal/resourcebuilder/networking/networking_test.go +++ b/apps/operator/internal/resourcebuilder/networking/networking_test.go @@ -371,6 +371,45 @@ func TestGatewayL4UDP(t *testing.T) { } } +func TestGatewayL4TCPAndUDPOnSamePort(t *testing.T) { + svc := setName(&v1.Service{ + Spec: v1.ServiceSpec{ + Config: v1.ServiceConfigSpec{ + Ports: []v1.PortSpec{ + {Port: 8388, NodePort: ptr.To(int32(8388)), Protocol: ptr.To(corev1.ProtocolTCP)}, + {Port: 8388, NodePort: ptr.To(int32(8388)), Protocol: ptr.To(corev1.ProtocolUDP)}, + }, + }, + }, + }, "ss") + + objs, err := New(ProviderGateway, Config{GatewayClassName: "unbind"}).BuildRoutes(RouteInput{Service: svc}) + if err != nil { + t.Fatalf("BuildRoutes: %v", err) + } + listeners := map[gwapiv1.SectionName]gwapiv1.ProtocolType{} + for _, l := range findGateway(objs).Spec.Listeners { + listeners[l.Name] = l.Protocol + } + want := map[gwapiv1.SectionName]gwapiv1.ProtocolType{"l4-8388": gwapiv1.TCPProtocolType, "l4-udp-8388": gwapiv1.UDPProtocolType} + if !maps.Equal(listeners, want) { + t.Fatalf("listeners = %v, want %v", listeners, want) + } + + sections := map[string]gwapiv1.SectionName{} + for _, o := range objs { + switch r := o.(type) { + case *gwapiv1a2.TCPRoute: + sections["TCPRoute"] = *r.Spec.ParentRefs[0].SectionName + case *gwapiv1a2.UDPRoute: + sections["UDPRoute"] = *r.Spec.ParentRefs[0].SectionName + } + } + if sections["TCPRoute"] != "l4-8388" || sections["UDPRoute"] != "l4-udp-8388" { + t.Errorf("route sections = %v, want TCPRoute on l4-8388 and UDPRoute on l4-udp-8388", sections) + } +} + func wildcardService() *v1.Service { svc := setName(publicService(), "web") svc.Spec.Config.Hosts = append(svc.Spec.Config.Hosts, v1.HostSpec{Host: "*.example.com", Path: "/"})