diff --git a/internal/gatewayapi/contexts.go b/internal/gatewayapi/contexts.go index c38cff0b02..7b611f4880 100644 --- a/internal/gatewayapi/contexts.go +++ b/internal/gatewayapi/contexts.go @@ -124,12 +124,12 @@ func (g *GatewayContext) attachEnvoyProxy(resources *resource.Resources, epMap m return err } -func (g *GatewayContext) IncreaseAttachedListenerSets() { - if g.Status.AttachedListenerSets == nil { - g.Status.AttachedListenerSets = new(int32(1)) - } else { - *g.Status.AttachedListenerSets++ +func (g *GatewayContext) SetAttachedListenerSets(count int32) { + if count <= 0 { + g.Status.AttachedListenerSets = nil + return } + g.Status.AttachedListenerSets = &count } // ListenerContext wraps a Listener and provides helper methods for @@ -277,13 +277,17 @@ func (l *ListenerContext) IsReady() bool { conditions = l.gateway.Status.Listeners[l.listenerStatusIdx].Conditions } + // Check if Accepted=False or Programmed=False exists. for _, cond := range conditions { - if cond.Type == string(gwapiv1.ListenerConditionProgrammed) && cond.Status == metav1.ConditionTrue { - return true + if cond.Type == string(gwapiv1.ListenerConditionAccepted) && cond.Status == metav1.ConditionFalse { + return false + } + if cond.Type == string(gwapiv1.ListenerConditionProgrammed) && cond.Status == metav1.ConditionFalse { + return false } } - return false + return true } func (l *ListenerContext) GetNamespace() string { diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index ade73750e4..5edb39c6a0 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -35,9 +35,6 @@ const ( TCPProtocol = "TCP" UDPProtocol = "UDP" - L4Protocol = "L4" - L7Protocol = "L7" - // CACertKey is the key used in ConfigMaps and Secrets to store CA certificate data CACertKey = "ca.crt" // CRLKey is the key used in ConfigMaps and Secrets to store certificate revocation list data @@ -424,24 +421,24 @@ func wildcardHostnameMatchesHostname(wildcardHostname, hostname string) bool { func containsPort(ports []*protocolPort, port *protocolPort) bool { for _, protocolPort := range ports { - curProtocol, curLevel := layer4Protocol(protocolPort) - myProtocol, myLevel := layer4Protocol(port) - if protocolPort.port == port.port && (curProtocol == myProtocol && curLevel == myLevel) { + curProtocol := layer4Protocol(protocolPort) + myProtocol := layer4Protocol(port) + if protocolPort.port == port.port && curProtocol == myProtocol { return true } } return false } -// layer4Protocol returns listener L4 protocol and listen protocol level -func layer4Protocol(protocolPort *protocolPort) (string, string) { +// layer4Protocol returns listener L4 protocol +func layer4Protocol(protocolPort *protocolPort) string { switch protocolPort.protocol { case gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType, gwapiv1.TLSProtocolType: - return TCPProtocol, L7Protocol + return TCPProtocol case gwapiv1.TCPProtocolType: - return TCPProtocol, L4Protocol + return TCPProtocol default: - return UDPProtocol, L4Protocol + return UDPProtocol } } diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index c088de59b8..5324f84f29 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -242,10 +242,11 @@ func allowedRouteKindsForProtocol(protocol gwapiv1.ProtocolType, tlsMode *gwapiv func (t *Translator) validateListenerSpec(listener *ListenerContext, resources *resource.Resources) bool { // Validate listener spec directly without relying on conditions. // Start with valid assumption and invalidate on failures. - // Phase 1: Validate fundamental rules + + // Phase 1: Validate allowed namespaces specValid := t.validateAllowedNamespaces(listener) - // Phase 2: Validate allowed routes based on protocol + // Phase 2: Validate protocol support and allowed route kinds if isSupportedListenerProtocol(listener.Protocol) { var tlsMode *gwapiv1.TLSModeType if listener.TLS != nil { @@ -313,7 +314,6 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource } t.processProxyReadyListener(xdsIR[irKey], gateway.envoyProxy) t.processProxyObservability(gateway, xdsIR[irKey], infraIR[irKey].Proxy, resources) - for _, listener := range gateway.listeners { // Finalize listener conditions and check readiness. t.validateListenerConditions(listener) @@ -419,7 +419,7 @@ func (t *Translator) checkOverlappingTLSConfig(gateways []*GatewayContext) { httpsListeners := []*ListenerContext{} for _, gateway := range gateways { for _, listener := range gateway.listeners { - if listener.Protocol == gwapiv1.HTTPSProtocolType { + if listener.Protocol == gwapiv1.HTTPSProtocolType && listener.IsReady() { httpsListeners = append(httpsListeners, listener) } } @@ -434,7 +434,7 @@ func (t *Translator) checkOverlappingTLSConfig(gateways []*GatewayContext) { for _, gateway := range gateways { httpsListeners := []*ListenerContext{} for _, listener := range gateway.listeners { - if listener.Protocol == gwapiv1.HTTPSProtocolType { + if listener.Protocol == gwapiv1.HTTPSProtocolType && listener.IsReady() { httpsListeners = append(httpsListeners, listener) } } diff --git a/internal/gatewayapi/listenerset.go b/internal/gatewayapi/listenerset.go index 03cbfebcac..84a7047ccb 100644 --- a/internal/gatewayapi/listenerset.go +++ b/internal/gatewayapi/listenerset.go @@ -93,7 +93,6 @@ func (t *Translator) processListenerSet(ls *gwapiv1.ListenerSet, gatewayMap map[ } gatewayCtx.listeners = append(gatewayCtx.listeners, listenerCtx) } - gatewayCtx.IncreaseAttachedListenerSets() } // ProcessListenerSetStatus computes the status of ListenerSets after their listeners have been processed. @@ -179,6 +178,43 @@ func (t *Translator) ProcessListenerSetStatus(listenerSets []*gwapiv1.ListenerSe } } +// UpdateGatewayAttachedListenerSetCount sets Gateway.status.attachedListenerSets based on +// ListenerSet Accepted=True status. +func (t *Translator) UpdateGatewayAttachedListenerSetCount(listenerSets []*gwapiv1.ListenerSet, gateways []*GatewayContext) { + gatewayMap := make(map[types.NamespacedName]*GatewayContext, len(gateways)) + for _, gw := range gateways { + gatewayMap[types.NamespacedName{Namespace: gw.Namespace, Name: gw.Name}] = gw + // reset for current reconciliation cycle + gw.SetAttachedListenerSets(0) + } + + counts := make(map[types.NamespacedName]int32) + for _, ls := range listenerSets { + if !listenerSetAccepted(ls) { + continue + } + parentNamespace := NamespaceDerefOr(ls.Spec.ParentRef.Namespace, ls.Namespace) + key := types.NamespacedName{Namespace: parentNamespace, Name: string(ls.Spec.ParentRef.Name)} + if _, ok := gatewayMap[key]; !ok { + continue + } + counts[key]++ + } + + for key, count := range counts { + gatewayMap[key].SetAttachedListenerSets(count) + } +} + +func listenerSetAccepted(ls *gwapiv1.ListenerSet) bool { + for _, cond := range ls.Status.Conditions { + if cond.Type == string(gwapiv1.ListenerSetConditionAccepted) { + return cond.Status == metav1.ConditionTrue + } + } + return false +} + func (t *Translator) isListenerSetAllowed(gateway *gwapiv1.Gateway, ls *gwapiv1.ListenerSet) bool { // If AllowedListeners is not set, attachment is not allowed (default is None) if gateway.Spec.AllowedListeners == nil || gateway.Spec.AllowedListeners.Namespaces == nil || gateway.Spec.AllowedListeners.Namespaces.From == nil { diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml index 20c4241d1a..0d839b9185 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml @@ -33,8 +33,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null @@ -56,8 +61,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml index ecb8190327..3f23b129a4 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml @@ -31,8 +31,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null @@ -54,8 +59,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.in.yaml index 4d899da6a8..103498cfa7 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.in.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.in.yaml @@ -25,6 +25,26 @@ gateways: mode: Terminate certificateRefs: - name: tls-secret-1 + - allowedRoutes: + namespaces: + from: All + name: udp-8162 + port: 8162 + protocol: UDP + # This listener(http-8162) is valid because HTTP + UDP are compatible protocols. + - allowedRoutes: + namespaces: + from: All + name: http-8162 + port: 8162 + protocol: HTTP + # This listener(tcp-8162) is invalid because TCP is not compatible with HTTP. + - allowedRoutes: + namespaces: + from: All + name: tcp-8162 + port: 8162 + protocol: TCP secrets: - apiVersion: v1 kind: Secret diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml index 4af2173f3d..cc7e5f875b 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml @@ -25,20 +25,38 @@ gateways: certificateRefs: - name: tls-secret-1 mode: Terminate + - allowedRoutes: + namespaces: + from: All + name: udp-8162 + port: 8162 + protocol: UDP + - allowedRoutes: + namespaces: + from: All + name: http-8162 + port: 8162 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + name: tcp-8162 + port: 8162 + protocol: TCP status: listeners: - attachedRoutes: 1 conditions: - lastTransitionTime: null - message: All listeners for a given port must use a compatible protocol - reason: ProtocolConflict + message: Sending translated listener configuration to the data plane + reason: Programmed status: "True" - type: Conflicted - - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid - status: "False" type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted - lastTransitionTime: null message: Listener references have been resolved reason: ResolvedRefs @@ -50,6 +68,50 @@ gateways: kind: HTTPRoute - group: gateway.networking.k8s.io kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: udp-8162 + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute - attachedRoutes: 1 conditions: - lastTransitionTime: null @@ -58,8 +120,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict status: "False" type: Programmed - lastTransitionTime: null @@ -67,12 +134,38 @@ gateways: reason: ResolvedRefs status: "True" type: ResolvedRefs - name: http-2 + name: http-8162 supportedKinds: - group: gateway.networking.k8s.io kind: HTTPRoute - group: gateway.networking.k8s.io kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: tcp-8162 + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute httpRoutes: - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute @@ -94,9 +187,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route @@ -110,6 +203,19 @@ httpRoutes: infraIR: envoy-gateway/gateway-1: proxy: + listeners: + - name: envoy-gateway/gateway-1/http-1 + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + - name: envoy-gateway/gateway-1/udp-8162 + ports: + - containerPort: 8162 + name: udp-8162 + protocol: UDP + servicePort: 8162 metadata: labels: gateway.envoyproxy.io/owning-gateway-name: gateway-1 @@ -145,8 +251,115 @@ xdsIR: sectionName: "8080" name: envoy-gateway/gateway-1 protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - foo.com + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http-1 + name: envoy-gateway/gateway-1/http-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: foo.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/foo_com + pathMatch: + distinct: false + name: "" + prefix: / + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - bar.com + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http-2 + name: envoy-gateway/gateway-1/http-2 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: bar.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/bar_com + pathMatch: + distinct: false + name: "" + prefix: / + tls: + alpnProtocols: null + certificates: + - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A0QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: envoy-gateway/tls-secret-1 + privateKey: '[redacted]' readyListener: address: 0.0.0.0 ipFamily: IPv4 path: /ready port: 19003 + udp: + - address: 0.0.0.0 + externalPort: 8162 + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: udp-8162 + name: envoy-gateway/gateway-1/udp-8162 + port: 8162 diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml index 1a31d49847..0dbba72c77 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml @@ -30,8 +30,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict status: "False" type: Programmed - lastTransitionTime: null @@ -53,8 +58,13 @@ gateways: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict status: "False" type: Programmed - lastTransitionTime: null diff --git a/internal/gatewayapi/testdata/listenerset-conflict-listeners.out.yaml b/internal/gatewayapi/testdata/listenerset-conflict-listeners.out.yaml index c8ead033c5..70a543d7b9 100644 --- a/internal/gatewayapi/testdata/listenerset-conflict-listeners.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-conflict-listeners.out.yaml @@ -54,15 +54,15 @@ gateways: - attachedRoutes: 0 conditions: - lastTransitionTime: null - message: All listeners for a given port must use a unique hostname - reason: HostnameConflict + message: Sending translated listener configuration to the data plane + reason: Programmed status: "True" - type: Conflicted - - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: Invalid - status: "False" type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted - lastTransitionTime: null message: Listener references have been resolved reason: ResolvedRefs @@ -84,6 +84,18 @@ infraIR: name: http-80 protocol: HTTP servicePort: 80 + - name: gateway-xls/composite-gateway/conflict-listener + ports: + - containerPort: 8888 + name: http-8888 + protocol: HTTP + servicePort: 8888 + - name: gateway-xls/composite-gateway/gateway-xls/conflict-listener-from-same-xls/conflict-listener-1 + ports: + - containerPort: 8089 + name: http-8089 + protocol: HTTP + servicePort: 8089 - name: gateway-xls/composite-gateway/gateway-xls/conflict-listener-from-two-xlss/good-listener ports: - containerPort: 8090 @@ -130,33 +142,38 @@ listenerSets: status: conditions: - lastTransitionTime: null - message: No listeners are accepted + message: Some listeners are invalid reason: ListenersNotValid - status: "False" + status: "True" type: Accepted - lastTransitionTime: null - message: No listeners are programmed - reason: ListenersNotValid - status: "False" + message: Some listeners are not programmed + reason: Programmed + status: "True" type: Programmed listeners: - attachedRoutes: 0 conditions: - lastTransitionTime: null - message: All listeners for a given port must use a unique hostname - reason: HostnameConflict + message: Sending translated listener configuration to the data plane + reason: Programmed status: "True" - type: Conflicted - - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: ListenersNotValid - status: "False" type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted - lastTransitionTime: null message: Listener references have been resolved reason: ResolvedRefs status: "True" type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted name: conflict-listener-1 supportedKinds: - group: gateway.networking.k8s.io @@ -171,8 +188,13 @@ listenerSets: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: ListenersNotValid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null @@ -228,8 +250,13 @@ listenerSets: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: ListenersNotValid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null @@ -313,8 +340,13 @@ listenerSets: status: "True" type: Conflicted - lastTransitionTime: null - message: Listener is invalid, see other Conditions for details. - reason: ListenersNotValid + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict status: "False" type: Programmed - lastTransitionTime: null @@ -397,6 +429,34 @@ xdsIR: escapedSlashesAction: UnescapeAndRedirect mergeSlashes: true port: 10080 + - address: 0.0.0.0 + externalPort: 8888 + hostnames: + - '*' + metadata: + kind: Gateway + name: composite-gateway + namespace: gateway-xls + sectionName: conflict-listener + name: gateway-xls/composite-gateway/conflict-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8888 + - address: 0.0.0.0 + externalPort: 8089 + hostnames: + - '*' + metadata: + kind: Gateway + name: composite-gateway + namespace: gateway-xls + sectionName: conflict-listener-1 + name: gateway-xls/composite-gateway/gateway-xls/conflict-listener-from-same-xls/conflict-listener-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8089 - address: 0.0.0.0 externalPort: 8090 hostnames: diff --git a/internal/gatewayapi/testdata/listenerset-hostname-conflict.in.yaml b/internal/gatewayapi/testdata/listenerset-hostname-conflict.in.yaml new file mode 100644 index 0000000000..17ec739ae1 --- /dev/null +++ b/internal/gatewayapi/testdata/listenerset-hostname-conflict.in.yaml @@ -0,0 +1,134 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + allowedListeners: + namespaces: + from: Same + listeners: + - name: gateway-listener + port: 80 + protocol: HTTP + hostname: "gateway-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be accepted based on listener precedence + - name: hostname-conflict-with-gateway-listener + port: 80 + protocol: HTTP + hostname: "hostname-conflict-with-gateway-listener.com" + allowedRoutes: + namespaces: + from: All +listenerSets: + # The listener `hostname-conflict-with-gateway-listener` should be rejected but the listenerSet should be accepted since it has other valid listeners + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-gateway-1 + namespace: envoy-gateway + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + listeners: + - name: listener-set-1-listener + port: 80 + protocol: HTTP + hostname: "listener-set-1-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be rejected since it conflicts with the gateway listener + - name: hostname-conflict-with-gateway-listener + port: 80 + protocol: HTTP + hostname: "hostname-conflict-with-gateway-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be accepted based on listener precedence + - name: hostname-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + hostname: "hostname-conflict-with-listener-set-listener.com" + allowedRoutes: + namespaces: + from: All + # This listenerSet should not be accepted since its only listener `hostname-conflict-with-gateway-listener` is rejected + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-gateway-2 + namespace: envoy-gateway + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + listeners: + # The following listener should be rejected since it conflicts with the gateway listener + - name: hostname-conflict-with-gateway-listener + port: 80 + protocol: HTTP + hostname: "hostname-conflict-with-gateway-listener.com" + allowedRoutes: + namespaces: + from: All + # The listener `hostname-conflict-with-listener-set-listener` should be rejected but the listenerSet should be accepted since it has other valid listeners + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-listener-set-1 + namespace: envoy-gateway + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + listeners: + - name: listener-set-2-listener + port: 80 + protocol: HTTP + hostname: "listener-set-2-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be rejected since it conflicts with another listenerSet's listener + - name: hostname-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + hostname: "hostname-conflict-with-listener-set-listener.com" + allowedRoutes: + namespaces: + from: All + # This listenerSet should not be accepted since its only listener `hostname-conflict-with-listener-set-listener` is rejected + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-listener-set-2 + namespace: envoy-gateway + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + listeners: + # The following listener should be rejected since it conflicts with another listenerSet's listener + - name: hostname-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + hostname: "hostname-conflict-with-listener-set-listener.com" + allowedRoutes: + namespaces: + from: All diff --git a/internal/gatewayapi/testdata/listenerset-hostname-conflict.out.yaml b/internal/gatewayapi/testdata/listenerset-hostname-conflict.out.yaml new file mode 100644 index 0000000000..b42e158066 --- /dev/null +++ b/internal/gatewayapi/testdata/listenerset-hostname-conflict.out.yaml @@ -0,0 +1,542 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + spec: + allowedListeners: + namespaces: + from: Same + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: gateway-listener.com + name: gateway-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: hostname-conflict-with-gateway-listener.com + name: hostname-conflict-with-gateway-listener + port: 80 + protocol: HTTP + status: + attachedListenerSets: 2 + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: hostname-conflict-with-gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-with-listenerset-hostname-conflict: + proxy: + listeners: + - name: envoy-gateway/gateway-with-listenerset-hostname-conflict/gateway-listener + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-with-listenerset-hostname-conflict + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway-system +listenerSets: +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-gateway-1 + namespace: envoy-gateway + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: listener-set-1-listener.com + name: listener-set-1-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: hostname-conflict-with-gateway-listener.com + name: hostname-conflict-with-gateway-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: hostname-conflict-with-listener-set-listener.com + name: hostname-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Some listeners are invalid + reason: ListenersNotValid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Some listeners are not programmed + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: listener-set-1-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: hostname-conflict-with-gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: hostname-conflict-with-listener-set-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-gateway-2 + namespace: envoy-gateway + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: hostname-conflict-with-gateway-listener.com + name: hostname-conflict-with-gateway-listener + port: 80 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: No listeners are accepted + reason: ListenersNotValid + status: "False" + type: Accepted + - lastTransitionTime: null + message: No listeners are programmed + reason: ListenersNotValid + status: "False" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: hostname-conflict-with-gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-listener-set-1 + namespace: envoy-gateway + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: listener-set-2-listener.com + name: listener-set-2-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: hostname-conflict-with-listener-set-listener.com + name: hostname-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Some listeners are invalid + reason: ListenersNotValid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Some listeners are not programmed + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: listener-set-2-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: hostname-conflict-with-listener-set-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-hostname-conflict-with-listener-set-2 + namespace: envoy-gateway + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: hostname-conflict-with-listener-set-listener.com + name: hostname-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: No listeners are accepted + reason: ListenersNotValid + status: "False" + type: Accepted + - lastTransitionTime: null + message: No listeners are programmed + reason: ListenersNotValid + status: "False" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a unique hostname + reason: HostnameConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: hostname-conflict-with-listener-set-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +xdsIR: + envoy-gateway/gateway-with-listenerset-hostname-conflict: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-with-listenerset-hostname-facad556 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-with-listenerset-hostname-conflict + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-with-listenerset-hostname-facad556 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-with-listenerset-hostname-conflict + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - gateway-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + sectionName: gateway-listener + name: envoy-gateway/gateway-with-listenerset-hostname-conflict/gateway-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - hostname-conflict-with-gateway-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + sectionName: hostname-conflict-with-gateway-listener + name: envoy-gateway/gateway-with-listenerset-hostname-conflict/hostname-conflict-with-gateway-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - listener-set-1-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + sectionName: listener-set-1-listener + name: envoy-gateway/gateway-with-listenerset-hostname-conflict/envoy-gateway/listenerset-with-hostname-conflict-with-gateway-1/listener-set-1-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - hostname-conflict-with-listener-set-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + sectionName: hostname-conflict-with-listener-set-listener + name: envoy-gateway/gateway-with-listenerset-hostname-conflict/envoy-gateway/listenerset-with-hostname-conflict-with-gateway-1/hostname-conflict-with-listener-set-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - listener-set-2-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-hostname-conflict + namespace: envoy-gateway + sectionName: listener-set-2-listener + name: envoy-gateway/gateway-with-listenerset-hostname-conflict/envoy-gateway/listenerset-with-hostname-conflict-with-listener-set-1/listener-set-2-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml index f56dd359de..be176e0d7f 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml @@ -20,7 +20,6 @@ gateways: port: 80 protocol: HTTP status: - attachedListenerSets: 1 listeners: - attachedRoutes: 0 conditions: diff --git a/internal/gatewayapi/testdata/listenerset-invalid.out.yaml b/internal/gatewayapi/testdata/listenerset-invalid.out.yaml index e09a3482d6..f2d9011df2 100644 --- a/internal/gatewayapi/testdata/listenerset-invalid.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-invalid.out.yaml @@ -20,7 +20,7 @@ gateways: port: 80 protocol: HTTP status: - attachedListenerSets: 2 + attachedListenerSets: 1 listeners: - attachedRoutes: 0 conditions: diff --git a/internal/gatewayapi/testdata/listenerset-protocol-conflict-udp-first.in.yaml b/internal/gatewayapi/testdata/listenerset-protocol-conflict-udp-first.in.yaml new file mode 100644 index 0000000000..eb2e1b32a0 --- /dev/null +++ b/internal/gatewayapi/testdata/listenerset-protocol-conflict-udp-first.in.yaml @@ -0,0 +1,64 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + allowedListeners: + namespaces: + from: Same + listeners: + - name: gateway-listener + port: 8080 + protocol: HTTP + hostname: "gateway-listener.com" + allowedRoutes: + namespaces: + from: All +listenerSets: + # A UDP listener can share the same port with HTTP or TCP listeners. + # (Only duplicate listeners of the same Layer 4 protocol on one port conflict.) + # This test verifies that: + # - a first UDP listener and following HTTP listener on port 80 can both be accepted, + # - a later TCP listener still conflicts with the existing HTTP listener on port 80, + # - and another HTTP listener after that remains valid. + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-udp-first + namespace: envoy-gateway + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + listeners: + - name: udp-first + port: 80 + protocol: UDP + allowedRoutes: + namespaces: + from: All + - name: http-after-udp + port: 80 + protocol: HTTP + hostname: "http-after-udp.example.com" + allowedRoutes: + namespaces: + from: All + - name: tcp-after-http + port: 80 + protocol: TCP + allowedRoutes: + namespaces: + from: All + - name: http-second-after-tcp + port: 80 + protocol: HTTP + hostname: "http-second-after-tcp.example.com" + allowedRoutes: + namespaces: + from: All diff --git a/internal/gatewayapi/testdata/listenerset-protocol-conflict-udp-first.out.yaml b/internal/gatewayapi/testdata/listenerset-protocol-conflict-udp-first.out.yaml new file mode 100644 index 0000000000..6904ffeca6 --- /dev/null +++ b/internal/gatewayapi/testdata/listenerset-protocol-conflict-udp-first.out.yaml @@ -0,0 +1,320 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + spec: + allowedListeners: + namespaces: + from: Same + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: gateway-listener.com + name: gateway-listener + port: 8080 + protocol: HTTP + status: + attachedListenerSets: 1 + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-with-listenerset-udp-first: + proxy: + listeners: + - name: envoy-gateway/gateway-with-listenerset-udp-first/gateway-listener + ports: + - containerPort: 8080 + name: http-8080 + protocol: HTTP + servicePort: 8080 + - name: envoy-gateway/gateway-with-listenerset-udp-first/envoy-gateway/listenerset-udp-first/udp-first + ports: + - containerPort: 10080 + name: udp-80 + protocol: UDP + servicePort: 80 + - name: envoy-gateway/gateway-with-listenerset-udp-first/envoy-gateway/listenerset-udp-first/http-after-udp + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-with-listenerset-udp-first + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-with-listenerset-udp-first + namespace: envoy-gateway-system +listenerSets: +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-udp-first + namespace: envoy-gateway + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + name: udp-first + port: 80 + protocol: UDP + - allowedRoutes: + namespaces: + from: All + hostname: http-after-udp.example.com + name: http-after-udp + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + name: tcp-after-http + port: 80 + protocol: TCP + - allowedRoutes: + namespaces: + from: All + hostname: http-second-after-tcp.example.com + name: http-second-after-tcp + port: 80 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Some listeners are invalid + reason: ListenersNotValid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Some listeners are not programmed + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: udp-first + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: http-after-udp + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: tcp-after-http + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: http-second-after-tcp + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +xdsIR: + envoy-gateway/gateway-with-listenerset-udp-first: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-with-listenerset-udp-first-2aa56f18 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-with-listenerset-udp-first + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-with-listenerset-udp-first-2aa56f18 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-with-listenerset-udp-first + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 8080 + hostnames: + - gateway-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + sectionName: gateway-listener + name: envoy-gateway/gateway-with-listenerset-udp-first/gateway-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - http-after-udp.example.com + metadata: + kind: Gateway + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + sectionName: http-after-udp + name: envoy-gateway/gateway-with-listenerset-udp-first/envoy-gateway/listenerset-udp-first/http-after-udp + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - http-second-after-tcp.example.com + metadata: + kind: Gateway + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + sectionName: http-second-after-tcp + name: envoy-gateway/gateway-with-listenerset-udp-first/envoy-gateway/listenerset-udp-first/http-second-after-tcp + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + udp: + - address: 0.0.0.0 + externalPort: 80 + metadata: + kind: Gateway + name: gateway-with-listenerset-udp-first + namespace: envoy-gateway + sectionName: udp-first + name: envoy-gateway/gateway-with-listenerset-udp-first/envoy-gateway/listenerset-udp-first/udp-first + port: 10080 diff --git a/internal/gatewayapi/testdata/listenerset-protocol-conflict.in.yaml b/internal/gatewayapi/testdata/listenerset-protocol-conflict.in.yaml new file mode 100644 index 0000000000..615a57d6dd --- /dev/null +++ b/internal/gatewayapi/testdata/listenerset-protocol-conflict.in.yaml @@ -0,0 +1,162 @@ +namespaces: + - apiVersion: v1 + kind: Namespace + metadata: + name: gateway-conformance-infra +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + spec: + gatewayClassName: envoy-gateway-class + allowedListeners: + namespaces: + from: Same + listeners: + - name: gateway-listener + port: 80 + protocol: HTTP + hostname: "gateway-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be accepted based on listener precedence + - name: protocol-conflict-with-gateway-listener + port: 80 + protocol: HTTP + hostname: "protocol-conflict-with-gateway-listener.com" + allowedRoutes: + namespaces: + from: All +listenerSets: + # The listener `protocol-conflict-with-gateway-listener` should be rejected but the listenerSet should be accepted since it has other valid listeners + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-gateway-1 + namespace: gateway-conformance-infra + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + listeners: + - name: listener-set-1-listener + port: 80 + protocol: HTTP + hostname: "listener-set-1-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be rejected since it conflicts with the gateway listener + - name: protocol-conflict-with-gateway-listener + port: 80 + protocol: TCP + allowedRoutes: + namespaces: + from: All + # The following listener should be accepted based on listener precedence + - name: protocol-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + hostname: "protocol-conflict-with-listener-set-listener.com" + allowedRoutes: + namespaces: + from: All + # This listenerSet should not be accepted since its only listener `protocol-conflict-with-gateway-listener` is rejected + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-gateway-2 + namespace: gateway-conformance-infra + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + listeners: + # The following listener should be rejected since it conflicts with the gateway listener + - name: protocol-conflict-with-gateway-listener + port: 80 + protocol: TCP + allowedRoutes: + namespaces: + from: All + # The listener `protocol-conflict-with-listener-set-listener` should be rejected but the listenerSet should be accepted since it has other valid listeners + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-listener-set-1 + namespace: gateway-conformance-infra + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + listeners: + - name: listener-set-2-listener + port: 80 + protocol: HTTP + hostname: "listener-set-2-listener.com" + allowedRoutes: + namespaces: + from: All + # The following listener should be rejected since it conflicts with another listenerSet's listener + - name: protocol-conflict-with-listener-set-listener + port: 80 + protocol: TCP + allowedRoutes: + namespaces: + from: All + # This listenerSet should not be accepted since its only listener `protocol-conflict-with-listener-set-listener` is rejected + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-listener-set-2 + namespace: gateway-conformance-infra + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + listeners: + # The following listener should be rejected since it conflicts with another listenerSet's listener + - name: protocol-conflict-with-listener-set-listener + port: 80 + protocol: TCP + allowedRoutes: + namespaces: + from: All + # The first listener is invalid due to unsupported protocol and should not block + # the following valid listener on the same port. + - apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-invalid-first-should-not-block-valid + namespace: gateway-conformance-infra + spec: + parentRef: + kind: Gateway + group: gateway.networking.k8s.io + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + listeners: + - name: invalid-unsupported-protocol-first + port: 81 + protocol: SCTP + allowedRoutes: + namespaces: + from: All + - name: valid-http-after-invalid + port: 81 + protocol: HTTP + hostname: "valid-http-after-invalid.com" + allowedRoutes: + namespaces: + from: All diff --git a/internal/gatewayapi/testdata/listenerset-protocol-conflict.out.yaml b/internal/gatewayapi/testdata/listenerset-protocol-conflict.out.yaml new file mode 100644 index 0000000000..b177eaf5e1 --- /dev/null +++ b/internal/gatewayapi/testdata/listenerset-protocol-conflict.out.yaml @@ -0,0 +1,634 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + spec: + allowedListeners: + namespaces: + from: Same + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: gateway-listener.com + name: gateway-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: protocol-conflict-with-gateway-listener.com + name: protocol-conflict-with-gateway-listener + port: 80 + protocol: HTTP + status: + attachedListenerSets: 3 + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: protocol-conflict-with-gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + gateway-conformance-infra/gateway-with-listenerset-protocol-conflict: + proxy: + listeners: + - name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-listener + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + - name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-conformance-infra/listenerset-invalid-first-should-not-block-valid/valid-http-after-invalid + ports: + - containerPort: 10081 + name: http-81 + protocol: HTTP + servicePort: 81 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-with-listenerset-protocol-conflict + gateway.envoyproxy.io/owning-gateway-namespace: gateway-conformance-infra + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict + namespace: envoy-gateway-system +listenerSets: +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-gateway-1 + namespace: gateway-conformance-infra + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: listener-set-1-listener.com + name: listener-set-1-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + name: protocol-conflict-with-gateway-listener + port: 80 + protocol: TCP + - allowedRoutes: + namespaces: + from: All + hostname: protocol-conflict-with-listener-set-listener.com + name: protocol-conflict-with-listener-set-listener + port: 80 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + status: + conditions: + - lastTransitionTime: null + message: Some listeners are invalid + reason: ListenersNotValid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Some listeners are not programmed + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: listener-set-1-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: protocol-conflict-with-gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: protocol-conflict-with-listener-set-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-gateway-2 + namespace: gateway-conformance-infra + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + name: protocol-conflict-with-gateway-listener + port: 80 + protocol: TCP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + status: + conditions: + - lastTransitionTime: null + message: No listeners are accepted + reason: ListenersNotValid + status: "False" + type: Accepted + - lastTransitionTime: null + message: No listeners are programmed + reason: ListenersNotValid + status: "False" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Only one TCP listener is allowed in a given port + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: protocol-conflict-with-gateway-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-listener-set-1 + namespace: gateway-conformance-infra + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: listener-set-2-listener.com + name: listener-set-2-listener + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + name: protocol-conflict-with-listener-set-listener + port: 80 + protocol: TCP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + status: + conditions: + - lastTransitionTime: null + message: Some listeners are invalid + reason: ListenersNotValid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Some listeners are not programmed + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: listener-set-2-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Only one TCP listener is allowed in a given port + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: protocol-conflict-with-listener-set-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-with-protocol-conflict-with-listener-set-2 + namespace: gateway-conformance-infra + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + name: protocol-conflict-with-listener-set-listener + port: 80 + protocol: TCP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + status: + conditions: + - lastTransitionTime: null + message: No listeners are accepted + reason: ListenersNotValid + status: "False" + type: Accepted + - lastTransitionTime: null + message: No listeners are programmed + reason: ListenersNotValid + status: "False" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Only one TCP listener is allowed in a given port + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: protocol-conflict-with-listener-set-listener + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: ListenerSet + metadata: + name: listenerset-invalid-first-should-not-block-valid + namespace: gateway-conformance-infra + spec: + listeners: + - allowedRoutes: + namespaces: + from: All + name: invalid-unsupported-protocol-first + port: 81 + protocol: SCTP + - allowedRoutes: + namespaces: + from: All + hostname: valid-http-after-invalid.com + name: valid-http-after-invalid + port: 81 + protocol: HTTP + parentRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + status: + conditions: + - lastTransitionTime: null + message: Some listeners are invalid + reason: ListenersNotValid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Some listeners are not programmed + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Protocol SCTP is unsupported, must be HTTP, HTTPS, TCP or UDP. + reason: UnsupportedProtocol + status: "False" + type: Accepted + - lastTransitionTime: null + message: Listener is invalid, see other Conditions for details. + reason: ListenersNotValid + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: invalid-unsupported-protocol-first + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: No conflicts detected + reason: NoConflicts + status: "False" + type: Conflicted + name: valid-http-after-invalid + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +xdsIR: + gateway-conformance-infra/gateway-with-listenerset-protocol-conflict: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-gateway-conformance-infra-gateway-with-listeners-eaf6e342 + namespace: envoy-gateway-system + sectionName: "8080" + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-gateway-conformance-infra-gateway-with-listeners-eaf6e342 + namespace: envoy-gateway-system + sectionName: "8080" + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - gateway-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + sectionName: gateway-listener + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - protocol-conflict-with-gateway-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + sectionName: protocol-conflict-with-gateway-listener + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/protocol-conflict-with-gateway-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - listener-set-1-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + sectionName: listener-set-1-listener + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-conformance-infra/listenerset-with-protocol-conflict-with-gateway-1/listener-set-1-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - protocol-conflict-with-listener-set-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + sectionName: protocol-conflict-with-listener-set-listener + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-conformance-infra/listenerset-with-protocol-conflict-with-gateway-1/protocol-conflict-with-listener-set-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - listener-set-2-listener.com + metadata: + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + sectionName: listener-set-2-listener + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-conformance-infra/listenerset-with-protocol-conflict-with-listener-set-1/listener-set-2-listener + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + externalPort: 81 + hostnames: + - valid-http-after-invalid.com + metadata: + kind: Gateway + name: gateway-with-listenerset-protocol-conflict + namespace: gateway-conformance-infra + sectionName: valid-http-after-invalid + name: gateway-conformance-infra/gateway-with-listenerset-protocol-conflict/gateway-conformance-infra/listenerset-invalid-first-should-not-block-valid/valid-http-after-invalid + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10081 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/merge-invalid-multiple-gateways-conflicting-layer4-protocol.in.yaml b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways-conflicting-layer4-protocol.in.yaml new file mode 100644 index 0000000000..d8ec5e7ae1 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways-conflicting-layer4-protocol.in.yaml @@ -0,0 +1,70 @@ +envoyProxyForGatewayClass: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + # This listener conflicts with gateway-1's HTTP/80: both map to the same + # TCP port in the merged infra, but they use incompatible protocols. + - name: tcp + port: 80 + protocol: TCP + allowedRoutes: + namespaces: + from: Same +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +tcpRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TCPRoute + metadata: + namespace: default + name: tcproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + rules: + - backendRefs: + - name: service-1 + port: 8163 diff --git a/internal/gatewayapi/testdata/merge-invalid-multiple-gateways-conflicting-layer4-protocol.out.yaml b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways-conflicting-layer4-protocol.out.yaml new file mode 100644 index 0000000000..3c403b8251 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways-conflicting-layer4-protocol.out.yaml @@ -0,0 +1,204 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: tcp + port: 80 + protocol: TCP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Accepted + - lastTransitionTime: null + message: All listeners for a given port must use a compatible protocol + reason: ProtocolConflict + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: tcp + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: No listeners included by this parent ref allowed this attachment. + reason: NotAllowedByListeners + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + status: {} + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway-class + namespace: envoy-gateway-system +tcpRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TCPRoute + metadata: + name: tcproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8163 + status: + parents: + - conditions: + - lastTransitionTime: null + message: No listeners included by this parent ref allowed this attachment. + reason: NotAllowedByListeners + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway +xdsIR: + envoy-gateway-class: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-class-3b1df594 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway-class + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-class-3b1df594 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway-class + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index eae1551cfe..4d83de82df 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -298,6 +298,7 @@ func (t *Translator) Translate(resources *resource.Resources) (*TranslateResult, // Compute ListenerSet status based on listener processing results // This should be done after ProcessListeners because ListenerSet status depends on listener processing results t.ProcessListenerSetStatus(resources.ListenerSets) + t.UpdateGatewayAttachedListenerSetCount(resources.ListenerSets, acceptedGateways) // Process EnvoyPatchPolicies envoyPatchPolicies := t.ProcessEnvoyPatchPolicies(resources.EnvoyPatchPolicies, xdsIR) diff --git a/internal/gatewayapi/validate.go b/internal/gatewayapi/validate.go index 4386d32d30..40f8f2d903 100644 --- a/internal/gatewayapi/validate.go +++ b/internal/gatewayapi/validate.go @@ -385,10 +385,8 @@ func hasInvalidCondition(listener *ListenerContext) bool { // isSpecValidForConflictChecks returns whether a listener should participate in // conflict detection. In the normal translation flow this is driven by -// listener.specValid. The fallback to hasInvalidCondition exists only for unit -// tests that invoke conflict checks directly without running per-listener spec -// validation (Phase 1) first. Production code paths always run validateListenerSpec -// before conflict detection. +// listener.specValid. The fallback keeps direct unit tests meaningful when they +// invoke conflict checks without running per-listener spec validation first. func isSpecValidForConflictChecks(listener *ListenerContext) bool { if listener.specValid { return true @@ -748,7 +746,6 @@ func (t *Translator) validateAllowedRoutes(listener *ListenerContext, routeKinds unSupportedKinds := make([]gwapiv1.RouteGroupKind, 0) for _, kind := range listener.AllowedRoutes.Kinds { - // if there is a group it must match `gateway.networking.k8s.io` if kind.Group != nil && string(*kind.Group) != gwapiv1.GroupName { listener.SetCondition( @@ -792,13 +789,18 @@ func (t *Translator) validateAllowedRoutes(listener *ListenerContext, routeKinds specValid = false } - listener.SetSupportedKinds(supportedKinds...) + // If no kinds were explicitly specified but there were no errors, set default supported kinds + if len(supportedKinds) == 0 && specValid { + listener.SetSupportedKinds(canSupportKinds...) + } else { + listener.SetSupportedKinds(supportedKinds...) + } return specValid } type portListeners struct { listeners []*ListenerContext - protocols sets.Set[string] + protocols map[string]bool hostnames map[string]int } @@ -830,115 +832,263 @@ func (t *Translator) validateConflictedMergedListeners(gateways []*GatewayContex } } -// validateConflictedProtocolsListeners checks for listeners that have conflicting protocols on the same port. -// UDP can coexist with any protocol. HTTPS and TLS are treated as compatible via getProtocolForListener. +// validateConflictedProtocolsListeners checks for listeners that have conflicting protocols on the same port and sets the Conflicted condition on those listeners. func (t *Translator) validateConflictedProtocolsListeners(gateways []*GatewayContext) { - validateByPort := func(listeners []*ListenerContext) { - portListenerInfo := map[gwapiv1.PortNumber][]*ListenerContext{} - for _, listener := range listeners { - if !isSpecValidForConflictChecks(listener) || !isSupportedListenerProtocol(listener.Protocol) { + // Detect listeners that share a port but use incompatible protocols. + // HTTPS and TLS may coexist; HTTP and TCP may not coexist with anything else on the same port. + // + // In merge mode, all gateways collapse to a single IR key, so listeners must be + // checked across the full set. Otherwise, conflicts are scoped per gateway. + if t.MergeGateways { + var all []*ListenerContext + for _, g := range gateways { + all = append(all, g.listeners...) + } + markProtocolConflicts(collectPortListeners(all)) + return + } + + for _, g := range gateways { + markProtocolConflicts(collectPortListeners(g.listeners)) + } +} + +// collectPortListeners groups the provided listeners by port, skipping any with unsupported protocols. +func collectPortListeners(listeners []*ListenerContext) map[gwapiv1.PortNumber]*portListeners { + portListenerInfo := map[gwapiv1.PortNumber]*portListeners{} + for _, listener := range listeners { + // Skip listeners that already failed per-listener validation. + // This avoids invalid listeners blocking valid listeners in conflict checks. + if !isSpecValidForConflictChecks(listener) { + continue + } + if !isSupportedListenerProtocol(listener.Protocol) { + continue + } + if portListenerInfo[listener.Port] == nil { + portListenerInfo[listener.Port] = &portListeners{ + hostnames: map[string]int{}, + protocols: map[string]bool{}, + } + } + portListenerInfo[listener.Port].listeners = append(portListenerInfo[listener.Port].listeners, listener) + protocol := getProtocolForListener(listener) + portListenerInfo[listener.Port].protocols[protocol] = true + } + return portListenerInfo +} + +func setProtocolConflictConditions(listener *ListenerContext) { + listener.SetCondition( + gwapiv1.ListenerConditionConflicted, + metav1.ConditionTrue, + gwapiv1.ListenerReasonProtocolConflict, + "All listeners for a given port must use a compatible protocol", + ) + // TODO: remove following once https://github.com/kubernetes-sigs/gateway-api/pull/4692 landed, + // we may want to have similar logic when protocol conflict happened. + listener.SetCondition( + gwapiv1.ListenerConditionAccepted, + metav1.ConditionFalse, + gwapiv1.ListenerReasonProtocolConflict, + "All listeners for a given port must use a compatible protocol", + ) + listener.SetCondition( + gwapiv1.ListenerConditionProgrammed, + metav1.ConditionFalse, + gwapiv1.ListenerReasonProtocolConflict, + "All listeners for a given port must use a compatible protocol", + ) +} + +// markProtocolConflicts iterates portListenerInfo and marks listeners with protocol conflicts. +func markProtocolConflicts(portListenerInfo map[gwapiv1.PortNumber]*portListeners) { + for _, info := range portListenerInfo { + // For Layer 7 protocols (HTTP, HTTPS, TLS) that support virtual hosting, + // group listeners by hostname and check for conflicts within each group. + // For Layer 4 protocols (TCP) that don't support virtual hosting, + // they conflict with all other listeners on the same port. + + // Group listeners by hostname for Layer 7 protocols + hostnameGroups := make(map[string][]*ListenerContext) + var layer4Listeners []*ListenerContext + + for _, listener := range info.listeners { + protocol := listener.Protocol + // TCP and UDP are Layer 4 protocols that don't support virtual hosting + if protocol == gwapiv1.TCPProtocolType || protocol == gwapiv1.UDPProtocolType { + if protocol == gwapiv1.TCPProtocolType { + layer4Listeners = append(layer4Listeners, listener) + } + // Skip UDP for conflict checking continue } - portListenerInfo[listener.Port] = append(portListenerInfo[listener.Port], listener) + + // Layer 7 protocols: group by hostname + var hostname string + if listener.Hostname != nil { + hostname = string(*listener.Hostname) + } + hostnameGroups[hostname] = append(hostnameGroups[hostname], listener) } - for _, listenersOnPort := range portListenerInfo { - nonUDPProtocols := sets.New[string]() + // Check for protocol conflicts within each hostname group + for _, listeners := range hostnameGroups { + protocols := sets.Set[string]{} nonListenerSetCount := 0 - for _, listener := range listenersOnPort { + for _, listener := range listeners { protocol := getProtocolForListener(listener) - if protocol == string(gwapiv1.UDPProtocolType) { - continue - } - nonUDPProtocols.Insert(protocol) + protocols.Insert(protocol) if !listener.isFromListenerSet() { nonListenerSetCount++ } } - // No protocol conflict when all non-UDP listeners are compatible. - if len(nonUDPProtocols) <= 1 { + // No conflict if all listeners in this hostname group use compatible protocols + if len(protocols) <= 1 { continue } - // If there are more than 1 non-UDP protocols and more than 1 listener not from ListenerSet, - // we cannot determine a clear winner and all listeners on this port are in conflict. + // Multiple Gateway listeners with incompatible protocols and same hostname: + // all listeners in this group are conflicted. if nonListenerSetCount > 1 { - // If any conflicted listener is not from ListenerSet, do not pick a winner. - for _, listener := range listenersOnPort { - if getProtocolForListener(listener) == string(gwapiv1.UDPProtocolType) { - continue - } - listener.SetCondition( - gwapiv1.ListenerConditionConflicted, - metav1.ConditionTrue, - gwapiv1.ListenerReasonProtocolConflict, - "All listeners for a given port must use a compatible protocol", - ) + for _, listener := range listeners { + setProtocolConflictConditions(listener) } continue } - // When nonListenerSetCount == 1, explicitly pick the Gateway-owned listener as winner. - // When nonListenerSetCount == 0, pick the first ListenerSet listener as winner. - // Note: UDP conflicts are handled by validateConflictedLayer4Listeners, so we skip - // UDP listeners here (this branch is only reached when len(nonUDPProtocols) > 1). + // One Gateway listener or all from ListenerSet: apply precedence rules var winnerProtocol string if nonListenerSetCount == 1 { - // Find and use the non-ListenerSet listener's protocol as the winner - for _, listener := range listenersOnPort { - protocol := getProtocolForListener(listener) - if !listener.isFromListenerSet() && protocol != string(gwapiv1.UDPProtocolType) { - winnerProtocol = protocol + // Gateway listener wins + for _, listener := range listeners { + if !listener.isFromListenerSet() { + winnerProtocol = getProtocolForListener(listener) break } } } - for _, listener := range listenersOnPort { + for _, listener := range listeners { protocol := getProtocolForListener(listener) - // Skip UDP listeners as they are handled by validateConflictedLayer4Listeners - if protocol == string(gwapiv1.UDPProtocolType) { + + // First ListenerSet listener wins if all from ListenerSet + if winnerProtocol == "" { + winnerProtocol = protocol continue } - // If we have an explicit winner protocol, use it; otherwise first one wins - if winnerProtocol != "" { - if protocol != winnerProtocol { - listener.SetCondition( - gwapiv1.ListenerConditionConflicted, - metav1.ConditionTrue, - gwapiv1.ListenerReasonProtocolConflict, - "All listeners for a given port must use a compatible protocol", - ) - } - } else { - // All conflicted listeners are from ListenerSet, first one wins - if winnerProtocol == "" { - winnerProtocol = protocol - } else if protocol != winnerProtocol { - listener.SetCondition( - gwapiv1.ListenerConditionConflicted, - metav1.ConditionTrue, - gwapiv1.ListenerReasonProtocolConflict, - "All listeners for a given port must use a compatible protocol", - ) - } + if protocol != winnerProtocol { + setProtocolConflictConditions(listener) } } } - } - for _, gateway := range gateways { - validateByPort(gateway.listeners) - } + // TCP listeners conflict with Layer 7 listeners on the same port + // because TCP doesn't support virtual hosting. + // Apply precedence rules: Gateway listeners win over ListenerSet listeners. + if len(layer4Listeners) > 0 && len(hostnameGroups) > 0 { + // Count Gateway listeners in each group + gatewayTCPCount := 0 + for _, listener := range layer4Listeners { + if !listener.isFromListenerSet() { + gatewayTCPCount++ + } + } - if t.MergeGateways { - allListeners := make([]*ListenerContext, 0) - for _, gateway := range gateways { - allListeners = append(allListeners, gateway.listeners...) + gatewayLayer7Count := 0 + for _, listeners := range hostnameGroups { + for _, listener := range listeners { + if !listener.isFromListenerSet() { + gatewayLayer7Count++ + break // Only count once per hostname group + } + } + } + + // Multiple Gateway listeners of mixed types (TCP and Layer 7): all conflicted + switch { + case gatewayTCPCount > 0 && gatewayLayer7Count > 0: + for _, listener := range layer4Listeners { + setProtocolConflictConditions(listener) + } + for _, listeners := range hostnameGroups { + for _, listener := range listeners { + setProtocolConflictConditions(listener) + } + } + case gatewayLayer7Count > 0: + // Gateway Layer 7 listeners win: mark all TCP (ListenerSet) listeners as conflicted + for _, listener := range layer4Listeners { + setProtocolConflictConditions(listener) + } + case gatewayTCPCount > 0: + // Gateway TCP listener wins: mark all Layer 7 (ListenerSet) listeners as conflicted + for _, listeners := range hostnameGroups { + for _, listener := range listeners { + setProtocolConflictConditions(listener) + } + } + default: + // All from ListenerSet: use deterministic first-listener precedence. + // Listeners that do not match the first listener's protocol are conflicted. + var winnerProtocol string + for _, listener := range info.listeners { + // UDP does not participate in protocol conflict checks. + if listener.Protocol == gwapiv1.UDPProtocolType { + continue + } + winnerProtocol = getProtocolForListener(listener) + break + } + if winnerProtocol == "" { + continue + } + for _, listener := range layer4Listeners { + if getProtocolForListener(listener) != winnerProtocol { + setProtocolConflictConditions(listener) + } + } + for _, listeners := range hostnameGroups { + for _, listener := range listeners { + if getProtocolForListener(listener) != winnerProtocol { + setProtocolConflictConditions(listener) + } + } + } + } + } else if len(layer4Listeners) > 1 { + // Multiple TCP listeners on same port: apply precedence rules + nonListenerSetCount := 0 + for _, listener := range layer4Listeners { + if !listener.isFromListenerSet() { + nonListenerSetCount++ + } + } + + switch { + case nonListenerSetCount > 1: + // Multiple Gateway TCP listeners: all conflicted + for _, listener := range layer4Listeners { + setProtocolConflictConditions(listener) + } + case nonListenerSetCount == 1: + // One Gateway TCP listener wins, mark ListenerSet TCP listeners as conflicted + for _, listener := range layer4Listeners { + if listener.isFromListenerSet() { + setProtocolConflictConditions(listener) + } + } + default: + // All from ListenerSet: first wins + for i, listener := range layer4Listeners { + if i > 0 { + setProtocolConflictConditions(listener) + } + } + } } - validateByPort(allListeners) } } @@ -958,23 +1108,13 @@ func (t *Translator) validateConflictedLayer7Listeners(gateways []*GatewayContex } if portListenerInfo[listener.Port] == nil { portListenerInfo[listener.Port] = &portListeners{ - protocols: sets.Set[string]{}, hostnames: map[string]int{}, + protocols: map[string]bool{}, } } portListenerInfo[listener.Port].listeners = append(portListenerInfo[listener.Port].listeners, listener) - var protocol string - switch listener.Protocol { - // HTTPS and TLS can co-exist on the same port - case gwapiv1.HTTPSProtocolType, gwapiv1.TLSProtocolType: - protocol = "https/tls" - default: - protocol = string(listener.Protocol) - } - portListenerInfo[listener.Port].protocols.Insert(protocol) - var hostname string if listener.Hostname != nil { hostname = string(*listener.Hostname) @@ -985,28 +1125,109 @@ func (t *Translator) validateConflictedLayer7Listeners(gateways []*GatewayContex // Set Conflicted conditions for any listeners with conflicting specs. for _, info := range portListenerInfo { + // Group listeners by hostname to determine conflicts + hostnameListeners := make(map[string][]*ListenerContext) for _, listener := range info.listeners { - if len(info.protocols) > 1 { - listener.SetCondition( - gwapiv1.ListenerConditionConflicted, - metav1.ConditionTrue, - gwapiv1.ListenerReasonProtocolConflict, - "All listeners for a given port must use a compatible protocol", - ) - } - var hostname string if listener.Hostname != nil { hostname = string(*listener.Hostname) } + hostnameListeners[hostname] = append(hostnameListeners[hostname], listener) + } - if info.hostnames[hostname] > 1 { - listener.SetCondition( - gwapiv1.ListenerConditionConflicted, - metav1.ConditionTrue, - gwapiv1.ListenerReasonHostnameConflict, - "All listeners for a given port must use a unique hostname", - ) + // Process each hostname group + for _, listeners := range hostnameListeners { + if len(listeners) <= 1 { + // No conflict if there's only one listener for this hostname + continue + } + + // Count Gateway listeners (non-ListenerSet) for this hostname + gatewayListenerCount := 0 + for _, listener := range listeners { + if !listener.isFromListenerSet() { + gatewayListenerCount++ + } + } + + // If there are multiple Gateway listeners with the same hostname on the same port, + // ALL Gateway listeners are conflicted (no "first wins" for plain Gateway conflicts). + // ListenerSet listeners are also conflicted (Gateway precedence applies). + switch { + case gatewayListenerCount > 1: + // Multiple Gateway listeners conflict: mark ALL listeners (Gateway + ListenerSet) as conflicted + for _, listener := range listeners { + listener.SetCondition( + gwapiv1.ListenerConditionConflicted, + metav1.ConditionTrue, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + // TODO: remove following once https://github.com/kubernetes-sigs/gateway-api/pull/4692 landed. + listener.SetCondition( + gwapiv1.ListenerConditionAccepted, + metav1.ConditionFalse, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + listener.SetCondition( + gwapiv1.ListenerConditionProgrammed, + metav1.ConditionFalse, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + } + case gatewayListenerCount == 1: + // One Gateway listener wins over all ListenerSet listeners (precedence handling). + // Mark only the ListenerSet listeners as conflicted. + for _, listener := range listeners { + if listener.isFromListenerSet() { + listener.SetCondition( + gwapiv1.ListenerConditionConflicted, + metav1.ConditionTrue, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + // TODO: remove following once https://github.com/kubernetes-sigs/gateway-api/pull/4692 landed. + listener.SetCondition( + gwapiv1.ListenerConditionAccepted, + metav1.ConditionFalse, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + listener.SetCondition( + gwapiv1.ListenerConditionProgrammed, + metav1.ConditionFalse, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + } + } + default: + // All listeners are from ListenerSet: first one wins, mark subsequent ones as conflicted + for i, listener := range listeners { + if i > 0 { + listener.SetCondition( + gwapiv1.ListenerConditionConflicted, + metav1.ConditionTrue, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + // TODO: remove following once https://github.com/kubernetes-sigs/gateway-api/pull/4692 landed. + listener.SetCondition( + gwapiv1.ListenerConditionAccepted, + metav1.ConditionFalse, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + listener.SetCondition( + gwapiv1.ListenerConditionProgrammed, + metav1.ConditionFalse, + gwapiv1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + } + } } } } diff --git a/internal/gatewayapi/validate_test.go b/internal/gatewayapi/validate_test.go new file mode 100644 index 0000000000..781ab2ae2b --- /dev/null +++ b/internal/gatewayapi/validate_test.go @@ -0,0 +1,105 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package gatewayapi + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestValidateConflictedProtocolsListenersIgnoresUnsupportedProtocols(t *testing.T) { + unsupported := gwapiv1.ProtocolType("INVALID") + + gateway := &gwapiv1.Gateway{} + gateway.Status.Listeners = []gwapiv1.ListenerStatus{{}, {}} + + gatewayCtx := &GatewayContext{Gateway: gateway} + gatewayCtx.listeners = []*ListenerContext{ + { + Listener: &gwapiv1.Listener{ + Name: "invalid", + Port: 80, + Protocol: unsupported, + }, + gateway: gatewayCtx, + listenerStatusIdx: 0, + }, + { + Listener: &gwapiv1.Listener{ + Name: "http", + Port: 80, + Protocol: gwapiv1.HTTPProtocolType, + }, + gateway: gatewayCtx, + listenerStatusIdx: 1, + }, + } + + translator := &Translator{} + translator.validateConflictedProtocolsListeners([]*GatewayContext{gatewayCtx}) + + httpConds := gatewayCtx.Status.Listeners[1].Conditions + require.False(t, hasListenerCondition(httpConds, gwapiv1.ListenerConditionConflicted, gwapiv1.ListenerReasonProtocolConflict, metav1.ConditionTrue)) + require.False(t, hasListenerCondition(httpConds, gwapiv1.ListenerConditionAccepted, gwapiv1.ListenerReasonProtocolConflict, metav1.ConditionFalse)) + require.False(t, hasListenerCondition(httpConds, gwapiv1.ListenerConditionProgrammed, gwapiv1.ListenerReasonProtocolConflict, metav1.ConditionFalse)) +} + +func TestValidateConflictedProtocolsListenersIgnoresInvalidListeners(t *testing.T) { + gateway := &gwapiv1.Gateway{} + gateway.Status.Listeners = []gwapiv1.ListenerStatus{{}, {}} + + gatewayCtx := &GatewayContext{Gateway: gateway} + gatewayCtx.listeners = []*ListenerContext{ + { + Listener: &gwapiv1.Listener{ + Name: "invalid-tcp", + Port: 80, + Protocol: gwapiv1.TCPProtocolType, + }, + gateway: gatewayCtx, + listenerStatusIdx: 0, + }, + { + Listener: &gwapiv1.Listener{ + Name: "http", + Port: 80, + Protocol: gwapiv1.HTTPProtocolType, + }, + gateway: gatewayCtx, + listenerStatusIdx: 1, + }, + } + + // Mark the first listener invalid. It should be ignored in protocol conflict checks. + gatewayCtx.listeners[0].SetCondition( + gwapiv1.ListenerConditionProgrammed, + metav1.ConditionFalse, + gwapiv1.ListenerReasonInvalid, + "listener is invalid", + ) + + translator := &Translator{} + translator.validateConflictedProtocolsListeners([]*GatewayContext{gatewayCtx}) + + httpConds := gatewayCtx.Status.Listeners[1].Conditions + require.False(t, hasListenerCondition(httpConds, gwapiv1.ListenerConditionConflicted, gwapiv1.ListenerReasonProtocolConflict, metav1.ConditionTrue)) + require.False(t, hasListenerCondition(httpConds, gwapiv1.ListenerConditionAccepted, gwapiv1.ListenerReasonProtocolConflict, metav1.ConditionFalse)) + require.False(t, hasListenerCondition(httpConds, gwapiv1.ListenerConditionProgrammed, gwapiv1.ListenerReasonProtocolConflict, metav1.ConditionFalse)) +} + +// nolint: unparam +func hasListenerCondition(conditions []metav1.Condition, condType gwapiv1.ListenerConditionType, reason gwapiv1.ListenerConditionReason, status metav1.ConditionStatus) bool { + for _, cond := range conditions { + if cond.Type == string(condType) && cond.Reason == string(reason) && cond.Status == status { + return true + } + } + + return false +} diff --git a/test/conformance/suite.go b/test/conformance/suite.go index fcef57bfb8..b98d896bf1 100644 --- a/test/conformance/suite.go +++ b/test/conformance/suite.go @@ -45,12 +45,6 @@ func conformanceOpts(t *testing.T) suite.ConformanceOptions { // SkipTests is a list of tests that are skipped in the conformance suite. func SkipTests(gatewayNamespaceMode bool) []suite.ConformanceTest { skipTests := make([]suite.ConformanceTest, 0, 4) - skipTests = append(skipTests, - // TODO: fix following conformance tests - tests.ListenerSetHostnameConflict, - tests.ListenerSetProtocolConflict, - ) - if gatewayNamespaceMode { return skipTests }