diff --git a/controllers/rate_limiting_wasmplugin_controller.go b/controllers/rate_limiting_wasmplugin_controller.go index 9b2787a61..9537234af 100644 --- a/controllers/rate_limiting_wasmplugin_controller.go +++ b/controllers/rate_limiting_wasmplugin_controller.go @@ -193,11 +193,11 @@ func (r *RateLimitingWASMPluginReconciler) wasmPluginConfig(ctx context.Context, // TODO(eastizle): Sort RLPs by name for consistent comparison with existing objects?? - for _, rlp := range rateLimitPolicies { + for _, policy := range rateLimitPolicies { + rlp := policy.(*kuadrantv1beta2.RateLimitPolicy) wasmRLP := r.WASMRateLimitPolicy(gatewayAPITopology, rlp, gw) if wasmRLP == nil { - // no need to add the policy if there are no rules; - // a rlp can return no rules if all its limits fail to match any route rule + // skip this RLP continue } @@ -207,11 +207,18 @@ func (r *RateLimitingWASMPluginReconciler) wasmPluginConfig(ctx context.Context, return wasmPlugin, nil } -func (r *RateLimitingWASMPluginReconciler) WASMRateLimitPolicy(t *common.KuadrantTopology, rlp *kuadrantv1beta2.RateLimitPolicy, gw *gatewayapiv1beta1.Gateway) []rlptools.Rule { - gwHostnames := common.TargetHostnames(gw) +func (r *RateLimitingWASMPluginReconciler) WASMRateLimitPolicy(t *common.KuadrantTopology, rlp *kuadrantv1beta2.RateLimitPolicy, gw *gatewayapiv1beta1.Gateway) *wasm.RateLimitPolicy { + gwHostnamesTmp := common.TargetHostnames(gw) + gwHostnames := common.Map(gwHostnamesTmp, func(str string) gatewayapiv1beta1.Hostname { return gatewayapiv1beta1.Hostname(str) }) route := r.RouteFromRLP(t, rlp, gw) + rules := rlptools.WasmRules(rlp, route) + if len(rules) == 0 { + // no need to add the policy if there are no rules; a rlp can return no rules if all its limits fail to match any route rule + return nil + } + // narrow the list of hostnames specified in the route so we don't generate wasm rules that only apply to other gateways // this is a no-op for the gateway rlp hostnames := common.FilterValidSubdomains(gwHostnames, route.Spec.Hostnames) @@ -219,17 +226,13 @@ func (r *RateLimitingWASMPluginReconciler) WASMRateLimitPolicy(t *common.Kuadran hostnames = gwHostnames } - // gwHostnames := common.Map(gwHostnamesTmp, func(str string) gatewayapiv1beta1.Hostname { return gatewayapiv1beta1.Hostname(str) }) - - wasmRLP := &wasm.RateLimitPolicy{ - Name: client.ObjectKeyFromObject(rlp).String(), - Domain: rlptools.LimitsNamespaceFromRLP(rlp), - Hostnames: common.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive - Service: common.KuadrantRateLimitClusterName, - Rules: nil, - } - - + return &wasm.RateLimitPolicy{ + Name: client.ObjectKeyFromObject(rlp).String(), + Domain: rlptools.LimitsNamespaceFromRLP(rlp), + Hostnames: common.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive + Service: common.KuadrantRateLimitClusterName, + Rules: rules, + } } func (r *RateLimitingWASMPluginReconciler) RouteFromRLP(t *common.KuadrantTopology, rlp *kuadrantv1beta2.RateLimitPolicy, gw *gatewayapiv1beta1.Gateway) *gatewayapiv1beta1.HTTPRoute { @@ -248,8 +251,11 @@ func (r *RateLimitingWASMPluginReconciler) RouteFromRLP(t *common.KuadrantTopolo freeRules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) for idx := range freeRoutes { freeroute := freeRoutes[idx] - freeRules = append(freeRules, route.Spec.Rules...) + freeRules = append(freeRules, freeroute.Spec.Rules...) } + + gwHostnamesTmp := common.TargetHostnames(gw) + gwHostnames := common.Map(gwHostnamesTmp, func(str string) gatewayapiv1beta1.Hostname { return gatewayapiv1beta1.Hostname(str) }) route = &gatewayapiv1beta1.HTTPRoute{ Spec: gatewayapiv1beta1.HTTPRouteSpec{ Hostnames: gwHostnames, @@ -261,145 +267,6 @@ func (r *RateLimitingWASMPluginReconciler) RouteFromRLP(t *common.KuadrantTopolo return route } - - - wasmRules := rlptools.WasmRules(rlp, route) - if len(wasmRules) == 0 { - // no need to add the policy if there are no rules; - // a rlp can return no rules if all its limits fail to match any route rule - continue - } - - wasmPlugin.RateLimitPolicies = append(wasmPlugin.RateLimitPolicies, wasm.RateLimitPolicy{ - Name: client.ObjectKeyFromObject(rlp).String(), - Domain: rlptools.LimitsNamespaceFromRLP(rlp), - Rules: wasmRules, - Hostnames: common.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive - Service: common.KuadrantRateLimitClusterName, - }) - - } - - return wasmPlugin, nil -} - - -// SHOULD BE DELETED -func (r *RateLimitingWASMPluginReconciler) wasmPluginConfigTOBEDELETED(ctx context.Context, gw common.GatewayWrapper, rlpRefs []client.ObjectKey) (*wasm.Plugin, error) { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("wasmPluginConfig").WithValues("gateway", gw.Key()) - - type store struct { - rlp kuadrantv1beta2.RateLimitPolicy - route gatewayapiv1beta1.HTTPRoute - skip bool - } - rlps := make(map[string]*store, len(rlpRefs)) - routeKeys := make(map[string]struct{}, 0) - var gwRLPKey string - - // store all rlps and find the one that targets the gateway (if there is one) - for _, rlpKey := range rlpRefs { - rlp := &kuadrantv1beta2.RateLimitPolicy{} - err := r.Client().Get(ctx, rlpKey, rlp) - logger.V(1).Info("get rlp", "ratelimitpolicy", rlpKey, "err", err) - if err != nil { - return nil, err - } - - // target ref is a HTTPRoute - if common.IsTargetRefHTTPRoute(rlp.Spec.TargetRef) { - route, err := r.FetchValidHTTPRoute(ctx, rlp.TargetKey()) - if err != nil { - return nil, err - } - rlps[rlpKey.String()] = &store{rlp: *rlp, route: *route} - routeKeys[client.ObjectKeyFromObject(route).String()] = struct{}{} - continue - } - - // target ref is a Gateway - if rlps[rlpKey.String()] != nil { - return nil, fmt.Errorf("wasmPluginConfig: multiple gateway RLP found and only one expected. rlp keys: %v", rlpRefs) - } - gwRLPKey = rlpKey.String() - rlps[gwRLPKey] = &store{rlp: *rlp} - } - - gwHostnames := gw.Hostnames() - if len(gwHostnames) == 0 { - gwHostnames = []gatewayapiv1beta1.Hostname{"*"} - } - - // if there is a gateway rlp, fake a single httproute with all rules from all httproutes accepted by the gateway, - // that do not have a rlp of its own, so we can generate wasm rules for those cases - if gwRLPKey != "" { - rules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) - routes := r.FetchAcceptedGatewayHTTPRoutes(ctx, rlps[gwRLPKey].rlp.TargetKey()) - for idx := range routes { - route := routes[idx] - // skip routes that have a rlp of its own - if _, found := routeKeys[client.ObjectKeyFromObject(&route).String()]; found { - continue - } - rules = append(rules, route.Spec.Rules...) - } - if len(rules) == 0 { - logger.V(1).Info("no httproutes attached to the targeted gateway, skipping wasm config for the gateway rlp", "ratelimitpolicy", gwRLPKey) - rlps[gwRLPKey].skip = true - } else { - rlps[gwRLPKey].route = gatewayapiv1beta1.HTTPRoute{ - Spec: gatewayapiv1beta1.HTTPRouteSpec{ - Hostnames: gwHostnames, - Rules: rules, - }, - } - } - } - - wasmPlugin := &wasm.Plugin{ - FailureMode: wasm.FailureModeDeny, - RateLimitPolicies: make([]wasm.RateLimitPolicy, 0), - } - - for _, rlpKey := range rlpRefs { - s := rlps[rlpKey.String()] - if s.skip { - continue - } - rlp := s.rlp - route := s.route - - // narrow the list of hostnames specified in the route so we don't generate wasm rules that only apply to other gateways - // this is a no-op for the gateway rlp - hostnames := common.FilterValidSubdomains(gwHostnames, route.Spec.Hostnames) - if len(hostnames) == 0 { // it should only happen when the route specifies no hostnames - hostnames = gwHostnames - } - route.Spec.Hostnames = hostnames - - rules := rlptools.WasmRules(&rlp, &route) - if len(rules) == 0 { - continue // no need to add the policy if there are no rules; a rlp can return no rules if all its limits fail to match any route rule - } - - wasmPlugin.RateLimitPolicies = append(wasmPlugin.RateLimitPolicies, wasm.RateLimitPolicy{ - Name: rlpKey.String(), - Domain: rlptools.LimitsNamespaceFromRLP(&rlp), - Rules: rules, - Hostnames: common.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive - Service: common.KuadrantRateLimitClusterName, - }) - } - - // avoid building a wasm plugin config if there are no rules to apply - if len(wasmPlugin.RateLimitPolicies) == 0 { - return nil, nil - } - - return wasmPlugin, nil -} - // SetupWithManager sets up the controller with the Manager. func (r *RateLimitingWASMPluginReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/pkg/common/kuadrant_topology.go b/pkg/common/kuadrant_topology.go index 8ae8f1b76..676de1c82 100644 --- a/pkg/common/kuadrant_topology.go +++ b/pkg/common/kuadrant_topology.go @@ -1,28 +1,173 @@ package common import ( + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) type KuadrantTopology struct { + // Gateway -> []Policy + gatewayPolicies map[client.ObjectKey][]KuadrantPolicy + + // Policy -> HTTPRoute + policyRoute map[client.ObjectKey]*gatewayapiv1beta1.HTTPRoute + + // Gateway -> []HTTPRoute (routes not targeted directly by any policy) + freeRoutes map[client.ObjectKey][]*gatewayapiv1beta1.HTTPRoute } -func NewKuadrantTopology(gateways []*gatewayapiv1beta1.Gateway, routes []*gatewayapiv1beta1.HTTPRoute, policy []KuadrantPolicy) *KuadrantTopology { - // TODO - return &KuadrantTopology{} +func NewKuadrantTopology(gateways []*gatewayapiv1beta1.Gateway, routes []*gatewayapiv1beta1.HTTPRoute, policies []KuadrantPolicy) *KuadrantTopology { + t := NewGatewayAPITopology(gateways, routes) + + return &KuadrantTopology{ + gatewayPolicies: buildGatewayPoliciesIndex(t, policies), + policyRoute: buildPolicyRouteIndex(t, policies), + freeRoutes: buildFreeRoutesIndex(t, policies), + } } func (k *KuadrantTopology) PoliciesFromGateway(gateway *gatewayapiv1beta1.Gateway) []KuadrantPolicy { + return k.gatewayPolicies[client.ObjectKeyFromObject(gateway)] +} + +func (k *KuadrantTopology) GetPolicyHTTPRoute(policy KuadrantPolicy) *gatewayapiv1beta1.HTTPRoute { + return k.policyRoute[client.ObjectKeyFromObject(policy)] +} + +func (k *KuadrantTopology) GetFreeRoutes(gateway *gatewayapiv1beta1.Gateway) []*gatewayapiv1beta1.HTTPRoute { + return k.freeRoutes[client.ObjectKeyFromObject(gateway)] +} + +func buildGatewayPoliciesIndex(t *gatewayAPITopology, policies []KuadrantPolicy) map[client.ObjectKey][]KuadrantPolicy { + // Build Gateway -> []Policy index with all the policies affecting the indexed gateway + index := make(map[client.ObjectKey][]KuadrantPolicy, 0) + + for _, policy := range policies { + if gateway := t.GatewayFromPolicy(policy); gateway != nil { + // policy targeting a gateway + policies := index[client.ObjectKeyFromObject(gateway)] + policies = append(policies, policy) + index[client.ObjectKeyFromObject(gateway)] = policies + } else if route := t.RouteFromPolicy(policy); route != nil { + // policy targeting a route + routeParents := t.GetRouteParents(route) + for _, routeParent := range routeParents { + policies := index[client.ObjectKeyFromObject(routeParent)] + policies = append(policies, policy) + index[client.ObjectKeyFromObject(routeParent)] = policies + } + } + + // skipping the policy as it does not target neither a valid route nor a valid gateway + } + + return index +} + +func buildPolicyRouteIndex(t *gatewayAPITopology, policies []KuadrantPolicy) map[client.ObjectKey]*gatewayapiv1beta1.HTTPRoute { + // Build Policy -> HTTPRoute index with the route targeted by the indexed policy + index := make(map[client.ObjectKey]*gatewayapiv1beta1.HTTPRoute, 0) + for _, policy := range policies { + if route := t.RouteFromPolicy(policy); route != nil { + index[client.ObjectKeyFromObject(policy)] = route + } + } + + return index +} + +func buildFreeRoutesIndex(t *gatewayAPITopology, policies []KuadrantPolicy) map[client.ObjectKey][]*gatewayapiv1beta1.HTTPRoute { + // Build Gateway -> []HTTPRoute index with all the routes not targeted by a policy + index := make(map[client.ObjectKey][]*gatewayapiv1beta1.HTTPRoute, 0) + + routesTargetedByPolicy := make(map[client.ObjectKey]bool, 0) + for _, policy := range policies { + if route := t.RouteFromPolicy(policy); route != nil { + routesTargetedByPolicy[client.ObjectKeyFromObject(route)] = true + } + } + + for _, gateway := range t.GetGateways() { + var gatewayFreeRoutes []*gatewayapiv1beta1.HTTPRoute + for _, route := range t.GetGatewayRoutes(gateway) { + if !routesTargetedByPolicy[client.ObjectKeyFromObject(route)] { + gatewayFreeRoutes = append(gatewayFreeRoutes, route) + } + } + + if len(gatewayFreeRoutes) > 0 { + index[client.ObjectKeyFromObject(gateway)] = gatewayFreeRoutes + } + } + + return index +} + +type gatewayAPITopology struct { + gatewaysIndex map[client.ObjectKey]*gatewayapiv1beta1.Gateway + routesIndex map[client.ObjectKey]*gatewayapiv1beta1.HTTPRoute +} + +func NewGatewayAPITopology(gateways []*gatewayapiv1beta1.Gateway, routes []*gatewayapiv1beta1.HTTPRoute) *gatewayAPITopology { + // First build topology with them all + // TODO Secondly, just valid gateways and accepted HTTPRoutes + + gatewaysIndex := make(map[client.ObjectKey]*gatewayapiv1beta1.Gateway, 0) + for _, gateway := range gateways { + gatewaysIndex[client.ObjectKeyFromObject(gateway)] = gateway + } + + routesIndex := make(map[client.ObjectKey]*gatewayapiv1beta1.HTTPRoute, 0) + for _, route := range routes { + routesIndex[client.ObjectKeyFromObject(route)] = route + } + + return &gatewayAPITopology{ + gatewaysIndex: gatewaysIndex, + routesIndex: routesIndex, + } +} + +func (g *gatewayAPITopology) GatewayFromPolicy(policy KuadrantPolicy) *gatewayapiv1beta1.Gateway { + // Return gateway targeted by the kuadrant policy + + if !IsTargetRefGateway(policy.GetTargetRef()) { + return nil + } + + namespace := string(ptr.Deref(policy.GetTargetRef().Namespace, policy.GetWrappedNamespace())) + + gwKey := client.ObjectKey{Name: string(policy.GetTargetRef().Name), Namespace: namespace} + + return g.gatewaysIndex[gwKey] +} + +func (g *gatewayAPITopology) RouteFromPolicy(policy KuadrantPolicy) *gatewayapiv1beta1.HTTPRoute { + // Return route targeted by the kuadrant policy + + if !IsTargetRefHTTPRoute(policy.GetTargetRef()) { + return nil + } + + namespace := string(ptr.Deref(policy.GetTargetRef().Namespace, policy.GetWrappedNamespace())) + + routeKey := client.ObjectKey{Name: string(policy.GetTargetRef().Name), Namespace: namespace} + + return g.routesIndex[routeKey] +} + +func (g *gatewayAPITopology) GetRouteParents(route *gatewayapiv1beta1.HTTPRoute) []*gatewayapiv1beta1.Gateway { // TODO return nil } -func (k *KuadrantTopology) GetPolicyHTTPRoute(policy KuadrantPolicy) *gatewayapiv1beta1.HTTPRoute { +func (g *gatewayAPITopology) GetGateways() []*gatewayapiv1beta1.Gateway { // TODO return nil } -func (k *KuadrantTopology) GetFreeRoutes(gateway *gatewayapiv1beta1.Gateway) []*gatewayapiv1beta1.HTTPRoute { +func (g *gatewayAPITopology) GetGatewayRoutes(gateway *gatewayapiv1beta1.Gateway) []*gatewayapiv1beta1.HTTPRoute { // TODO return nil }