From 7f7db79dc821f212cc405b77fa7a4217fbf66ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Wed, 22 May 2024 16:59:06 +0200 Subject: [PATCH] feat: fill in AttachedRoutes in Gateways' Listeners status (#251) * feat: fill in AttachedRoutes in Gateways' Listeners status * refactor: use metav1.LabelSelectorAsSelector for converting label selector to selector Co-authored-by: Jakub Warczarek --------- Co-authored-by: Jakub Warczarek --- CHANGELOG.md | 2 + .../gateway-httproute-allowedroutes.yaml | 141 +++++ controller/gateway/controller.go | 23 +- .../gateway/controller_reconciler_utils.go | 196 ++++++- .../controller_reconciler_utils_test.go | 555 +++++++++++++++--- controller/gateway/controller_watch.go | 83 +++ controller/pkg/patch/patch.go | 5 +- internal/types/gatewaytypes.go | 21 + pkg/utils/gateway/ownerrefs.go | 40 ++ test/conformance/conformance_test.go | 1 + 10 files changed, 963 insertions(+), 104 deletions(-) create mode 100644 config/samples/gateway-httproute-allowedroutes.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 894f1c2bc..995ea9e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ - `Gateway` do not have their `Ready` status condition set anymore. This aligns with Gateway API and its conformance test suite. [#246](https://github.com/Kong/gateway-operator/pull/246) +- `Gateway`s' listeners now have their `attachedRoutes` count filled in in status. + [#251](https://github.com/Kong/gateway-operator/pull/251) ### Fixes diff --git a/config/samples/gateway-httproute-allowedroutes.yaml b/config/samples/gateway-httproute-allowedroutes.yaml new file mode 100644 index 000000000..618804069 --- /dev/null +++ b/config/samples/gateway-httproute-allowedroutes.yaml @@ -0,0 +1,141 @@ +apiVersion: v1 +kind: Service +metadata: + name: echo +spec: + ports: + - protocol: TCP + name: http + port: 80 + targetPort: http + selector: + app: echo +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: echo + name: echo +spec: + replicas: 1 + selector: + matchLabels: + app: echo + template: + metadata: + labels: + app: echo + spec: + containers: + - name: echo + image: registry.k8s.io/e2e-test-images/agnhost:2.40 + command: + - /agnhost + - netexec + - --http-port=8080 + ports: + - containerPort: 8080 + name: http + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + resources: + requests: + cpu: 10m +--- +kind: GatewayConfiguration +apiVersion: gateway-operator.konghq.com/v1beta1 +metadata: + name: kong + namespace: default +spec: + dataPlaneOptions: + deployment: + podTemplateSpec: + spec: + containers: + - name: proxy + # renovate: datasource=docker versioning=docker + image: kong/kong-gateway:3.6 + readinessProbe: + initialDelaySeconds: 1 + periodSeconds: 1 + controlPlaneOptions: + deployment: + podTemplateSpec: + spec: + containers: + - name: controller + # renovate: datasource=docker versioning=docker + image: kong/kubernetes-ingress-controller:3.1.5 + readinessProbe: + initialDelaySeconds: 1 + periodSeconds: 1 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: kong +spec: + controllerName: konghq.com/gateway-operator + parametersRef: + group: gateway-operator.konghq.com + kind: GatewayConfiguration + name: kong + namespace: default +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: kong + namespace: default +spec: + gatewayClassName: kong + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + kinds: + - kind: HTTPRoute + namespaces: + from: Selector + selector: + matchLabels: + # This label is added automatically as of K8s 1.22 to all namespaces + kubernetes.io/metadata.name: default +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httproute-echo + namespace: default + annotations: + konghq.com/strip-path: "true" +spec: + parentRefs: + - name: kong + rules: + - matches: + - path: + type: PathPrefix + value: /echo + backendRefs: + - name: echo + kind: Service + port: 80 diff --git a/controller/gateway/controller.go b/controller/gateway/controller.go index 267f99d99..e5a7f3eb3 100644 --- a/controller/gateway/controller.go +++ b/controller/gateway/controller.go @@ -88,8 +88,17 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Watches( &gatewayv1beta1.ReferenceGrant{}, handler.EnqueueRequestsFromMapFunc(r.listReferenceGrantsForGateway), - builder.WithPredicates(predicate.NewPredicateFuncs(referenceGrantHasGatewayFrom)), - ). + builder.WithPredicates(predicate.NewPredicateFuncs(referenceGrantHasGatewayFrom))). + // watch HTTPRoutes so that Gateway listener status can be updated. + Watches( + &gatewayv1beta1.HTTPRoute{}, + handler.EnqueueRequestsFromMapFunc(r.listGatewaysAttachedByHTTPRoute)). + // watch Namespaces so that managed routes have correct status reflected in Gateway's + // status in status.listeners.attachedRoutes + // This is required to properly support Gateway's listeners.allowedRoutes.namespaces.selector. + Watches( + &corev1.Namespace{}, + handler.EnqueueRequestsFromMapFunc(r.listManagedGatewaysInNamespace)). Complete(r) } @@ -152,7 +161,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu log.Trace(logger, "resource is supported, ensuring that it gets marked as accepted", gateway) gwConditionAware.initListenersStatus() gwConditionAware.setConflicted() - gwConditionAware.setAccepted() + if err = gwConditionAware.setAcceptedAndAttachedRoutes(ctx, r.Client); err != nil { + return ctrl.Result{}, err + } + gwConditionAware.initProgrammedAndListenersStatus() if err := gwConditionAware.setResolvedRefsAndSupportedKinds(ctx, r.Client); err != nil { return ctrl.Result{}, err @@ -160,7 +172,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu acceptedCondition, _ := k8sutils.GetCondition(k8sutils.ConditionType(gatewayv1.GatewayConditionAccepted), gwConditionAware) // If the static Gateway API conditions (Accepted, ResolvedRefs, Conflicted) changed, we need to update the Gateway status if gatewayStatusNeedsUpdate(oldGwConditionsAware, gwConditionAware) { - if err := r.patchStatus(ctx, &gateway, oldGateway); err != nil { // requeue will be triggered by the update of the dataplane status + // Requeue will be triggered by the update of the gateway status. + if _, err := patch.ApplyGatewayStatusPatchIfNotEmpty(ctx, r.Client, logger, &gateway, oldGateway); err != nil { return ctrl.Result{}, err } if acceptedCondition.Status == metav1.ConditionTrue { @@ -346,7 +359,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu gatewayConditionsAndListenersAware(&gateway)) } - gwConditionAware.setProgrammedAndListenersConditions() + gwConditionAware.setProgrammed() res, err := patch.ApplyGatewayStatusPatchIfNotEmpty(ctx, r.Client, logger, &gateway, oldGateway) if err != nil { return ctrl.Result{}, err diff --git a/controller/gateway/controller_reconciler_utils.go b/controller/gateway/controller_reconciler_utils.go index 31d3a933a..193aa351f 100644 --- a/controller/gateway/controller_reconciler_utils.go +++ b/controller/gateway/controller_reconciler_utils.go @@ -16,6 +16,7 @@ import ( networkingv1 "k8s.io/api/networking/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -544,19 +545,22 @@ func (g *gatewayConditionsAndListenersAwareT) initProgrammedAndListenersStatus() k8sutils.InitProgrammed(g) for i := range g.Spec.Listeners { lStatus := listenerConditionsAware(&g.Status.Listeners[i]) - k8sutils.SetCondition(metav1.Condition{ - Type: string(gatewayv1.ListenerConditionProgrammed), - Status: metav1.ConditionFalse, - Reason: string(gatewayv1.ListenerReasonPending), - ObservedGeneration: g.Generation, - LastTransitionTime: metav1.Now(), - }, lStatus) + _, ok := k8sutils.GetCondition(k8sutils.ConditionType(gatewayv1.ListenerConditionProgrammed), lStatus) + if !ok { + k8sutils.SetCondition(metav1.Condition{ + Type: string(gatewayv1.ListenerConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: string(gatewayv1.ListenerReasonPending), + ObservedGeneration: g.Generation, + LastTransitionTime: metav1.Now(), + }, lStatus) + } } } func (g *gatewayConditionsAndListenersAwareT) setResolvedRefsAndSupportedKinds(ctx context.Context, c client.Client) error { for i, listener := range g.Spec.Listeners { - supportedKinds, resolvedRefsCondition, err := getSupportedKindsWithResolvedRefsCondition(ctx, c, g.Namespace, g.Generation, listener) + supportedKinds, resolvedRefsCondition, err := getSupportedKindsWithResolvedRefsCondition(ctx, c, g.GetNamespace(), g.Generation, listener) if err != nil { return err } @@ -580,8 +584,9 @@ func (g *gatewayConditionsAndListenersAwareT) initListenersStatus() { g.Status.Listeners = listenersStatus } -// setAccepted sets the listeners and gateway Accepted condition according to the Gateway API specification. -func (g *gatewayConditionsAndListenersAwareT) setAccepted() { +// setAcceptedAndAttachedRoutes sets the listeners and gateway Accepted condition according to the Gateway API specification. +// It also sets the AttachedRoutes field in the listener status. +func (g *gatewayConditionsAndListenersAwareT) setAcceptedAndAttachedRoutes(ctx context.Context, c client.Client) error { for i, listener := range g.Spec.Listeners { acceptedCondition := metav1.Condition{ Type: string(gatewayv1.ListenerConditionAccepted), @@ -590,14 +595,138 @@ func (g *gatewayConditionsAndListenersAwareT) setAccepted() { LastTransitionTime: metav1.Now(), ObservedGeneration: g.Generation, } - if listener.Protocol != gatewayv1.HTTPProtocolType && listener.Protocol != gatewayv1.HTTPSProtocolType { + + if _, protocolSupported := supportedRoutesByProtocol()[listener.Protocol]; !protocolSupported { acceptedCondition.Status = metav1.ConditionFalse acceptedCondition.Reason = string(gatewayv1.ListenerReasonUnsupportedProtocol) } listenerConditionsAware := listenerConditionsAware(&g.Status.Listeners[i]) listenerConditionsAware.SetConditions(append(listenerConditionsAware.Conditions, acceptedCondition)) + + // AttachedRoutes + count, err := countAttachedRoutesForGatewayListener(ctx, g.Gateway, g.Gateway.Spec.Listeners[i], c) + if err != nil { + return fmt.Errorf("failed to count attached routes for Gateway %s: %w", client.ObjectKeyFromObject(g), err) + } + + g.Status.Listeners[i].AttachedRoutes = count } + k8sutils.SetAcceptedConditionOnGateway(g) + return nil +} + +// countAttachedRoutesForGatewayListener counts the number of attached routes for a given listener. +// It takes into account the AllowedRoutes field in the listener spec and route's ParentRefs. +// It returns the number of attached routes and an error. +func countAttachedRoutesForGatewayListener(ctx context.Context, g *gwtypes.Gateway, listener gwtypes.Listener, cl client.Client) (int32, error) { + allowedRoutes := listener.AllowedRoutes + // Gateway API defines a default value for AllowedRoutes, so if this is nil there's something wrong. + if allowedRoutes == nil { + return 0, fmt.Errorf("AllowedRoutes is nil for listener %s in gateway %s", + listener.Name, client.ObjectKeyFromObject(g), + ) + } + + var ( + count int32 + opts []client.ListOption + ) + + namespaces := allowedRoutes.Namespaces + // Gateway API defines a default value for AllowedRoutes.Namespaces, so + // if this is nil there's something wrong. + if namespaces == nil || namespaces.From == nil { + return 0, fmt.Errorf("AllowedRoutes.Namespaces is nil for listener %s in gateway %s", + listener.Name, client.ObjectKeyFromObject(g), + ) + } + + switch *namespaces.From { + case gatewayv1.NamespacesFromAll: + case gatewayv1.NamespacesFromSame: + opts = append(opts, client.InNamespace(g.Namespace)) + case gatewayv1.NamespacesFromSelector: + var nsList corev1.NamespaceList + + s, err := metav1.LabelSelectorAsSelector(listener.AllowedRoutes.Namespaces.Selector) + if err != nil { + return 0, fmt.Errorf("failed to create requirement for namespace selector (for Gateway %s): %w", + client.ObjectKeyFromObject(g), err, + ) + } + reqs, selectable := s.Requirements() + if !selectable { + return 0, fmt.Errorf("namespace selector is not selectable (for Gateway %s)", client.ObjectKeyFromObject(g)) + } + labelSelector := labels.NewSelector() + for _, req := range reqs { + labelSelector = labelSelector.Add(req) + } + if err := cl.List(ctx, &nsList, &client.ListOptions{ + LabelSelector: labelSelector, + }); err != nil { + if k8serrors.IsNotFound(err) { + return 0, nil + } + return 0, fmt.Errorf("failed to list namespaces for gateway %s: %w", g.Name, err) + } + + switch len(nsList.Items) { + case 0: + // If no namespaces matching the selector are found, set the AttachedRoutes to 0 as + // there are no routes to attach. + return 0, nil + + default: + for _, ns := range nsList.Items { + opts = append(opts, client.InNamespace(ns.Name)) + } + } + } + + kindsForProtocol, protocolSupported := supportedRoutesByProtocol()[listener.Protocol] + switch len(allowedRoutes.Kinds) { + case 0: + if protocolSupported { + for k := range kindsForProtocol { + // NOTE: Count other types of routes when they are supported. + + switch k { + case "HTTPRoute": + httpRoutes, err := gatewayutils.ListHTTPRoutesForGateway(ctx, cl, g, opts...) + if err != nil { + return 0, fmt.Errorf( + "failed to list HTTPRoutes for Gateway %s when counting AttachedRoutes: %w", + client.ObjectKeyFromObject(g), err, + ) + } + count += int32(len(httpRoutes)) + default: + return 0, fmt.Errorf("unsupported route kind: %T", k) + } + } + } + default: + if lo.ContainsBy(allowedRoutes.Kinds, func(gvk gatewayv1.RouteGroupKind) bool { + if _, ok := kindsForProtocol[gvk.Kind]; !ok { + return false + } + return gvk.Group != nil && *gvk.Group == gatewayv1.Group(gatewayv1.GroupVersion.Group) + }) { + httpRoutes, err := gatewayutils.ListHTTPRoutesForGateway(ctx, cl, g, opts...) + if err != nil { + return 0, fmt.Errorf( + "failed to list HTTPRoutes for Gateway %s when counting AttachedRoutes: %w", + client.ObjectKeyFromObject(g), err, + ) + } + + count += int32(len(httpRoutes)) + } + } + + return count, nil } // setConflicted sets the gateway Conflicted condition according to the Gateway API specification. @@ -634,14 +763,15 @@ func (g *gatewayConditionsAndListenersAwareT) setConflicted() { } } -// setProgrammedAndListenersConditions sets the gateway Programmed condition by setting the underlying +// setProgrammed sets the gateway Programmed condition by setting the underlying // Gateway Programmed status to true. // It also sets the listeners Programmed condition by setting the underlying // Listener Programmed status to true. -func (g *gatewayConditionsAndListenersAwareT) setProgrammedAndListenersConditions() { +func (g *gatewayConditionsAndListenersAwareT) setProgrammed() { k8sutils.SetProgrammed(g) - for i := range g.Spec.Listeners { + for i := range g.Status.Listeners { + listener := &g.Status.Listeners[i] programmedCondition := metav1.Condition{ Type: string(gatewayv1.ListenerConditionProgrammed), Status: metav1.ConditionTrue, @@ -649,7 +779,7 @@ func (g *gatewayConditionsAndListenersAwareT) setProgrammedAndListenersCondition ObservedGeneration: g.GetGeneration(), LastTransitionTime: metav1.Now(), } - listenerStatus := listenerConditionsAware(&g.Status.Listeners[i]) + listenerStatus := listenerConditionsAware(listener) k8sutils.SetCondition(programmedCondition, listenerStatus) } } @@ -777,24 +907,24 @@ func getSupportedKindsWithResolvedRefsCondition(ctx context.Context, c client.Cl if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { supportedRoutes := supportedRoutesByProtocol()[listener.Protocol] - for route := range supportedRoutes { + for routeKind := range supportedRoutes { supportedKinds = append(supportedKinds, gatewayv1.RouteGroupKind{ Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), - Kind: route, + Kind: routeKind, }) } } else { - for _, k := range listener.AllowedRoutes.Kinds { + for _, routeGK := range listener.AllowedRoutes.Kinds { validRoutes := supportedRoutesByProtocol()[listener.Protocol] - if _, ok := validRoutes[k.Kind]; !ok || k.Group == nil || *k.Group != gatewayv1.Group(gatewayv1.GroupVersion.Group) { + if _, ok := validRoutes[routeGK.Kind]; !ok || routeGK.Group == nil || *routeGK.Group != gatewayv1.Group(gatewayv1.GroupVersion.Group) { resolvedRefsCondition.Reason = string(gatewayv1.ListenerReasonInvalidRouteKinds) - message = conditionMessage(message, fmt.Sprintf("Route %s not supported", string(k.Kind))) + message = conditionMessage(message, fmt.Sprintf("Route %s not supported", string(routeGK.Kind))) continue } supportedKinds = append(supportedKinds, gatewayv1.RouteGroupKind{ - Group: k.Group, - Kind: k.Kind, + Group: routeGK.Group, + Kind: routeGK.Kind, }) } } @@ -925,23 +1055,27 @@ func gatewayStatusNeedsUpdate(oldGateway, newGateway gatewayConditionsAndListene return true } - for i, newlistener := range newGateway.GetListenersConditions() { + for i, newListener := range newGateway.GetListenersConditions() { oldListener := oldGateway.Status.Listeners[i] - if len(newlistener.Conditions) != len(oldListener.Conditions) { + if newListener.AttachedRoutes != oldListener.AttachedRoutes { + return true + } + if len(newListener.Conditions) != len(oldListener.Conditions) { return true } - if !cmp.Equal(newlistener.SupportedKinds, oldListener.SupportedKinds) { + if !cmp.Equal(newListener.SupportedKinds, oldListener.SupportedKinds) { return true } - for j, newListenerCond := range newlistener.Conditions { - // Do not consider the programmed condition, as it depends on the DataPlane and ControlPlane status. - if newListenerCond.Type != string(gatewayv1.ListenerConditionProgrammed) { - if !areConditionsEqual(oldListener.Conditions[j], newListenerCond) { + for j, newListenerCond := range newListener.Conditions { + switch newListenerCond.Type { + case string(gatewayv1.ListenerConditionProgrammed): + // Do not consider the programmed condition, as it depends on the DataPlane and ControlPlane status. + if oldListener.Conditions[j].Type != string(gatewayv1.ListenerConditionProgrammed) { return true } - } else { - if oldListener.Conditions[j].Type != string(gatewayv1.ListenerConditionProgrammed) { + default: + if !areConditionsEqual(oldListener.Conditions[j], newListenerCond) { return true } } diff --git a/controller/gateway/controller_reconciler_utils_test.go b/controller/gateway/controller_reconciler_utils_test.go index 03eeb29f3..95eeef615 100644 --- a/controller/gateway/controller_reconciler_utils_test.go +++ b/controller/gateway/controller_reconciler_utils_test.go @@ -3,8 +3,6 @@ package gateway import ( "context" "errors" - "fmt" - "os" "testing" "github.com/samber/lo" @@ -13,7 +11,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -21,21 +18,11 @@ import ( operatorv1beta1 "github.com/kong/gateway-operator/api/v1beta1" gwtypes "github.com/kong/gateway-operator/internal/types" + "github.com/kong/gateway-operator/modules/manager/scheme" "github.com/kong/gateway-operator/pkg/consts" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" ) -func init() { - if err := gatewayv1.Install(scheme.Scheme); err != nil { - fmt.Println("error while adding gatewayv1 scheme") - os.Exit(1) - } - if err := gatewayv1beta1.Install(scheme.Scheme); err != nil { - fmt.Println("error while adding gatewayv1 scheme") - os.Exit(1) - } -} - func TestParseKongProxyListenEnv(t *testing.T) { testcases := []struct { Name string @@ -528,7 +515,7 @@ func TestSetAcceptedOnGateway(t *testing.T) { func TestSetDataPlaneIngressServicePorts(t *testing.T) { testCases := []struct { name string - listeners []gatewayv1.Listener + listeners []gwtypes.Listener expectedPorts []operatorv1beta1.DataPlaneServicePort expectedError error }{ @@ -537,10 +524,10 @@ func TestSetDataPlaneIngressServicePorts(t *testing.T) { }, { name: "only valid listeners", - listeners: []gatewayv1.Listener{ + listeners: []gwtypes.Listener{ { Name: "http", - Protocol: gatewayv1.HTTPProtocolType, + Protocol: gwtypes.HTTPProtocolType, Port: gatewayv1.PortNumber(80), }, { @@ -564,10 +551,10 @@ func TestSetDataPlaneIngressServicePorts(t *testing.T) { }, { name: "some invalid listeners", - listeners: []gatewayv1.Listener{ + listeners: []gwtypes.Listener{ { Name: "http", - Protocol: gatewayv1.HTTPProtocolType, + Protocol: gwtypes.HTTPProtocolType, Port: gatewayv1.PortNumber(80), }, { @@ -609,9 +596,9 @@ func TestIsSecretCrossReferenceGranted(t *testing.T) { return rg } - badSecretName := gatewayv1.ObjectName("wrong-secret") - emptySecretName := gatewayv1.ObjectName("") - goodSecretName := gatewayv1.ObjectName("good-secret") + badSecretName := gwtypes.ObjectName("wrong-secret") + emptySecretName := gwtypes.ObjectName("") + goodSecretName := gwtypes.ObjectName("good-secret") referenceGrant := gatewayv1beta1.ReferenceGrant{ Spec: gatewayv1beta1.ReferenceGrantSpec{ From: []gatewayv1beta1.ReferenceGrantFrom{ @@ -730,7 +717,7 @@ func TestGatewayStatusNeedsUpdate(t *testing.T) { } listenerStatus := gatewayv1.ListenerStatus{ - SupportedKinds: []gatewayv1.RouteGroupKind{ + SupportedKinds: []gwtypes.RouteGroupKind{ { Kind: "HTTPRoute", }, @@ -820,7 +807,7 @@ func TestGatewayStatusNeedsUpdate(t *testing.T) { name: "update needed, different supportedkinds", needsUpdate: true, oldGateway: gatewayConditionsAndListenersAware(customizeGateway(gateway, func(g *gatewayv1.Gateway) { - g.Status.Listeners[0].SupportedKinds = []gatewayv1.RouteGroupKind{} + g.Status.Listeners[0].SupportedKinds = []gwtypes.RouteGroupKind{} })), newGateway: gatewayConditionsAndListenersAware(&gateway), }, @@ -873,20 +860,20 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { testCases := []struct { name string gatewayNamespace string - listener gatewayv1.Listener + listener gwtypes.Listener referenceGrants []client.Object secrets []client.Object - expectedSupportedKinds []gatewayv1.RouteGroupKind + expectedSupportedKinds []gwtypes.RouteGroupKind expectedResolvedRefsCondition metav1.Condition }{ { name: "no tls, HTTP protocol, no allowed routes", - listener: gatewayv1.Listener{ - Protocol: gatewayv1.HTTPProtocolType, + listener: gwtypes.Listener{ + Protocol: gwtypes.HTTPProtocolType, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -900,10 +887,10 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, { name: "no tls, UDP protocol, no allowed routes", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.UDPProtocolType, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{}, + expectedSupportedKinds: []gwtypes.RouteGroupKind{}, expectedResolvedRefsCondition: metav1.Condition{ Type: string(gatewayv1.ListenerConditionResolvedRefs), Status: metav1.ConditionTrue, @@ -914,24 +901,24 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, { name: "no tls, HTTP protocol, HTTP and UDP routes", - listener: gatewayv1.Listener{ - Protocol: gatewayv1.HTTPProtocolType, - AllowedRoutes: &gatewayv1.AllowedRoutes{ - Kinds: []gatewayv1.RouteGroupKind{ + listener: gwtypes.Listener{ + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Kinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "UDPRoute", }, }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -946,7 +933,7 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls well-formed, no cross-namespace reference", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModeTerminate), @@ -965,9 +952,9 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -982,7 +969,7 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls with passthrough, HTTPS protocol, no allowed routes", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModePassthrough), @@ -1001,9 +988,9 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -1018,7 +1005,7 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls bad-formed, multiple TLS secrets no cross-namespace reference", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModeTerminate), @@ -1032,9 +1019,9 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -1049,7 +1036,7 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls bad-formed, no tls secret, no cross-namespace reference", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModeTerminate), @@ -1060,9 +1047,9 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -1077,22 +1064,22 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls bad-formed, bad group and kind of tls secret, no cross-namespace reference", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModeTerminate), CertificateRefs: []gatewayv1.SecretObjectReference{ { Name: "test-secret", - Group: (*gatewayv1.Group)(lo.ToPtr("bad-group")), - Kind: (*gatewayv1.Kind)(lo.ToPtr("bad-kind")), + Group: (*gwtypes.Group)(lo.ToPtr("bad-group")), + Kind: (*gwtypes.Kind)(lo.ToPtr("bad-kind")), }, }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -1107,7 +1094,7 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls well-formed, with allowed cross-namespace reference", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModeTerminate), @@ -1144,15 +1131,15 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { Group: "", Kind: "Secret", - Name: (*gatewayv1.ObjectName)(lo.ToPtr("test-secret")), + Name: (*gwtypes.ObjectName)(lo.ToPtr("test-secret")), }, }, }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -1167,7 +1154,7 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { { name: "tls well-formed, with unallowed cross-namespace reference", gatewayNamespace: "default", - listener: gatewayv1.Listener{ + listener: gwtypes.Listener{ Protocol: gatewayv1.HTTPSProtocolType, TLS: &gatewayv1.GatewayTLSConfig{ Mode: lo.ToPtr(gatewayv1.TLSModeTerminate), @@ -1187,9 +1174,9 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }, }, }, - expectedSupportedKinds: []gatewayv1.RouteGroupKind{ + expectedSupportedKinds: []gwtypes.RouteGroupKind{ { - Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), Kind: "HTTPRoute", }, }, @@ -1205,10 +1192,10 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { for _, tc := range testCases { tc := tc - ctx := context.TODO() + ctx := context.Background() client := fakectrlruntimeclient. NewClientBuilder(). - WithScheme(scheme.Scheme). + WithScheme(scheme.Get()). WithObjects(tc.referenceGrants...). WithObjects(tc.secrets...). Build() @@ -1228,3 +1215,439 @@ func TestGetSupportedKindsWithResolvedRefsCondition(t *testing.T) { }) } } + +func TestCountAttachedRoutesForGatewayListener(t *testing.T) { + testCases := []struct { + Name string + Gateway gwtypes.Gateway + Objects []client.Object + ExpectedRoutes []int32 + ExpectedError []error + }{ + { + Name: "no routes", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromSame), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{0}, + ExpectedError: []error{nil}, + }, + { + Name: "1 HTTPRoute in the same namespace as the Gateway", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromSame), + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{1}, + ExpectedError: []error{nil}, + }, + { + Name: "1 HTTPRoute in a different namespace than the Gateway", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromSame), + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace-2", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{0}, + ExpectedError: []error{nil}, + }, + { + Name: "1 HTTPRoute in a different namespace than the Gateway but allowed through 'All' namespace selector", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace-2", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{1}, + ExpectedError: []error{nil}, + }, + { + Name: "2 HTTPRoutes, 1 matching the Gateway's namespace and 1 not", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromSame), + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace-2", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-2", + Namespace: "test-namespace", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{1}, + ExpectedError: []error{nil}, + }, + { + Name: "2 HTTPRoutes, both matching due to 'All' selector used", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromAll), + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace-2", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-2", + Namespace: "test-namespace", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{2}, + ExpectedError: []error{nil}, + }, + { + Name: "1 HTTPRoute, not matching due to namespace label selector not matching", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromSelector), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": "test-namespace-non-existing", + }, + }, + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{0}, + ExpectedError: []error{nil}, + }, + { + Name: "1 HTTPRoute, matching thanks to namespace label selector matching", + Gateway: gwtypes.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-namespace", + }, + Spec: gwtypes.GatewaySpec{ + Listeners: []gwtypes.Listener{ + { + Name: gatewayv1.SectionName("http"), + Protocol: gwtypes.HTTPProtocolType, + AllowedRoutes: &gwtypes.AllowedRoutes{ + Namespaces: &gwtypes.RouteNamespaces{ + From: lo.ToPtr(gwtypes.NamespacesFromSelector), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": "test-namespace-2", + }, + }, + }, + }, + }, + }, + }, + }, + Objects: []client.Object{ + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace-2", + Labels: map[string]string{ + "kubernetes.io/metadata.name": "test-namespace-2", + }, + }, + }, + &gwtypes.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-1", + Namespace: "test-namespace-2", + }, + Spec: gwtypes.HTTPRouteSpec{ + CommonRouteSpec: gwtypes.CommonRouteSpec{ + ParentRefs: []gwtypes.ParentReference{ + { + Name: gwtypes.ObjectName("test-gw"), + Group: (*gwtypes.Group)(&gatewayv1.GroupVersion.Group), + Kind: lo.ToPtr(gwtypes.Kind("Gateway")), + }, + }, + }, + }, + }, + }, + ExpectedRoutes: []int32{1}, + ExpectedError: []error{nil}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + client := fakectrlruntimeclient. + NewClientBuilder(). + WithScheme(scheme.Get()). + WithObjects(&tc.Gateway). + WithObjects(tc.Objects...). + Build() + + ctx := context.Background() + for i, listener := range tc.Gateway.Spec.Listeners { + routes, err := countAttachedRoutesForGatewayListener(ctx, &tc.Gateway, listener, client) + assert.Equal(t, tc.ExpectedRoutes[i], routes, "#%d", i) + assert.Equal(t, tc.ExpectedError[i], err, "#%d", i) + } + }) + } +} diff --git a/controller/gateway/controller_watch.go b/controller/gateway/controller_watch.go index 46e5e074f..975d42890 100644 --- a/controller/gateway/controller_watch.go +++ b/controller/gateway/controller_watch.go @@ -222,6 +222,89 @@ func (r *Reconciler) listReferenceGrantsForGateway(ctx context.Context, obj clie return recs } +// listManagedGatewaysInNamespace is a watch predicate which finds all Gateways +// in provided namespace. +func (r *Reconciler) listManagedGatewaysInNamespace(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + ns, ok := obj.(*corev1.Namespace) + if !ok { + logger.Error( + fmt.Errorf("unexpected object type"), + "Namespace watch predicate received unexpected object type", + "expected", "*corev1.Namespace", "found", reflect.TypeOf(obj), + ) + return nil + } + gateways := &gatewayv1.GatewayList{} + if err := r.Client.List(ctx, gateways, &client.ListOptions{ + Namespace: ns.Name, + }); err != nil { + logger.Error(err, "Failed to list gateways in watch", "namespace", ns.Name) + return nil + } + recs := make([]reconcile.Request, 0, len(gateways.Items)) + for _, gateway := range gateways.Items { + objKey := client.ObjectKey{Name: string(gateway.Spec.GatewayClassName)} + + var gatewaClass gatewayv1.GatewayClass + if err := r.Client.Get(ctx, objKey, &gatewaClass); err != nil { + logger.Error( + fmt.Errorf("failed to get GatewayClass"), + "failed to Get Gateway's GatewayClass", + "gatewayClass", objKey.Name, "gateway", gateway.Name, "namespace", gateway.Namespace, + ) + continue + } + if string(gatewaClass.Spec.ControllerName) == vars.ControllerName() { + recs = append(recs, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: gateway.Namespace, + Name: gateway.Name, + }, + }) + } + } + return recs +} + +// listGatewaysAttachedByHTTPRoute is a watch predicate which finds all Gateways mentioned +// in HTTPRoutes' Parents field. +func (r *Reconciler) listGatewaysAttachedByHTTPRoute(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + httpRoute, ok := obj.(*gatewayv1beta1.HTTPRoute) + if !ok { + logger.Error( + fmt.Errorf("unexpected object type"), + "HTTPRoute watch predicate received unexpected object type", + "expected", "*gatewayapi.HTTPRoute", "found", reflect.TypeOf(obj), + ) + return nil + } + gateways := &gatewayv1.GatewayList{} + if err := r.Client.List(ctx, gateways); err != nil { + logger.Error(err, "Failed to list gateways in watch", "HTTPRoute", httpRoute.Name) + return nil + } + var recs []reconcile.Request + for _, gateway := range gateways.Items { + for _, parentRef := range httpRoute.Spec.ParentRefs { + if parentRef.Group != nil && string(*parentRef.Group) == gatewayv1.GroupName && + parentRef.Kind != nil && string(*parentRef.Kind) == "Gateway" && + string(parentRef.Name) == gateway.Name { + recs = append(recs, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: gateway.Namespace, + Name: gateway.Name, + }, + }) + } + } + } + return recs +} + // ----------------------------------------------------------------------------- // GatewayReconciler - Config Defaults // ----------------------------------------------------------------------------- diff --git a/controller/pkg/patch/patch.go b/controller/pkg/patch/patch.go index 15fe3065d..ba848309f 100644 --- a/controller/pkg/patch/patch.go +++ b/controller/pkg/patch/patch.go @@ -68,7 +68,8 @@ func ApplyGatewayStatusPatchIfNotEmpty(ctx context.Context, cl client.Client, logger logr.Logger, existingGateway *gatewayv1.Gateway, - oldExistingGateway *gatewayv1.Gateway) (res op.CreatedUpdatedOrNoop, err error) { + oldExistingGateway *gatewayv1.Gateway, +) (res op.CreatedUpdatedOrNoop, err error) { // Check if the patch to be applied is empty. patch := client.MergeFrom(oldExistingGateway) b, err := patch.Data(existingGateway) @@ -81,7 +82,7 @@ func ApplyGatewayStatusPatchIfNotEmpty(ctx context.Context, return op.Noop, nil } - if err := cl.Status().Patch(ctx, existingGateway, client.MergeFrom(oldExistingGateway)); err != nil { + if err := cl.Status().Patch(ctx, existingGateway, patch); err != nil { return op.Noop, fmt.Errorf("failed patching gateway %s/%s: %w", existingGateway.Namespace, existingGateway.Name, err) } log.Debug(logger, "Resource modified", existingGateway) diff --git a/internal/types/gatewaytypes.go b/internal/types/gatewaytypes.go index ebdbf2032..885fcf6b9 100644 --- a/internal/types/gatewaytypes.go +++ b/internal/types/gatewaytypes.go @@ -7,5 +7,26 @@ import ( type ( Gateway = gatewayv1.Gateway GatewayAddress = gatewayv1.GatewayAddress + GatewaySpec = gatewayv1.GatewaySpec GatewayStatusAddress = gatewayv1.GatewayStatusAddress + Listener = gatewayv1.Listener + HTTPRoute = gatewayv1.HTTPRoute + HTTPRouteSpec = gatewayv1.HTTPRouteSpec + HTTPRouteList = gatewayv1.HTTPRouteList + ParentReference = gatewayv1.ParentReference + CommonRouteSpec = gatewayv1.CommonRouteSpec + Kind = gatewayv1.Kind + Group = gatewayv1.Group + AllowedRoutes = gatewayv1.AllowedRoutes + RouteGroupKind = gatewayv1.RouteGroupKind + RouteNamespaces = gatewayv1.RouteNamespaces + ObjectName = gatewayv1.ObjectName +) + +const ( + HTTPProtocolType = gatewayv1.HTTPProtocolType + + NamespacesFromAll = gatewayv1.NamespacesFromAll + NamespacesFromSame = gatewayv1.NamespacesFromSame + NamespacesFromSelector = gatewayv1.NamespacesFromSelector ) diff --git a/pkg/utils/gateway/ownerrefs.go b/pkg/utils/gateway/ownerrefs.go index 84d0c3af6..860007415 100644 --- a/pkg/utils/gateway/ownerrefs.go +++ b/pkg/utils/gateway/ownerrefs.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/samber/lo" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -87,6 +88,45 @@ func ListControlPlanesForGateway( return controlplanes, nil } +// ListHTTPRoutesForGateway is a helper function which returns a list of HTTPRoutes +// that have the provided Gateway set as parent in their status. +func ListHTTPRoutesForGateway( + ctx context.Context, + c client.Client, + gateway *gwtypes.Gateway, + opts ...client.ListOption, +) ([]gwtypes.HTTPRoute, error) { + if gateway.Namespace == "" { + return nil, fmt.Errorf("can't list HTTPRoutes for gateway: Gateway %s was missing namespace", gateway.Name) + } + + var httpRoutesList gwtypes.HTTPRouteList + err := c.List( + ctx, + &httpRoutesList, + opts..., + ) + if err != nil { + return nil, fmt.Errorf("can't list HTTPRoutes for gateway: %w", err) + } + + var httpRoutes []gwtypes.HTTPRoute + for _, httpRoute := range httpRoutesList.Items { + if !lo.ContainsBy(httpRoute.Spec.ParentRefs, func(parentRef gwtypes.ParentReference) bool { + gwGVK := gateway.GroupVersionKind() + return (parentRef.Group != nil && string(*parentRef.Group) == gwGVK.Group) && + (parentRef.Kind != nil && string(*parentRef.Kind) == gwGVK.Kind) && + string(parentRef.Name) == gateway.Name + }) { + continue + } + + httpRoutes = append(httpRoutes, httpRoute) + } + + return httpRoutes, nil +} + // GetDataPlaneForControlPlane retrieves the DataPlane object referenced by a ControlPlane func GetDataPlaneForControlPlane( ctx context.Context, diff --git a/test/conformance/conformance_test.go b/test/conformance/conformance_test.go index f14314890..f187d2271 100644 --- a/test/conformance/conformance_test.go +++ b/test/conformance/conformance_test.go @@ -24,6 +24,7 @@ var skippedTests = []string{ // gateway tests.GatewayInvalidTLSConfiguration.ShortName, tests.GatewayModifyListeners.ShortName, + // TODO: https://github.com/Kong/gateway-operator/issues/56 tests.GatewayWithAttachedRoutes.ShortName, // httproute