diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 1d24c6abcd..718a8e34d6 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -2359,6 +2359,23 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M } } + if err := c.Watch( + source.Kind(mgr.GetCache(), &corev1.Namespace{}, + handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, ns *corev1.Namespace) []reconcile.Request { + // Gateway listener restricts route attachment with allowedRoutes.namespaces.from: Selector + // changing a namespace's labels after an HTTPRoute in it has been evaluated should trigger re-evaluation. + // It's hard to determine which Gateway/GatewayClass(es) are affected by a namespace label change, + // so we enqueue all GatewayClasses for reconciliation. + // In the worst case, changes unrelated namespace labels will trigger unnecessary reconciliations, but this is a rare event. + + if !r.hasSelectorAllowedRoutesGateway(ctx) { + return nil + } + return r.enqueueClass(ctx, ns) + }))); err != nil { + return fmt.Errorf("failed to watch Namespace: %w", err) + } + // Watch HTTPRoute CRUDs and process affected Gateways. httprPredicates := commonPredicates[*gwapiv1.HTTPRoute]() if r.namespaceLabel != nil { diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go index d05fe9d7be..450a05c485 100644 --- a/internal/provider/kubernetes/predicates.go +++ b/internal/provider/kubernetes/predicates.go @@ -19,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -82,6 +83,52 @@ func isNamespaceSelectorBypassInfrastructureResource(obj any) bool { } } +func (r *gatewayAPIReconciler) hasSelectorAllowedRoutesGateway(ctx context.Context) bool { + gtwList := &gwapiv1.GatewayList{} + if err := r.client.List(ctx, gtwList, &client.ListOptions{}); err != nil { + // If we can't list Gateways, we can't determine if + // any of them have SelectorAllowedRoutes set to true, so we return true. + return true + } + + for i := range gtwList.Items { + gtw := >wList.Items[i] + for _, l := range gtw.Spec.Listeners { + if hasNamespacesFromSelector(l.AllowedRoutes) { + return true + } + } + } + + if !r.listenerSetCRDExists { + return false + } + + listenerSetList := &gwapiv1.ListenerSetList{} + if err := r.client.List(ctx, gtwList, &client.ListOptions{}); err != nil { + // If we can't list ListenerSet, we can't determine if + // any of them have SelectorAllowedRoutes set to true, so we return true. + return true + } + for i := range listenerSetList.Items { + ls := &listenerSetList.Items[i] + for _, l := range ls.Spec.Listeners { + if hasNamespacesFromSelector(l.AllowedRoutes) { + return true + } + } + } + + return false +} + +func hasNamespacesFromSelector(ar *gwapiv1.AllowedRoutes) bool { + if ar == nil || ar.Namespaces == nil { + return false + } + return ptr.Deref(ar.Namespaces.From, gwapiv1.NamespacesFromSame) == gwapiv1.NamespacesFromSelector +} + type NamespaceGetter interface { GetNamespace() string }