From 11e0f2e0f2d386176e8b2d0e9c9ce3892a5781aa Mon Sep 17 00:00:00 2001 From: danehans Date: Wed, 21 Sep 2022 14:04:36 -0700 Subject: [PATCH] Adds Gateway Watch to HTTPRoute Controller Signed-off-by: danehans --- internal/provider/kubernetes/httproute.go | 50 +++ .../provider/kubernetes/httproute_test.go | 377 ++++++++++++++++++ 2 files changed, 427 insertions(+) diff --git a/internal/provider/kubernetes/httproute.go b/internal/provider/kubernetes/httproute.go index 92987bc096..8844748fe0 100644 --- a/internal/provider/kubernetes/httproute.go +++ b/internal/provider/kubernetes/httproute.go @@ -92,6 +92,14 @@ func newHTTPRouteController(mgr manager.Manager, cfg *config.Server, su status.U return err } + // Watch Gateway CRUDs and reconcile affected HTTPRoutes. + if err := c.Watch( + &source.Kind{Type: &gwapiv1b1.Gateway{}}, + handler.EnqueueRequestsFromMapFunc(r.getHTTPRoutesForGateway), + ); err != nil { + return err + } + // Watch Service CRUDs and reconcile affected HTTPRoutes. if err := c.Watch( &source.Kind{Type: &corev1.Service{}}, @@ -104,6 +112,48 @@ func newHTTPRouteController(mgr manager.Manager, cfg *config.Server, su status.U return nil } +// getHTTPRoutesForGateway uses a Gateway obj to fetch HTTPRoutes, iterating +// through them and creating a reconciliation request for each valid HTTPRoute +// that references obj. +func (r *httpRouteReconciler) getHTTPRoutesForGateway(obj client.Object) []reconcile.Request { + ctx := context.Background() + + gw, ok := obj.(*gwapiv1b1.Gateway) + if !ok { + r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) + return []reconcile.Request{} + } + + routes := &gwapiv1b1.HTTPRouteList{} + if err := r.client.List(ctx, routes); err != nil { + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for i := range routes.Items { + route := routes.Items[i] + gateways, err := r.validateParentRefs(ctx, &route) + if err != nil { + r.log.Info("invalid parentRefs for httproute, bypassing reconciliation", "object", obj) + continue + } + for j := range gateways { + if gateways[j].Namespace == gw.Namespace && gateways[j].Name == gw.Name { + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: route.Namespace, + Name: route.Name, + }, + } + requests = append(requests, req) + break + } + } + } + + return requests +} + // getHTTPRoutesForService uses a Service obj to fetch HTTPRoutes that references // the Service using `.spec.rules.backendRefs`. The affected HTTPRoutes are then // pushed for reconciliation. diff --git a/internal/provider/kubernetes/httproute_test.go b/internal/provider/kubernetes/httproute_test.go index 575265c39f..6fd509c78b 100644 --- a/internal/provider/kubernetes/httproute_test.go +++ b/internal/provider/kubernetes/httproute_test.go @@ -7,15 +7,392 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway" "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/log" ) +func TestGetHTTPRoutesForGateway(t *testing.T) { + testCases := []struct { + name string + obj client.Object + routes []gwapiv1b1.HTTPRoute + classes []gwapiv1b1.GatewayClass + expect []reconcile.Request + }{ + { + name: "valid route", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "gc1", + }, + }, + routes: []gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h1", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("Gateway"), + Name: gwapiv1b1.ObjectName("gw1"), + }, + }, + }, + }, + }, + }, + classes: []gwapiv1b1.GatewayClass{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + }, + }, + expect: []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: "test", + Name: "h1", + }, + }, + }, + }, + { + name: "one valid route in different namespace", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "gc1", + }, + }, + routes: []gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test2", + Name: "h1", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("Gateway"), + Name: gwapiv1b1.ObjectName("gw1"), + Namespace: gatewayapi.NamespacePtr("test"), + }, + }, + }, + }, + }, + }, + classes: []gwapiv1b1.GatewayClass{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + }, + }, + expect: []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: "test2", + Name: "h1", + }, + }, + }, + }, + { + name: "two valid routes", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "gc1", + }, + }, + routes: []gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h1", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("Gateway"), + Name: gwapiv1b1.ObjectName("gw1"), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h2", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("Gateway"), + Name: gwapiv1b1.ObjectName("gw1"), + }, + }, + }, + }, + }, + }, + classes: []gwapiv1b1.GatewayClass{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + }, + }, + expect: []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: "test", + Name: "h1", + }, + }, + { + NamespacedName: types.NamespacedName{ + Namespace: "test", + Name: "h2", + }, + }, + }, + }, + { + name: "object referenced unmanaged gateway", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "gc1", + }, + }, + routes: []gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h1", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("Gateway"), + Name: gwapiv1b1.ObjectName("gw1"), + }, + }, + }, + }, + }, + }, + classes: []gwapiv1b1.GatewayClass{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gwapiv1b1.GatewayController("unmanaged.controller"), + }, + }, + }, + expect: []reconcile.Request{}, + }, + { + name: "valid route", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "gc1", + }, + }, + routes: []gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h1", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("Gateway"), + Name: gwapiv1b1.ObjectName("gw1"), + }, + }, + }, + }, + }, + }, + classes: []gwapiv1b1.GatewayClass{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + }, + }, + expect: []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: "test", + Name: "h1", + }, + }, + }, + }, + { + name: "no valid routes", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "gc1", + }, + }, + routes: []gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h1", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("UnsupportedKind"), + Name: gwapiv1b1.ObjectName("unsupported"), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "h2", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Group: gatewayapi.GroupPtr(gwapiv1b1.GroupName), + Kind: gatewayapi.KindPtr("UnsupportedKind"), + Name: gwapiv1b1.ObjectName("unsupported2"), + }, + }, + }, + }, + }, + }, + classes: []gwapiv1b1.GatewayClass{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + }, + }, + expect: []reconcile.Request{}, + }, + { + name: "no routes", + obj: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gw1", + }, + }, + expect: []reconcile.Request{}, + }, + { + name: "invalid object type", + obj: &gwapiv1b1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gc1", + }, + }, + expect: []reconcile.Request{}, + }, + } + + // Create the reconciler. + logger, err := log.NewLogger() + require.NoError(t, err) + r := &httpRouteReconciler{ + log: logger, + classController: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName)} + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + objs := []client.Object{tc.obj} + for i := range tc.routes { + objs = append(objs, &tc.routes[i]) + } + for i := range tc.classes { + objs = append(objs, &tc.classes[i]) + } + r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build() + reqs := r.getHTTPRoutesForGateway(tc.obj) + assert.Equal(t, tc.expect, reqs) + }) + } +} + func TestValidateParentRefs(t *testing.T) { testCases := []struct { name string