diff --git a/api/v1alpha1/ratelimitfilter_types.go b/api/v1alpha1/ratelimitfilter_types.go index 5d3510eff5c..e5b4d8b00a9 100644 --- a/api/v1alpha1/ratelimitfilter_types.go +++ b/api/v1alpha1/ratelimitfilter_types.go @@ -9,6 +9,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // KindRateLimitFilter is the name of the RateLimitFilter kind. + KindRateLimitFilter = "RateLimitFilter" +) + // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` @@ -162,3 +167,7 @@ type RateLimitValue struct { // // +kubebuilder:validation:Enum=Second;Minute;Hour;Day type RateLimitUnit string + +func init() { + SchemeBuilder.Register(&RateLimitFilter{}, &RateLimitFilterList{}) +} diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 6276ccaf5ae..301e2cde773 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -196,10 +196,12 @@ func ValidateHTTPRouteFilter(filter *v1beta1.HTTPRouteFilter) error { return errors.New("extensionRef field must be specified for an extended filter") case string(filter.ExtensionRef.Group) != egv1a1.GroupVersion.Group: return fmt.Errorf("invalid group; must be %s", egv1a1.GroupVersion.Group) - case string(filter.ExtensionRef.Kind) != egv1a1.KindAuthenticationFilter: - return fmt.Errorf("invalid kind; must be %s", egv1a1.KindAuthenticationFilter) - default: + case string(filter.ExtensionRef.Kind) == egv1a1.KindAuthenticationFilter: + return nil + case string(filter.ExtensionRef.Kind) == egv1a1.KindRateLimitFilter: return nil + default: + return fmt.Errorf("unknown %s kind", string(filter.ExtensionRef.Kind)) } } diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go index 89d18939dd4..5a3da91fb9c 100644 --- a/internal/gatewayapi/helpers_test.go +++ b/internal/gatewayapi/helpers_test.go @@ -109,6 +109,18 @@ func TestValidateAuthenFilterRef(t *testing.T) { }, expected: true, }, + { + name: "valid rateLimitfilter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: gwapiv1b1.Group(egv1a1.GroupVersion.Group), + Kind: egv1a1.KindRateLimitFilter, + Name: "test", + }, + }, + expected: true, + }, } for _, tc := range testCases { diff --git a/internal/gatewayapi/resource.go b/internal/gatewayapi/resource.go index 18798782098..74aa8be8440 100644 --- a/internal/gatewayapi/resource.go +++ b/internal/gatewayapi/resource.go @@ -22,32 +22,34 @@ type InfraIRMap map[string]*ir.Infra // resources that the translators needs as inputs. // +k8s:deepcopy-gen=true type Resources struct { - Gateways []*v1beta1.Gateway - HTTPRoutes []*v1beta1.HTTPRoute - GRPCRoutes []*v1alpha2.GRPCRoute - TLSRoutes []*v1alpha2.TLSRoute - TCPRoutes []*v1alpha2.TCPRoute - UDPRoutes []*v1alpha2.UDPRoute - ReferenceGrants []*v1alpha2.ReferenceGrant - Namespaces []*v1.Namespace - Services []*v1.Service - Secrets []*v1.Secret - AuthenFilters []*egv1a1.AuthenticationFilter - EnvoyProxy *egcfgv1a1.EnvoyProxy + Gateways []*v1beta1.Gateway + HTTPRoutes []*v1beta1.HTTPRoute + GRPCRoutes []*v1alpha2.GRPCRoute + TLSRoutes []*v1alpha2.TLSRoute + TCPRoutes []*v1alpha2.TCPRoute + UDPRoutes []*v1alpha2.UDPRoute + ReferenceGrants []*v1alpha2.ReferenceGrant + Namespaces []*v1.Namespace + Services []*v1.Service + Secrets []*v1.Secret + AuthenFilters []*egv1a1.AuthenticationFilter + RateLimitFilters []*egv1a1.RateLimitFilter + EnvoyProxy *egcfgv1a1.EnvoyProxy } func NewResources() *Resources { return &Resources{ - Gateways: []*v1beta1.Gateway{}, - HTTPRoutes: []*v1beta1.HTTPRoute{}, - GRPCRoutes: []*v1alpha2.GRPCRoute{}, - TLSRoutes: []*v1alpha2.TLSRoute{}, - Services: []*v1.Service{}, - Secrets: []*v1.Secret{}, - ReferenceGrants: []*v1alpha2.ReferenceGrant{}, - Namespaces: []*v1.Namespace{}, - AuthenFilters: []*egv1a1.AuthenticationFilter{}, - EnvoyProxy: new(egcfgv1a1.EnvoyProxy), + Gateways: []*v1beta1.Gateway{}, + HTTPRoutes: []*v1beta1.HTTPRoute{}, + GRPCRoutes: []*v1alpha2.GRPCRoute{}, + TLSRoutes: []*v1alpha2.TLSRoute{}, + Services: []*v1.Service{}, + Secrets: []*v1.Secret{}, + ReferenceGrants: []*v1alpha2.ReferenceGrant{}, + Namespaces: []*v1.Namespace{}, + AuthenFilters: []*egv1a1.AuthenticationFilter{}, + RateLimitFilters: []*egv1a1.RateLimitFilter{}, + EnvoyProxy: new(egcfgv1a1.EnvoyProxy), } } diff --git a/internal/gatewayapi/zz_generated.deepcopy.go b/internal/gatewayapi/zz_generated.deepcopy.go index 39b7b955582..221c511cfc3 100644 --- a/internal/gatewayapi/zz_generated.deepcopy.go +++ b/internal/gatewayapi/zz_generated.deepcopy.go @@ -142,6 +142,17 @@ func (in *Resources) DeepCopyInto(out *Resources) { } } } + if in.RateLimitFilters != nil { + in, out := &in.RateLimitFilters, &out.RateLimitFilters + *out = make([]*v1alpha1.RateLimitFilter, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1alpha1.RateLimitFilter) + (*in).DeepCopyInto(*out) + } + } + } if in.EnvoyProxy != nil { in, out := &in.EnvoyProxy, &out.EnvoyProxy *out = new(configv1alpha1.EnvoyProxy) diff --git a/internal/provider/kubernetes/config/rbac/role.yaml b/internal/provider/kubernetes/config/rbac/role.yaml index cc4b644b278..d1b657b6b1e 100644 --- a/internal/provider/kubernetes/config/rbac/role.yaml +++ b/internal/provider/kubernetes/config/rbac/role.yaml @@ -36,6 +36,7 @@ rules: - gateway.envoyproxy.io resources: - authenticationfilters + - ratelimitfilters verbs: - get - list diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 8057670d6be..c2e96252055 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -38,20 +38,21 @@ import ( ) const ( - classGatewayIndex = "classGatewayIndex" - gatewayTLSRouteIndex = "gatewayTLSRouteIndex" - gatewayHTTPRouteIndex = "gatewayHTTPRouteIndex" - gatewayGRPCRouteIndex = "gatewayGRPCRouteIndex" - gatewayTCPRouteIndex = "gatewayTCPRouteIndex" - gatewayUDPRouteIndex = "gatewayUDPRouteIndex" - secretGatewayIndex = "secretGatewayIndex" - targetRefGrantRouteIndex = "targetRefGrantRouteIndex" - serviceHTTPRouteIndex = "serviceHTTPRouteIndex" - serviceGRPCRouteIndex = "serviceGRPCRouteIndex" - serviceTLSRouteIndex = "serviceTLSRouteIndex" - serviceTCPRouteIndex = "serviceTCPRouteIndex" - serviceUDPRouteIndex = "serviceUDPRouteIndex" - authenFilterHTTPRouteIndex = "authenHTTPRouteIndex" + classGatewayIndex = "classGatewayIndex" + gatewayTLSRouteIndex = "gatewayTLSRouteIndex" + gatewayHTTPRouteIndex = "gatewayHTTPRouteIndex" + gatewayGRPCRouteIndex = "gatewayGRPCRouteIndex" + gatewayTCPRouteIndex = "gatewayTCPRouteIndex" + gatewayUDPRouteIndex = "gatewayUDPRouteIndex" + secretGatewayIndex = "secretGatewayIndex" + targetRefGrantRouteIndex = "targetRefGrantRouteIndex" + serviceHTTPRouteIndex = "serviceHTTPRouteIndex" + serviceGRPCRouteIndex = "serviceGRPCRouteIndex" + serviceTLSRouteIndex = "serviceTLSRouteIndex" + serviceTCPRouteIndex = "serviceTCPRouteIndex" + serviceUDPRouteIndex = "serviceUDPRouteIndex" + authenFilterHTTPRouteIndex = "authenHTTPRouteIndex" + rateLimitFilterHTTPRouteIndex = "rateLimitHTTPRouteIndex" ) type gatewayAPIReconciler struct { @@ -103,6 +104,9 @@ type resourceMappings struct { // authenFilters is a map of AuthenticationFilters, where the key is the // namespaced name of the AuthenticationFilter. authenFilters map[types.NamespacedName]*egv1a1.AuthenticationFilter + // rateLimitFilters is a map of RateLimitFilters, where the key is the + // namespaced name of the RateLimitFilter. + rateLimitFilters map[types.NamespacedName]*egv1a1.RateLimitFilter } func newResourceMapping() *resourceMappings { @@ -111,6 +115,7 @@ func newResourceMapping() *resourceMappings { allAssociatedBackendRefs: map[types.NamespacedName]struct{}{}, allAssociatedRefGrants: map[types.NamespacedName]*gwapiv1a2.ReferenceGrant{}, authenFilters: map[types.NamespacedName]*egv1a1.AuthenticationFilter{}, + rateLimitFilters: map[types.NamespacedName]*egv1a1.RateLimitFilter{}, } } @@ -357,6 +362,15 @@ func (r *gatewayAPIReconciler) getAuthenticationFilters(ctx context.Context) ([] return authenList.Items, nil } +func (r *gatewayAPIReconciler) getRateLimitFilters(ctx context.Context) ([]egv1a1.RateLimitFilter, error) { + rateLimitList := new(egv1a1.RateLimitFilterList) + if err := r.client.List(ctx, rateLimitList); err != nil { + return nil, fmt.Errorf("failed to list RateLimitFilters: %v", err) + } + + return rateLimitList.Items, nil +} + func (r *gatewayAPIReconciler) processGateways(ctx context.Context, acceptedGC *gwapiv1b1.GatewayClass, resourceMap *resourceMappings, resourceTree *gatewayapi.Resources) error { // Find gateways for the acceptedGC // Find the Gateways that reference this Class. @@ -474,7 +488,7 @@ func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error { // addHTTPRouteIndexers adds indexing on HTTPRoute. // - For Service objects that are referenced in HTTPRoute objects via `.spec.rules.backendRefs`. // This helps in querying for HTTPRoutes that are affected by a particular Service CRUD. -// - For AuthenticationFilter objects that are referenced in HTTPRoute objects via +// - For AuthenticationFilter and RateLimitFilter objects that are referenced in HTTPRoute objects via // `.spec.rules[].filters`. This helps in querying for HTTPRoutes that are affected by a // particular AuthenticationFilter CRUD. func addHTTPRouteIndexers(ctx context.Context, mgr manager.Manager) error { @@ -486,30 +500,56 @@ func addHTTPRouteIndexers(ctx context.Context, mgr manager.Manager) error { return err } - if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.HTTPRoute{}, authenFilterHTTPRouteIndex, func(obj client.Object) []string { - httproute := obj.(*gwapiv1b1.HTTPRoute) - var filters []string - for _, rule := range httproute.Spec.Rules { - for i := range rule.Filters { - filter := rule.Filters[i] - if filter.Type == gwapiv1b1.HTTPRouteFilterExtensionRef { - if err := gatewayapi.ValidateHTTPRouteFilter(&filter); err != nil { - filters = append(filters, - types.NamespacedName{ - Namespace: httproute.Namespace, - Name: string(filter.ExtensionRef.Name), - }.String(), - ) - } + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.HTTPRoute{}, authenFilterHTTPRouteIndex, authenFilterHTTPRouteIndexFunc); err != nil { + return err + } + + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.HTTPRoute{}, rateLimitFilterHTTPRouteIndex, rateLimitFilterHTTPRouteIndexFunc); err != nil { + return err + } + return nil +} + +func authenFilterHTTPRouteIndexFunc(rawObj client.Object) []string { + httproute := rawObj.(*gwapiv1b1.HTTPRoute) + var filters []string + for _, rule := range httproute.Spec.Rules { + for i := range rule.Filters { + filter := rule.Filters[i] + if filter.Type == gwapiv1b1.HTTPRouteFilterExtensionRef && string(filter.ExtensionRef.Kind) == egv1a1.KindAuthenticationFilter { + if err := gatewayapi.ValidateHTTPRouteFilter(&filter); err != nil { + filters = append(filters, + types.NamespacedName{ + Namespace: httproute.Namespace, + Name: string(filter.ExtensionRef.Name), + }.String(), + ) } } } - return filters - }); err != nil { - return err } + return filters +} - return nil +func rateLimitFilterHTTPRouteIndexFunc(rawObj client.Object) []string { + httproute := rawObj.(*gwapiv1b1.HTTPRoute) + var filters []string + for _, rule := range httproute.Spec.Rules { + for i := range rule.Filters { + filter := rule.Filters[i] + if filter.Type == gwapiv1b1.HTTPRouteFilterExtensionRef && string(filter.ExtensionRef.Kind) == egv1a1.KindRateLimitFilter { + if err := gatewayapi.ValidateHTTPRouteFilter(&filter); err != nil { + filters = append(filters, + types.NamespacedName{ + Namespace: httproute.Namespace, + Name: string(filter.ExtensionRef.Name), + }.String(), + ) + } + } + } + } + return filters } func gatewayHTTPRouteIndexFunc(rawObj client.Object) []string { @@ -1133,6 +1173,14 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M return err } + // Watch RateLimitFilter CRUDs and enqueue associated HTTPRoute objects. + if err := c.Watch( + &source.Kind{Type: &egv1a1.RateLimitFilter{}}, + &handler.EnqueueRequestForObject{}, + predicate.NewPredicateFuncs(r.httpRoutesForRateLimitFilter)); err != nil { + return err + } + r.log.Info("watching gatewayAPI related objects") return nil } diff --git a/internal/provider/kubernetes/kubernetes_test.go b/internal/provider/kubernetes/kubernetes_test.go index a9f7d7b1ef1..3debdab37a5 100644 --- a/internal/provider/kubernetes/kubernetes_test.go +++ b/internal/provider/kubernetes/kubernetes_test.go @@ -524,6 +524,14 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour require.NoError(t, cli.Delete(ctx, authenFilter)) }() + rateLimitFilter := test.GetRateLimitFilter("test-rateLimit", ns.Name) + + require.NoError(t, cli.Create(ctx, rateLimitFilter)) + + defer func() { + require.NoError(t, cli.Delete(ctx, rateLimitFilter)) + }() + redirectHostname := gwapiv1b1.PreciseHostname("redirect.hostname.local") redirectPort := gwapiv1b1.PortNumber(8443) redirectStatus := 301 diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go index fdabbb2fca4..a5a6bdf52fa 100644 --- a/internal/provider/kubernetes/predicates.go +++ b/internal/provider/kubernetes/predicates.go @@ -213,6 +213,28 @@ func (r *gatewayAPIReconciler) httpRoutesForAuthenticationFilter(obj client.Obje return len(httpRouteList.Items) != 0 } +// httpRoutesForRateLimitFilter tries finding HTTPRoute referents of the provided +// RateLimitFilter and returns true if any exist. +func (r *gatewayAPIReconciler) httpRoutesForRateLimitFilter(obj client.Object) bool { + ctx := context.Background() + filter, ok := obj.(*egv1a1.RateLimitFilter) + if !ok { + r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) + return false + } + + // Check if the RateLimitFilter belongs to a managed HTTPRoute. + httpRouteList := &gwapiv1b1.HTTPRouteList{} + if err := r.client.List(ctx, httpRouteList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(rateLimitFilterHTTPRouteIndex, utils.NamespacedName(filter).String()), + }); err != nil { + r.log.Error(err, "unable to find associated HTTPRoutes") + return false + } + + return len(httpRouteList.Items) != 0 +} + // envoyDeploymentForGateway returns the Envoy Deployment, returning nil if the Deployment doesn't exist. func (r *gatewayAPIReconciler) envoyDeploymentForGateway(ctx context.Context, gateway *gwapiv1b1.Gateway) (*appsv1.Deployment, error) { key := types.NamespacedName{ diff --git a/internal/provider/kubernetes/rbac.go b/internal/provider/kubernetes/rbac.go index 11322cce932..dedc26e8e73 100644 --- a/internal/provider/kubernetes/rbac.go +++ b/internal/provider/kubernetes/rbac.go @@ -10,8 +10,8 @@ package kubernetes // +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;grpcroutes/status;tlsroutes/status;tcproutes/status;udproutes/status,verbs=update // RBAC for Envoy Gateway custom resources. -// +kubebuilder:rbac:groups="gateway.envoyproxy.io",resources=authenticationfilters,verbs=get;list;watch;update // +kubebuilder:rbac:groups="config.gateway.envoyproxy.io",resources=envoyproxies,verbs=get;list;watch;update +// +kubebuilder:rbac:groups="gateway.envoyproxy.io",resources=authenticationfilters;ratelimitfilters,verbs=get;list;watch;update // RBAC for watched resources of Gateway API controllers. // +kubebuilder:rbac:groups="",resources=secrets;services;namespaces,verbs=get;list;watch diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go index 8db8ad8d16f..41f4aa6c02a 100644 --- a/internal/provider/kubernetes/routes.go +++ b/internal/provider/kubernetes/routes.go @@ -15,6 +15,7 @@ import ( gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/provider/utils" ) @@ -153,16 +154,26 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam resourceMap *resourceMappings, resourceTree *gatewayapi.Resources) error { httpRouteList := &gwapiv1b1.HTTPRouteList{} - // An HTTPRoute may reference an AuthenticationFilter, so add them to the resource map first (if they exist). - filters, err := r.getAuthenticationFilters(ctx) + // An HTTPRoute may reference an AuthenticationFilter or RateLimitFilter, + // so add them to the resource map first (if they exist). + authenFilters, err := r.getAuthenticationFilters(ctx) if err != nil { return err } - for i := range filters { - filter := filters[i] + for i := range authenFilters { + filter := authenFilters[i] resourceMap.authenFilters[utils.NamespacedName(&filter)] = &filter } + rateLimitFilters, err := r.getRateLimitFilters(ctx) + if err != nil { + return err + } + for i := range rateLimitFilters { + filter := rateLimitFilters[i] + resourceMap.rateLimitFilters[utils.NamespacedName(&filter)] = &filter + } + if err := r.client.List(ctx, httpRouteList, &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector(gatewayHTTPRouteIndex, gatewayNamespaceName), }); err != nil { @@ -273,19 +284,35 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam } } } else if filter.Type == gwapiv1b1.HTTPRouteFilterExtensionRef { - key := types.NamespacedName{ - // The AuthenticationFilter must be in the same namespace as the HTTPRoute. - Namespace: httpRoute.Namespace, - Name: string(filter.ExtensionRef.Name), - } - filter, ok := resourceMap.authenFilters[key] - if !ok { - r.log.Error(err, "AuthenticationFilter not found; bypassing rule", "index", i) - continue - } + if string(filter.ExtensionRef.Kind) == egv1a1.KindAuthenticationFilter { + key := types.NamespacedName{ + // The AuthenticationFilter must be in the same namespace as the HTTPRoute. + Namespace: httpRoute.Namespace, + Name: string(filter.ExtensionRef.Name), + } + filter, ok := resourceMap.authenFilters[key] + if !ok { + r.log.Error(err, "AuthenticationFilter not found; bypassing rule", "index", i) + continue + } - resourceTree.AuthenFilters = append(resourceTree.AuthenFilters, filter) + resourceTree.AuthenFilters = append(resourceTree.AuthenFilters, filter) + } else if string(filter.ExtensionRef.Kind) == egv1a1.KindRateLimitFilter { + key := types.NamespacedName{ + // The RateLimitFilter must be in the same namespace as the HTTPRoute. + Namespace: httpRoute.Namespace, + Name: string(filter.ExtensionRef.Name), + } + filter, ok := resourceMap.rateLimitFilters[key] + if !ok { + r.log.Error(err, "RateLimitFilter not found; bypassing rule", "index", i) + continue + } + + resourceTree.RateLimitFilters = append(resourceTree.RateLimitFilters, filter) + } } + } } diff --git a/internal/provider/kubernetes/routes_test.go b/internal/provider/kubernetes/routes_test.go index d4b697ab104..b11821d4bcc 100644 --- a/internal/provider/kubernetes/routes_test.go +++ b/internal/provider/kubernetes/routes_test.go @@ -58,10 +58,11 @@ func TestProcessHTTPRoutes(t *testing.T) { gwNsName := utils.NamespacedName(gw).String() testCases := []struct { - name string - routes []*gwapiv1b1.HTTPRoute - filters []*egv1a1.AuthenticationFilter - expected bool + name string + routes []*gwapiv1b1.HTTPRoute + authenFilters []*egv1a1.AuthenticationFilter + rateLimitFilters []*egv1a1.RateLimitFilter + expected bool }{ { name: "valid httproute", @@ -159,7 +160,7 @@ func TestProcessHTTPRoutes(t *testing.T) { }, }, }, - filters: []*egv1a1.AuthenticationFilter{ + authenFilters: []*egv1a1.AuthenticationFilter{ { TypeMeta: metav1.TypeMeta{ Kind: egv1a1.KindAuthenticationFilter, @@ -186,6 +187,95 @@ func TestProcessHTTPRoutes(t *testing.T) { }, expected: true, }, + { + name: "httproute with one rateLimitfilter", + routes: []*gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Name: "test", + }, + }, + }, + Rules: []gwapiv1b1.HTTPRouteRule{ + { + Matches: []gwapiv1b1.HTTPRouteMatch{ + { + Path: &gwapiv1b1.HTTPPathMatch{ + Type: gatewayapi.PathMatchTypePtr(gwapiv1b1.PathMatchPathPrefix), + Value: gatewayapi.StringPtr("/"), + }, + }, + }, + Filters: []gwapiv1b1.HTTPRouteFilter{ + { + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: gwapiv1b1.Group(egv1a1.GroupVersion.Group), + Kind: gwapiv1b1.Kind(egv1a1.KindRateLimitFilter), + Name: gwapiv1b1.ObjectName("test"), + }, + }, + }, + BackendRefs: []gwapiv1b1.HTTPBackendRef{ + { + BackendRef: gwapiv1b1.BackendRef{ + BackendObjectReference: gwapiv1b1.BackendObjectReference{ + Group: gatewayapi.GroupPtr(corev1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindService), + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + rateLimitFilters: []*egv1a1.RateLimitFilter{ + { + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindRateLimitFilter, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.RateLimitFilterSpec{ + Type: egv1a1.GlobalRateLimitType, + Global: &egv1a1.GlobalRateLimit{ + Rules: []egv1a1.RateLimitRule{ + { + ClientSelectors: []egv1a1.RateLimitSelectCondition{ + { + Headers: []egv1a1.HeaderMatch{ + { + Name: "x-user-id", + Value: gatewayapi.StringPtr("one"), + }, + }, + }, + }, + Limit: egv1a1.RateLimitValue{ + Requests: 5, + Unit: "Second", + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, } for i := range testCases { @@ -209,7 +299,10 @@ func TestProcessHTTPRoutes(t *testing.T) { for _, route := range tc.routes { objs = append(objs, route) } - for _, filter := range tc.filters { + for _, filter := range tc.authenFilters { + objs = append(objs, filter) + } + for _, filter := range tc.rateLimitFilters { objs = append(objs, filter) } r.client = fakeclient.NewClientBuilder(). @@ -226,8 +319,8 @@ func TestProcessHTTPRoutes(t *testing.T) { require.NoError(t, err) // Ensure the resource tree and map are as expected. require.Equal(t, tc.routes, resourceTree.HTTPRoutes) - if tc.filters != nil { - for i, filter := range tc.filters { + if tc.authenFilters != nil { + for i, filter := range tc.authenFilters { key := types.NamespacedName{ // The AuthenticationFilter must be in the same namespace as the HTTPRoute. Namespace: tc.routes[i].Namespace, @@ -236,6 +329,16 @@ func TestProcessHTTPRoutes(t *testing.T) { require.Equal(t, filter, resourceMap.authenFilters[key]) } } + if tc.rateLimitFilters != nil { + for i, filter := range tc.rateLimitFilters { + key := types.NamespacedName{ + // The RateLimitFilter must be in the same namespace as the HTTPRoute. + Namespace: tc.routes[i].Namespace, + Name: filter.Name, + } + require.Equal(t, filter, resourceMap.rateLimitFilters[key]) + } + } } else { require.Error(t, err) } diff --git a/internal/provider/kubernetes/test/utils.go b/internal/provider/kubernetes/test/utils.go index add8c4b5be9..11585295edf 100644 --- a/internal/provider/kubernetes/test/utils.go +++ b/internal/provider/kubernetes/test/utils.go @@ -312,3 +312,40 @@ func GetAuthenticationFilter(name, ns string) *egv1a1.AuthenticationFilter { }, } } + +// GetRateLimitFilter returns a pointer to an RateLimitFilter with dummy rules. +func GetRateLimitFilter(name, ns string) *egv1a1.RateLimitFilter { + return &egv1a1.RateLimitFilter{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindRateLimitFilter, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + Spec: egv1a1.RateLimitFilterSpec{ + Type: egv1a1.GlobalRateLimitType, + Global: &egv1a1.GlobalRateLimit{ + Rules: []egv1a1.RateLimitRule{ + { + ClientSelectors: []egv1a1.RateLimitSelectCondition{ + { + Headers: []egv1a1.HeaderMatch{ + { + Name: "x-user-id", + Value: gatewayapi.StringPtr("one"), + }, + }, + }, + }, + Limit: egv1a1.RateLimitValue{ + Requests: 5, + Unit: "Second", + }, + }, + }, + }, + }, + } +}