Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions internal/gatewayapi/contexts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package gatewayapi

import (
"math"

certificatesv1b1 "k8s.io/api/certificates/v1beta1"
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
Expand Down Expand Up @@ -114,12 +116,17 @@ func (g *GatewayContext) attachEnvoyProxy(resources *resource.Resources, epMap m
}
}

func (g *GatewayContext) IncreaseAttachedListenerSets() {
func (g *GatewayContext) IncreaseAttachedListenerSets(count uint32) {
if g.Status.AttachedListenerSets == nil {
g.Status.AttachedListenerSets = ptr.To[int32](1)
} else {
*g.Status.AttachedListenerSets++
if count > 0 {
g.Status.AttachedListenerSets = new(int32(min(int64(count), math.MaxInt32)))
}
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we return here, arent we avoid an increment ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

if we reach here, that means new count is 0 and AttachedListenerSets is nil, so it's safe to skip.

}

// Check for potential overflow before adding
newValue := int64(*g.Status.AttachedListenerSets) + int64(count)
*g.Status.AttachedListenerSets = int32(min(newValue, math.MaxInt32))
}

// ListenerContext wraps a Listener and provides helper methods for
Expand All @@ -138,6 +145,10 @@ type ListenerContext struct {

namespaceSelector labels.Selector

// specValid indicates whether per-listener spec validation succeeded.
// Conflict detection should only consider listeners with specValid=true.
specValid bool

tls ListenerTLSConfig

httpIR *ir.HTTPListener
Expand Down
16 changes: 8 additions & 8 deletions internal/gatewayapi/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,24 +423,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
}
}

Expand Down
206 changes: 158 additions & 48 deletions internal/gatewayapi/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/google/cel-go/cel"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -214,19 +215,118 @@ func validClientCertificateRef(ref *gwapiv1.SecretObjectReference) error {
return nil
}

// allowedRouteKindsForProtocol returns the route kinds supported by the given listener protocol.
func allowedRouteKindsForProtocol(protocol gwapiv1.ProtocolType, tlsMode *gwapiv1.TLSModeType) []gwapiv1.Kind {
switch protocol {
case gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType:
return []gwapiv1.Kind{resource.KindHTTPRoute, resource.KindGRPCRoute}
case gwapiv1.TLSProtocolType:
if tlsMode != nil && *tlsMode == gwapiv1.TLSModePassthrough {
return []gwapiv1.Kind{resource.KindTLSRoute}
}
// Terminate mode or unspecified defaults to accept both TCP and TLS routes
return []gwapiv1.Kind{resource.KindTCPRoute, resource.KindTLSRoute}
case gwapiv1.TCPProtocolType:
return []gwapiv1.Kind{resource.KindTCPRoute}
case gwapiv1.UDPProtocolType:
return []gwapiv1.Kind{resource.KindUDPRoute}
default:
return nil
}
}

// validateProtocolRules validates protocol-specific constraints (hostname, TLS requirements).
// Returns true if all constraints are satisfied, false otherwise.
func validateProtocolRules(listener *ListenerContext) bool {
switch listener.Protocol {
case gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType, gwapiv1.TLSProtocolType,
gwapiv1.TCPProtocolType, gwapiv1.UDPProtocolType:
// All supported protocols pass basic validation here.
// Protocol-specific constraints (TLS, hostname) are validated in
// validateTLSConfiguration and validateHostName respectively,
// where they can set proper conditions.
return true

default:
// Unsupported protocol handled separately
return false
}
}

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
specValid := t.validateAllowedNamespaces(listener)
if !validateProtocolRules(listener) {
specValid = false
}

// Phase 2: Validate allowed routes based on protocol
if listener.Protocol == gwapiv1.HTTPProtocolType ||
listener.Protocol == gwapiv1.HTTPSProtocolType ||
listener.Protocol == gwapiv1.TCPProtocolType ||
listener.Protocol == gwapiv1.UDPProtocolType ||
listener.Protocol == gwapiv1.TLSProtocolType {
var tlsMode *gwapiv1.TLSModeType
if listener.TLS != nil {
tlsMode = listener.TLS.Mode
}
allowedKinds := allowedRouteKindsForProtocol(listener.Protocol, tlsMode)
if !t.validateAllowedRoutes(listener, allowedKinds...) {
specValid = false
}
} else {
Comment on lines +261 to +280

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this be reworked or simplified? The else block effectively covers the !validateProtocolRules case if I'm reading correctly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

that's why I want merge #8577 first, will make this one cleaner.

// Unsupported protocol
specValid = false
listener.SetSupportedKinds()
listener.SetCondition(
gwapiv1.ListenerConditionAccepted,
metav1.ConditionFalse,
gwapiv1.ListenerReasonUnsupportedProtocol,
fmt.Sprintf("Protocol %s is unsupported, must be %s, %s, %s or %s.", listener.Protocol,
gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType, gwapiv1.TCPProtocolType, gwapiv1.UDPProtocolType),
)
}

// Phase 3: Validate TLS configuration details
if !t.validateTLSConfiguration(listener, resources) {
specValid = false
}

// Phase 4: Validate Hostname configuration
if !t.validateHostName(listener) {
specValid = false
}

return specValid
}

func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource.XdsIRMap, infraIR resource.InfraIRMap, resources *resource.Resources) {
// Infra IR proxy ports must be unique.
foundPorts := make(map[string][]*protocolPort)

// Phase 1: Validate each listener's spec independently.
// This must happen before conflict resolution so that invalid listeners
// don't block valid ones during conflict detection.
for _, gateway := range gateways {
for _, listener := range gateway.listeners {
listener.specValid = t.validateListenerSpec(listener, resources)
}
}

// Phase 2: Run conflict detection.
// Only listeners that haven't been marked as invalid will participate in conflict resolution.
t.validateConflictedProtocolsListeners(gateways)
t.validateConflictedLayer7Listeners(gateways)
t.validateConflictedLayer4Listeners(gateways, gwapiv1.TCPProtocolType)
t.validateConflictedLayer4Listeners(gateways, gwapiv1.UDPProtocolType)
if t.MergeGateways {
t.validateConflictedMergedListeners(gateways)
}

// Iterate through all listeners to validate spec
// and compute status for each, and add valid ones
// to the Xds IR.
// Phase 3: Build IR for valid listeners.
for _, gateway := range gateways {
irKey := t.getIRKey(gateway.Gateway)

Expand All @@ -235,57 +335,27 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource
}
t.processProxyReadyListener(xdsIR[irKey], gateway.envoyProxy)
t.processProxyObservability(gateway, xdsIR[irKey], infraIR[irKey].Proxy, resources)

gatewayAttachedListenerSets := sets.New[string]()
for _, listener := range gateway.listeners {
// Process protocol & supported kinds
switch listener.Protocol {
case gwapiv1.TLSProtocolType:
if listener.TLS != nil {
switch *listener.TLS.Mode {
case gwapiv1.TLSModePassthrough:
t.validateAllowedRoutes(listener, resource.KindTLSRoute)
case gwapiv1.TLSModeTerminate:
t.validateAllowedRoutes(listener, resource.KindTCPRoute, resource.KindTLSRoute)
default:
t.validateAllowedRoutes(listener, resource.KindTCPRoute, resource.KindTLSRoute)
}
} else {
t.validateAllowedRoutes(listener, resource.KindTCPRoute, resource.KindTLSRoute)
}
case gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType:
t.validateAllowedRoutes(listener, resource.KindHTTPRoute, resource.KindGRPCRoute)
case gwapiv1.TCPProtocolType:
t.validateAllowedRoutes(listener, resource.KindTCPRoute)
case gwapiv1.UDPProtocolType:
t.validateAllowedRoutes(listener, resource.KindUDPRoute)
default:
listener.SetSupportedKinds()
listener.SetCondition(
gwapiv1.ListenerConditionAccepted,
metav1.ConditionFalse,
gwapiv1.ListenerReasonUnsupportedProtocol,
fmt.Sprintf("Protocol %s is unsupported, must be %s, %s, %s or %s.", listener.Protocol,
gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType, gwapiv1.TCPProtocolType, gwapiv1.UDPProtocolType),
)
}

// Validate allowed namespaces
t.validateAllowedNamespaces(listener)

// Process TLS configuration
t.validateTLSConfiguration(listener, resources)

// Process Hostname configuration
t.validateHostName(listener)

// Process conditions and check if the listener is ready
// Finalize listener conditions and check readiness.
t.validateListenerConditions(listener)
if !listener.IsReady() {
// Skip invalid listeners from IR building
continue
}

// Skip listeners with invalid frontend TLS validation as they are not functional.
if listener.frontendTLSValidationInvalid() {
continue
}

if listener.isFromListenerSet() {
gatewayAttachedListenerSets.Insert(types.NamespacedName{
Namespace: listener.listenerSet.Namespace,
Name: listener.listenerSet.Name,
}.String())
}

address := netutils.IPv4ListenerAddress
ipFamily := getEnvoyIPFamily(gateway.envoyProxy)
if ipFamily != nil && (*ipFamily == egv1a1.IPv6 || *ipFamily == egv1a1.DualStack) {
Expand Down Expand Up @@ -365,11 +435,35 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource
foundPorts[irKey] = append(foundPorts[irKey], servicePort)
}
}
gateway.IncreaseAttachedListenerSets(uint32(len(gatewayAttachedListenerSets)))
}

t.checkOverlappingTLSConfig(gateways)
}

// isListenerReady returns true if the listener is ready (Accepted=True and Programmed=True or no conditions yet).
// A listener is not ready if it has Accepted=False or Programmed=False.
func isListenerReady(listener *ListenerContext) bool {
conditions := listener.GetConditions()

// No conditions yet means it will be set to ready during validation.
if len(conditions) == 0 {
return true
}

// Check if Accepted=False or Programmed=False exists.
for _, cond := range conditions {
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 true
}

// checkOverlappingTLSConfig checks for overlapping hostnames and certificates between listeners and sets
// the `OverlappingTLSConfig` condition if there are overlapping hostnames or certificates.
func (t *Translator) checkOverlappingTLSConfig(gateways []*GatewayContext) {
Expand All @@ -378,7 +472,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 && isListenerReady(listener) {
httpsListeners = append(httpsListeners, listener)
}
}
Expand All @@ -393,7 +487,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 && isListenerReady(listener) {
httpsListeners = append(httpsListeners, listener)
}
}
Expand Down Expand Up @@ -422,10 +516,18 @@ func checkOverlappingHostnames(httpsListeners []*ListenerContext) {
if overlappingListeners[i] != nil {
continue
}
// Skip listeners that are already marked as invalid from per-listener validation
if hasInvalidCondition(httpsListeners[i]) {
continue
}
for j := i + 1; j < len(httpsListeners); j++ {
if overlappingListeners[j] != nil {
continue
}
// Skip listeners that are already marked as invalid from per-listener validation
if hasInvalidCondition(httpsListeners[j]) {
continue
}
if httpsListeners[i].Port != httpsListeners[j].Port {
continue
}
Expand Down Expand Up @@ -504,10 +606,18 @@ func checkOverlappingCertificates(httpsListeners []*ListenerContext) {
if overlappingListeners[i] != nil {
continue
}
// Skip listeners that are already marked as invalid from per-listener validation
if hasInvalidCondition(httpsListeners[i]) {
continue
}
for j := i + 1; j < len(httpsListeners); j++ {
if overlappingListeners[j] != nil {
continue
}
// Skip listeners that are already marked as invalid from per-listener validation
if hasInvalidCondition(httpsListeners[j]) {
continue
}
if httpsListeners[i].Port != httpsListeners[j].Port {
continue
}
Expand Down
1 change: 0 additions & 1 deletion internal/gatewayapi/listenerset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -2282,7 +2282,7 @@ func (t *Translator) processAllowedListenersForParentRefs(
}
parentRefCtx.SetListeners(allowedListeners...)

if !HasReadyListener(selectedListeners) {
if !HasReadyListener(allowedListeners) {
routeStatus := GetRouteStatus(routeContext)
status.SetRouteStatusCondition(routeStatus,
parentRefCtx.routeParentStatusIdx,
Expand Down
Loading