diff --git a/api/v1/merge_strategies.go b/api/v1/merge_strategies.go new file mode 100644 index 000000000..09977500f --- /dev/null +++ b/api/v1/merge_strategies.go @@ -0,0 +1,200 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "sort" + "strings" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/runtime" + k8stypes "k8s.io/apimachinery/pkg/types" +) + +const ( + AtomicMergeStrategy = "atomic" + PolicyRuleMergeStrategy = "merge" +) + +type MergeableRule struct { + Spec any + Source string +} + +// +kubebuilder:object:generate=false +type MergeablePolicy interface { + machinery.Policy + + Rules() map[string]MergeableRule + SetRules(map[string]MergeableRule) + Empty() bool + + DeepCopyObject() runtime.Object +} + +// AtomicDefaultsMergeStrategy implements a merge strategy that returns the target Policy if it exists, +// otherwise it returns the source Policy. +func AtomicDefaultsMergeStrategy(source, target machinery.Policy) machinery.Policy { + if source == nil { + return target + } + if target == nil { + return source + } + + mergeableTargetPolicy := target.(MergeablePolicy) + + if !mergeableTargetPolicy.Empty() { + return mergeableTargetPolicy.DeepCopyObject().(machinery.Policy) + } + + return source.(MergeablePolicy).DeepCopyObject().(machinery.Policy) +} + +var _ machinery.MergeStrategy = AtomicDefaultsMergeStrategy + +// AtomicOverridesMergeStrategy implements a merge strategy that overrides a target Policy with +// a source one. +func AtomicOverridesMergeStrategy(source, _ machinery.Policy) machinery.Policy { + if source == nil { + return nil + } + return source.(MergeablePolicy).DeepCopyObject().(machinery.Policy) +} + +var _ machinery.MergeStrategy = AtomicOverridesMergeStrategy + +// PolicyRuleDefaultsMergeStrategy implements a merge strategy that merges a source Policy into a target one +// by keeping the policy rules from the target and adding the ones from the source that do not exist in the target. +func PolicyRuleDefaultsMergeStrategy(source, target machinery.Policy) machinery.Policy { + if source == nil { + return target + } + if target == nil { + return source + } + + sourceMergeablePolicy := source.(MergeablePolicy) + targetMergeablePolicy := target.(MergeablePolicy) + + // copy rules from the target + rules := targetMergeablePolicy.Rules() + + // add extra rules from the source + for ruleID, rule := range sourceMergeablePolicy.Rules() { + if _, ok := targetMergeablePolicy.Rules()[ruleID]; !ok { + rules[ruleID] = MergeableRule{ + Spec: rule.Spec, + Source: source.GetLocator(), + } + } + } + + mergedPolicy := targetMergeablePolicy.DeepCopyObject().(MergeablePolicy) + mergedPolicy.SetRules(rules) + return mergedPolicy +} + +var _ machinery.MergeStrategy = PolicyRuleDefaultsMergeStrategy + +// PolicyRuleOverridesMergeStrategy implements a merge strategy that merges a source Policy into a target one +// by using the policy rules from the source and keeping from the target only the policy rules that do not exist in +// the source. +func PolicyRuleOverridesMergeStrategy(source, target machinery.Policy) machinery.Policy { + sourceMergeablePolicy := source.(MergeablePolicy) + targetMergeablePolicy := target.(MergeablePolicy) + + // copy rules from the source + rules := sourceMergeablePolicy.Rules() + + // add extra rules from the target + for ruleID, rule := range targetMergeablePolicy.Rules() { + if _, ok := sourceMergeablePolicy.Rules()[ruleID]; !ok { + rules[ruleID] = rule + } + } + + mergedPolicy := targetMergeablePolicy.DeepCopyObject().(MergeablePolicy) + mergedPolicy.SetRules(rules) + return mergedPolicy +} + +var _ machinery.MergeStrategy = PolicyRuleOverridesMergeStrategy + +func DefaultsMergeStrategy(strategy string) machinery.MergeStrategy { + switch strategy { + case AtomicMergeStrategy: + return AtomicDefaultsMergeStrategy + case PolicyRuleMergeStrategy: + return PolicyRuleDefaultsMergeStrategy + default: + return AtomicDefaultsMergeStrategy + } +} + +func OverridesMergeStrategy(strategy string) machinery.MergeStrategy { + switch strategy { + case AtomicMergeStrategy: + return AtomicOverridesMergeStrategy + case PolicyRuleMergeStrategy: + return PolicyRuleOverridesMergeStrategy + default: + return AtomicOverridesMergeStrategy + } +} + +// EffectivePolicyForPath returns the effective policy for a given path, merging all policies in the path. +// The policies in the path are sorted from the least specific to the most specific. +// Only policies whose predicate returns true are considered. +func EffectivePolicyForPath[T machinery.Policy](path []machinery.Targetable, predicate func(machinery.Policy) bool) *T { + policies := PoliciesInPath(path, predicate) + if len(policies) == 0 { + return nil + } + + // map reduces the policies from most specific to least specific, merging them into one effective policy + effectivePolicy := lo.ReduceRight(policies, func(effectivePolicy machinery.Policy, policy machinery.Policy, _ int) machinery.Policy { + return effectivePolicy.Merge(policy) + }, policies[len(policies)-1]) + + concreteEffectivePolicy, _ := effectivePolicy.(T) + return &concreteEffectivePolicy +} + +// OrderedPoliciesForPath gathers all policies in a path sorted from the least specific to the most specific. +// Only policies whose predicate returns true are considered. +func PoliciesInPath(path []machinery.Targetable, predicate func(machinery.Policy) bool) []machinery.Policy { + return lo.FlatMap(path, func(targetable machinery.Targetable, _ int) []machinery.Policy { + policies := lo.FilterMap(targetable.Policies(), func(policy machinery.Policy, _ int) (controller.Object, bool) { + o, object := policy.(controller.Object) + return o, object && predicate(policy) + }) + sort.Sort(controller.ObjectsByCreationTimestamp(policies)) + return lo.Map(policies, func(policy controller.Object, _ int) machinery.Policy { + p, _ := policy.(machinery.Policy) + return p + }) + }) +} + +func PathID(path []machinery.Targetable) string { + return strings.Join(lo.Map(path, func(t machinery.Targetable, _ int) string { + return strings.TrimPrefix(k8stypes.NamespacedName{Namespace: t.GetNamespace(), Name: t.GetName()}.String(), string(k8stypes.Separator)) + }), "|") +} diff --git a/api/v1beta3/groupversion_info.go b/api/v1beta3/groupversion_info.go index 518ca630c..bca312b26 100644 --- a/api/v1beta3/groupversion_info.go +++ b/api/v1beta3/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright 2021. +Copyright 2024. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1beta3 contains API Schema definitions for the kuadrant v1beta3 API group +// API schema definitions for the Kuadrant v1beta3 API group // +kubebuilder:object:generate=true // +groupName=kuadrant.io package v1beta3 import ( "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" + ctrl "sigs.k8s.io/controller-runtime/pkg/scheme" ) -var ( - // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "kuadrant.io", Version: "v1beta3"} +// GroupName specifies the group name used to register the objects. +const GroupName = "kuadrant.io" - // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} +// GroupVersion specifies the group and the version used to register the objects. +var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta3"} - // AddToScheme adds the types in this group-version to the given scheme. - AddToScheme = SchemeBuilder.AddToScheme -) +// SchemeGroupVersion is group version used to register these objects +// Deprecated: use GroupVersion instead. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta3"} + +// SchemeBuilder is used to add go types to the GroupVersionKind scheme +var SchemeBuilder = &ctrl.Builder{GroupVersion: GroupVersion} + +// AddToScheme adds the types in this group-version to the given scheme. +var AddToScheme = SchemeBuilder.AddToScheme + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/api/v1beta3/ratelimitpolicy_types.go b/api/v1beta3/ratelimitpolicy_types.go index 587ae0dbf..4c4adecdf 100644 --- a/api/v1beta3/ratelimitpolicy_types.go +++ b/api/v1beta3/ratelimitpolicy_types.go @@ -1,5 +1,5 @@ /* -Copyright 2021 Red Hat, Inc. +Copyright 2024. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,47 +17,20 @@ limitations under the License. package v1beta3 import ( - "context" - "fmt" + "encoding/json" - "github.com/go-logr/logr" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" + "github.com/kuadrant/policy-machinery/machinery" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - -var ( - RateLimitPolicyGVK schema.GroupVersionKind = schema.GroupVersionKind{ - Group: GroupVersion.Group, - Version: GroupVersion.Version, - Kind: "RateLimitPolicy", - } -) - -// ContextSelector defines one item from the well known attributes -// Attributes: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes -// Well-known selectors: https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors -// They are named by a dot-separated path (e.g. request.path) -// Example: "request.path" -> The path portion of the URL -// +kubebuilder:validation:MinLength=1 -// +kubebuilder:validation:MaxLength=253 -type ContextSelector string - -// +kubebuilder:validation:Enum:=eq;neq;startswith;endswith;incl;excl;matches -type WhenConditionOperator string - const ( EqualOperator WhenConditionOperator = "eq" NotEqualOperator WhenConditionOperator = "neq" @@ -67,271 +40,372 @@ const ( ExcludeOperator WhenConditionOperator = "excl" MatchesOperator WhenConditionOperator = "matches" + // TODO: remove after fixing the integration tests that still depend on these RateLimitPolicyBackReferenceAnnotationName = "kuadrant.io/ratelimitpolicies" RateLimitPolicyDirectReferenceAnnotationName = "kuadrant.io/ratelimitpolicy" ) -// +kubebuilder:validation:Enum:=second;minute;hour;day -type TimeUnit string +var ( + RateLimitPolicyGroupKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "RateLimitPolicy"} + RateLimitPoliciesResource = SchemeGroupVersion.WithResource("ratelimitpolicies") +) -// Rate defines the actual rate limit that will be used when there is a match -type Rate struct { - // Limit defines the max value allowed for a given period of time - Limit int `json:"limit"` +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" +// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=`.status.conditions[?(@.type=="Accepted")].status`,description="RateLimitPolicy Accepted",priority=2 +// +kubebuilder:printcolumn:name="Enforced",type=string,JSONPath=`.status.conditions[?(@.type=="Enforced")].status`,description="RateLimitPolicy Enforced",priority=2 +// +kubebuilder:printcolumn:name="TargetKind",type="string",JSONPath=".spec.targetRef.kind",description="Kind of the object to which the policy aaplies",priority=2 +// +kubebuilder:printcolumn:name="TargetName",type="string",JSONPath=".spec.targetRef.name",description="Name of the object to which the policy applies",priority=2 +// +kubebuilder:printcolumn:name="TargetSection",type="string",JSONPath=".spec.targetRef.sectionName",description="Name of the section within the object to which the policy applies ",priority=2 +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" - // Duration defines the time period for which the Limit specified above applies. - Duration int `json:"duration"` +// RateLimitPolicy enables rate limiting for service workloads in a Gateway API network +type RateLimitPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` - // Duration defines the time uni - // Possible values are: "second", "minute", "hour", "day" - Unit TimeUnit `json:"unit"` + Spec RateLimitPolicySpec `json:"spec,omitempty"` + Status RateLimitPolicyStatus `json:"status,omitempty"` } -// WhenCondition defines semantics for matching an HTTP request based on conditions -// https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteSpec -type WhenCondition struct { - // Selector defines one item from the well known selectors - // TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors - Selector ContextSelector `json:"selector"` - - // The binary operator to be applied to the content fetched from the selector - // Possible values are: "eq" (equal to), "neq" (not equal to) - Operator WhenConditionOperator `json:"operator"` +var _ machinery.Policy = &RateLimitPolicy{} - // The value of reference for the comparison. - Value string `json:"value"` +func (p *RateLimitPolicy) GetNamespace() string { + return p.Namespace } -// Limit represents a complete rate limit configuration -type Limit struct { - // When holds the list of conditions for the policy to be enforced. - // Called also "soft" conditions as route selectors must also match - // +optional - When []WhenCondition `json:"when,omitempty"` +func (p *RateLimitPolicy) GetName() string { + return p.Name +} - // Counters defines additional rate limit counters based on context qualifiers and well known selectors - // TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors - // +optional - Counters []ContextSelector `json:"counters,omitempty"` +func (p *RateLimitPolicy) GetLocator() string { + return machinery.LocatorFromObject(p) +} - // Rates holds the list of limit rates - // +optional - Rates []Rate `json:"rates,omitempty"` +// DEPRECATED: Use GetTargetRefs instead +func (p *RateLimitPolicy) GetTargetRef() gatewayapiv1alpha2.LocalPolicyTargetReference { + return p.Spec.TargetRef.LocalPolicyTargetReference } -func (l Limit) CountersAsStringList() []string { - if len(l.Counters) == 0 { - return nil +func (p *RateLimitPolicy) GetTargetRefs() []machinery.PolicyTargetReference { + return []machinery.PolicyTargetReference{ + machinery.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReferenceWithSectionName: p.Spec.TargetRef, + PolicyNamespace: p.Namespace, + }, } - return utils.Map(l.Counters, func(counter ContextSelector) string { return string(counter) }) } -// RateLimitPolicySpec defines the desired state of RateLimitPolicy -// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && has(self.limits))",message="Implicit and explicit defaults are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && has(self.overrides))",message="Overrides and explicit defaults are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && has(self.limits))",message="Overrides and implicit defaults are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && self.targetRef.kind != 'Gateway')",message="Overrides are only allowed for policies targeting a Gateway resource" -type RateLimitPolicySpec struct { - // TargetRef identifies an API object to apply policy to. - // +kubebuilder:validation:XValidation:rule="self.group == 'gateway.networking.k8s.io'",message="Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io'" - // +kubebuilder:validation:XValidation:rule="self.kind == 'HTTPRoute' || self.kind == 'Gateway'",message="Invalid targetRef.kind. The only supported values are 'HTTPRoute' and 'Gateway'" - TargetRef gatewayapiv1alpha2.LocalPolicyTargetReference `json:"targetRef"` - - // Defaults define explicit default values for this policy and for policies inheriting this policy. - // Defaults are mutually exclusive with implicit defaults defined by RateLimitPolicyCommonSpec. - // +optional - Defaults *RateLimitPolicyCommonSpec `json:"defaults,omitempty"` - - // Overrides define override values for this policy and for policies inheriting this policy. - // Overrides are mutually exclusive with implicit defaults and explicit Defaults defined by RateLimitPolicyCommonSpec. - // +optional - Overrides *RateLimitPolicyCommonSpec `json:"overrides,omitempty"` - - // RateLimitPolicyCommonSpec defines implicit default values for this policy and for policies inheriting this policy. - // RateLimitPolicyCommonSpec is mutually exclusive with explicit defaults defined by Defaults. - RateLimitPolicyCommonSpec `json:""` +func (p *RateLimitPolicy) GetMergeStrategy() machinery.MergeStrategy { + if spec := p.Spec.Defaults; spec != nil { + return kuadrantv1.DefaultsMergeStrategy(spec.Strategy) + } + if spec := p.Spec.Overrides; spec != nil { + return kuadrantv1.OverridesMergeStrategy(spec.Strategy) + } + return kuadrantv1.AtomicDefaultsMergeStrategy } -// RateLimitPolicyCommonSpec contains common shared fields. -type RateLimitPolicyCommonSpec struct { - // Limits holds the struct of limits indexed by a unique name - // +optional - // +kubebuilder:validation:MaxProperties=14 - Limits map[string]Limit `json:"limits,omitempty"` +func (p *RateLimitPolicy) Merge(other machinery.Policy) machinery.Policy { + source, ok := other.(*RateLimitPolicy) + if !ok { + return p + } + return source.GetMergeStrategy()(source, p) } -// RateLimitPolicyStatus defines the observed state of RateLimitPolicy -type RateLimitPolicyStatus struct { - reconcilers.StatusMeta `json:",inline"` +var _ kuadrantv1.MergeablePolicy = &RateLimitPolicy{} - // Represents the observations of a foo's current state. - // Known .status.conditions.type are: "Available" - // +patchMergeKey=type - // +patchStrategy=merge - // +listType=map - // +listMapKey=type - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +func (p *RateLimitPolicy) Empty() bool { + return len(p.Spec.Proper().Limits) == 0 } -func RateLimitPolicyStatusMutator(desiredStatus *RateLimitPolicyStatus, logger logr.Logger) reconcilers.StatusMutatorFunc { - return func(obj client.Object) (bool, error) { - existingRLP, ok := obj.(*RateLimitPolicy) - if !ok { - return false, fmt.Errorf("unsupported object type %T", obj) - } +func (p *RateLimitPolicy) Rules() map[string]kuadrantv1.MergeableRule { + rules := make(map[string]kuadrantv1.MergeableRule) - opts := cmp.Options{ - cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"), - cmpopts.IgnoreMapEntries(func(k string, _ any) bool { - return k == "lastTransitionTime" - }), + for ruleID := range p.Spec.Proper().Limits { + limit := p.Spec.Proper().Limits[ruleID] + origin := limit.Origin + if origin == "" { + origin = p.GetLocator() } - - if cmp.Equal(*desiredStatus, existingRLP.Status, opts) { - return false, nil + rules[ruleID] = kuadrantv1.MergeableRule{ + Spec: limit, + Source: origin, } + } - if logger.V(1).Enabled() { - diff := cmp.Diff(*desiredStatus, existingRLP.Status, opts) - logger.V(1).Info("status not equal", "difference", diff) - } + return rules +} - existingRLP.Status = *desiredStatus +func (p *RateLimitPolicy) SetRules(rules map[string]kuadrantv1.MergeableRule) { + if len(rules) > 0 && p.Spec.Proper().Limits == nil { + p.Spec.Proper().Limits = make(map[string]Limit) + } - return true, nil + for ruleID := range rules { + rule := rules[ruleID] + limit := rule.Spec.(Limit) + limit.Origin = rule.Source + p.Spec.Proper().Limits[ruleID] = limit } } -func (s *RateLimitPolicyStatus) GetConditions() []metav1.Condition { - return s.Conditions +// DEPRECATED. impl: kuadrant.Policy +func (p *RateLimitPolicy) GetStatus() kuadrantgatewayapi.PolicyStatus { + return &p.Status } -var _ kuadrant.Policy = &RateLimitPolicy{} -var _ kuadrant.Referrer = &RateLimitPolicy{} -var _ kuadrantgatewayapi.Policy = &RateLimitPolicy{} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" -// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=`.status.conditions[?(@.type=="Accepted")].status`,description="RateLimitPolicy Accepted",priority=2 -// +kubebuilder:printcolumn:name="Enforced",type=string,JSONPath=`.status.conditions[?(@.type=="Enforced")].status`,description="RateLimitPolicy Enforced",priority=2 -// +kubebuilder:printcolumn:name="TargetRefKind",type="string",JSONPath=".spec.targetRef.kind",description="Type of the referenced Gateway API resource",priority=2 -// +kubebuilder:printcolumn:name="TargetRefName",type="string",JSONPath=".spec.targetRef.name",description="Name of the referenced Gateway API resource",priority=2 -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" - -// RateLimitPolicy enables rate limiting for service workloads in a Gateway API network -type RateLimitPolicy struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` +// DEPRECATED. impl: kuadrant.Policy +func (p *RateLimitPolicy) PolicyClass() kuadrantgatewayapi.PolicyClass { + return kuadrantgatewayapi.InheritedPolicy +} - Spec RateLimitPolicySpec `json:"spec,omitempty"` - Status RateLimitPolicyStatus `json:"status,omitempty"` +// DEPRECATED. impl: kuadrant.Policy +func (p *RateLimitPolicy) GetWrappedNamespace() gatewayapiv1.Namespace { + return gatewayapiv1.Namespace(p.GetNamespace()) } -func (r *RateLimitPolicy) GetObservedGeneration() int64 { return r.Status.GetObservedGeneration() } -func (r *RateLimitPolicy) SetObservedGeneration(o int64) { r.Status.SetObservedGeneration(o) } +// DEPRECATED. impl: kuadrant.Policy +func (p *RateLimitPolicy) GetRulesHostnames() []string { + return []string{} +} -//+kubebuilder:object:root=true +// DEPRECATED. impl: kuadrant.Policy +func (p *RateLimitPolicy) Kind() string { + return RateLimitPolicyGroupKind.Kind +} -// RateLimitPolicyList contains a list of RateLimitPolicy -type RateLimitPolicyList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []RateLimitPolicy `json:"items"` +// TODO: remove +func (p *RateLimitPolicy) DirectReferenceAnnotationName() string { + return RateLimitPolicyDirectReferenceAnnotationName } -func (l *RateLimitPolicyList) GetItems() []kuadrant.Policy { - return utils.Map(l.Items, func(item RateLimitPolicy) kuadrant.Policy { - return &item - }) +// TODO: remove +func (p *RateLimitPolicy) BackReferenceAnnotationName() string { + return RateLimitPolicyBackReferenceAnnotationName } -func (r *RateLimitPolicy) GetTargetRef() gatewayapiv1alpha2.LocalPolicyTargetReference { - return r.Spec.TargetRef +// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && has(self.limits))",message="Implicit and explicit defaults are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && has(self.overrides))",message="Overrides and explicit defaults are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && has(self.limits))",message="Overrides and implicit defaults are mutually exclusive" +type RateLimitPolicySpec struct { + // Reference to the object to which this policy applies. + // +kubebuilder:validation:XValidation:rule="self.group == 'gateway.networking.k8s.io'",message="Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io'" + // +kubebuilder:validation:XValidation:rule="self.kind == 'HTTPRoute' || self.kind == 'Gateway'",message="Invalid targetRef.kind. The only supported values are 'HTTPRoute' and 'Gateway'" + TargetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRef"` + + // Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + // Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). + // +optional + Defaults *MergeableRateLimitPolicySpec `json:"defaults,omitempty"` + + // Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + // Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). + // +optional + Overrides *MergeableRateLimitPolicySpec `json:"overrides,omitempty"` + + // Bare set of policy rules (implicit defaults). + // Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). + RateLimitPolicySpecProper `json:""` } -func (r *RateLimitPolicy) GetStatus() kuadrantgatewayapi.PolicyStatus { - return &r.Status +// UnmarshalJSON unmarshals the RateLimitPolicySpec from JSON byte array. +// This should not be needed, but runtime.DefaultUnstructuredConverter.FromUnstructured does not work well with embedded structs. +func (s *RateLimitPolicySpec) UnmarshalJSON(j []byte) error { + targetRef := struct { + gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRef"` + }{} + if err := json.Unmarshal(j, &targetRef); err != nil { + return err + } + s.TargetRef = targetRef.LocalPolicyTargetReferenceWithSectionName + + defaults := &struct { + *MergeableRateLimitPolicySpec `json:"defaults,omitempty"` + }{} + if err := json.Unmarshal(j, defaults); err != nil { + return err + } + s.Defaults = defaults.MergeableRateLimitPolicySpec + + overrides := &struct { + *MergeableRateLimitPolicySpec `json:"overrides,omitempty"` + }{} + if err := json.Unmarshal(j, overrides); err != nil { + return err + } + s.Overrides = overrides.MergeableRateLimitPolicySpec + + proper := struct { + RateLimitPolicySpecProper `json:""` + }{} + if err := json.Unmarshal(j, &proper); err != nil { + return err + } + s.RateLimitPolicySpecProper = proper.RateLimitPolicySpecProper + + return nil } -func (r *RateLimitPolicy) GetWrappedNamespace() gatewayapiv1.Namespace { - return gatewayapiv1.Namespace(r.Namespace) +func (s *RateLimitPolicySpec) Proper() *RateLimitPolicySpecProper { + if s.Defaults != nil { + return &s.Defaults.RateLimitPolicySpecProper + } + + if s.Overrides != nil { + return &s.Overrides.RateLimitPolicySpecProper + } + + return &s.RateLimitPolicySpecProper } -func (r *RateLimitPolicy) GetRulesHostnames() (ruleHosts []string) { - ruleHosts = make([]string, 0) - return +type MergeableRateLimitPolicySpec struct { + // Strategy defines the merge strategy to apply when merging this policy with other policies. + // +kubebuilder:validation:Enum=atomic;merge + // +kubebuilder:default=atomic + Strategy string `json:"strategy,omitempty"` + + RateLimitPolicySpecProper `json:""` } -func (r *RateLimitPolicy) Kind() string { - return NewRateLimitPolicyType().GetGVK().Kind +// RateLimitPolicySpecProper contains common shared fields for defaults and overrides +type RateLimitPolicySpecProper struct { + // Limits holds the struct of limits indexed by a unique name + // +optional + Limits map[string]Limit `json:"limits,omitempty"` } -func (r *RateLimitPolicy) TargetProgrammedGatewaysOnly() bool { - return true +// Limit represents a complete rate limit configuration +type Limit struct { + // When holds the list of conditions for the policy to be enforced. + // Called also "soft" conditions as route selectors must also match + // +optional + When []WhenCondition `json:"when,omitempty"` + + // Counters defines additional rate limit counters based on context qualifiers and well known selectors + // TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors + // +optional + Counters []ContextSelector `json:"counters,omitempty"` + + // Rates holds the list of limit rates + // +optional + Rates []Rate `json:"rates,omitempty"` + + // origin stores the resource where the limit is originally defined (internal use) + Origin string `json:"-"` } -func (r *RateLimitPolicy) PolicyClass() kuadrantgatewayapi.PolicyClass { - return kuadrantgatewayapi.InheritedPolicy +func (l Limit) CountersAsStringList() []string { + if len(l.Counters) == 0 { + return nil + } + return utils.Map(l.Counters, func(counter ContextSelector) string { return string(counter) }) } -func (r *RateLimitPolicy) BackReferenceAnnotationName() string { - return NewRateLimitPolicyType().BackReferenceAnnotationName() +// +kubebuilder:validation:Enum:=second;minute;hour;day +type TimeUnit string + +var timeUnitMap = map[TimeUnit]int{ + TimeUnit("second"): 1, + TimeUnit("minute"): 60, + TimeUnit("hour"): 60 * 60, + TimeUnit("day"): 60 * 60 * 24, } -func (r *RateLimitPolicy) DirectReferenceAnnotationName() string { - return NewRateLimitPolicyType().DirectReferenceAnnotationName() +// Rate defines the actual rate limit that will be used when there is a match +type Rate struct { + // Limit defines the max value allowed for a given period of time + Limit int `json:"limit"` + + // Duration defines the time period for which the Limit specified above applies. + Duration int `json:"duration"` + + // Duration defines the time uni + // Possible values are: "second", "minute", "hour", "day" + Unit TimeUnit `json:"unit"` } -// CommonSpec returns the Default RateLimitPolicyCommonSpec if it is defined. -// Otherwise, it returns the RateLimitPolicyCommonSpec from the spec. -// This function should be used instead of accessing the fields directly, so that either the explicit or implicit default -// is returned. -func (r *RateLimitPolicySpec) CommonSpec() *RateLimitPolicyCommonSpec { - if r.Defaults != nil { - return r.Defaults +// ToSeconds converts the rate to to Limitador's Limit format (maxValue, seconds) +func (r Rate) ToSeconds() (maxValue, seconds int) { + maxValue = r.Limit + seconds = 0 + + if tmpSecs, ok := timeUnitMap[r.Unit]; ok && r.Duration > 0 { + seconds = tmpSecs * r.Duration } - if r.Overrides != nil { - return r.Overrides + if r.Duration < 0 { + seconds = 0 } - return &r.RateLimitPolicyCommonSpec + if r.Limit < 0 { + maxValue = 0 + } + + return } -type rateLimitPolicyType struct{} +// WhenCondition defines semantics for matching an HTTP request based on conditions +// https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteSpec +type WhenCondition struct { + // Selector defines one item from the well known selectors + // TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors + Selector ContextSelector `json:"selector"` + + // The binary operator to be applied to the content fetched from the selector + // Possible values are: "eq" (equal to), "neq" (not equal to) + Operator WhenConditionOperator `json:"operator"` -func NewRateLimitPolicyType() kuadrantgatewayapi.PolicyType { - return &rateLimitPolicyType{} + // The value of reference for the comparison. + Value string `json:"value"` } -func (r rateLimitPolicyType) GetGVK() schema.GroupVersionKind { - return RateLimitPolicyGVK -} -func (r rateLimitPolicyType) GetInstance() client.Object { - return &RateLimitPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: RateLimitPolicyGVK.Kind, - APIVersion: GroupVersion.String(), - }, - } +// ContextSelector defines one item from the well known attributes +// Attributes: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes +// Well-known selectors: https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors +// They are named by a dot-separated path (e.g. request.path) +// Example: "request.path" -> The path portion of the URL +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=253 +type ContextSelector string + +// +kubebuilder:validation:Enum:=eq;neq;startswith;endswith;incl;excl;matches +type WhenConditionOperator string + +type RateLimitPolicyStatus struct { + // ObservedGeneration reflects the generation of the most recently observed spec. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: "Available" + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` } -func (r rateLimitPolicyType) GetList(ctx context.Context, cl client.Client, listOpts ...client.ListOption) ([]kuadrantgatewayapi.Policy, error) { - rlpList := &RateLimitPolicyList{} - err := cl.List(ctx, rlpList, listOpts...) - if err != nil { - return nil, err - } - return utils.Map(rlpList.Items, func(p RateLimitPolicy) kuadrantgatewayapi.Policy { return &p }), nil +func (s *RateLimitPolicyStatus) GetConditions() []metav1.Condition { + return s.Conditions } -func (r rateLimitPolicyType) BackReferenceAnnotationName() string { - return RateLimitPolicyBackReferenceAnnotationName +//+kubebuilder:object:root=true + +// RateLimitPolicyList contains a list of RateLimitPolicy +type RateLimitPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RateLimitPolicy `json:"items"` } -func (r rateLimitPolicyType) DirectReferenceAnnotationName() string { - return RateLimitPolicyDirectReferenceAnnotationName +// DEPRECATED. impl: kuadrant.PolicyList +func (l *RateLimitPolicyList) GetItems() []kuadrant.Policy { + return utils.Map(l.Items, func(item RateLimitPolicy) kuadrant.Policy { + return &item + }) } func init() { diff --git a/api/v1beta3/ratelimitpolicy_types_test.go b/api/v1beta3/ratelimitpolicy_types_test.go index 4e5e28781..07598f6a8 100644 --- a/api/v1beta3/ratelimitpolicy_types_test.go +++ b/api/v1beta3/ratelimitpolicy_types_test.go @@ -4,102 +4,98 @@ package v1beta3 import ( "testing" - - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) -func testBuildBasicRLP(name string, kind gatewayapiv1.Kind, mutateFn func(*RateLimitPolicy)) *RateLimitPolicy { - p := &RateLimitPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "RateLimitPolicy", - APIVersion: GroupVersion.String(), +func TestConvertRateIntoSeconds(t *testing.T) { + testCases := []struct { + name string + rate Rate + expectedMaxValue int + expectedSeconds int + }{ + { + name: "seconds", + rate: Rate{ + Limit: 5, Duration: 2, Unit: TimeUnit("second"), + }, + expectedMaxValue: 5, + expectedSeconds: 2, }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "testNS", + { + name: "minutes", + rate: Rate{ + Limit: 5, Duration: 2, Unit: TimeUnit("minute"), + }, + expectedMaxValue: 5, + expectedSeconds: 2 * 60, }, - Spec: RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: kind, - Name: "some-name", + { + name: "hours", + rate: Rate{ + Limit: 5, Duration: 2, Unit: TimeUnit("hour"), }, + expectedMaxValue: 5, + expectedSeconds: 2 * 60 * 60, }, - } - - if mutateFn != nil { - mutateFn(p) - } - - return p -} - -func testBuildBasicHTTPRouteRLP(name string, mutateFn func(*RateLimitPolicy)) *RateLimitPolicy { - return testBuildBasicRLP(name, "HTTPRoute", mutateFn) -} - -func TestRateLimitPolicyListGetItems(t *testing.T) { - list := &RateLimitPolicyList{} - if len(list.GetItems()) != 0 { - t.Errorf("Expected empty list of items") - } - policy := RateLimitPolicy{} - list.Items = []RateLimitPolicy{policy} - result := list.GetItems() - if len(result) != 1 { - t.Errorf("Expected 1 item, got %d", len(result)) - } - _, ok := result[0].(kuadrant.Policy) - if !ok { - t.Errorf("Expected item to be a Policy") - } -} - -func TestRateLimitPolicy_GetLimits(t *testing.T) { - const name = "policy" - var ( - defaultLimits = map[string]Limit{ - "default": { - Rates: []Rate{{Limit: 10, Duration: 1, Unit: "seconds"}}, + { + name: "day", + rate: Rate{ + Limit: 5, Duration: 2, Unit: TimeUnit("day"), + }, + expectedMaxValue: 5, + expectedSeconds: 2 * 60 * 60 * 24, + }, + { + name: "negative limit", + rate: Rate{ + Limit: -5, Duration: 2, Unit: TimeUnit("second"), + }, + expectedMaxValue: 0, + expectedSeconds: 2, + }, + { + name: "negative duration", + rate: Rate{ + Limit: 5, Duration: -2, Unit: TimeUnit("second"), + }, + expectedMaxValue: 5, + expectedSeconds: 0, + }, + { + name: "limit is 0", + rate: Rate{ + Limit: 0, Duration: 2, Unit: TimeUnit("second"), }, - } - implicitLimits = map[string]Limit{ - "implicit": { - Rates: []Rate{{Limit: 20, Duration: 2, Unit: "minutes"}}, + expectedMaxValue: 0, + expectedSeconds: 2, + }, + { + name: "rate is 0", + rate: Rate{ + Limit: 5, Duration: 0, Unit: TimeUnit("second"), }, - } - ) + expectedMaxValue: 5, + expectedSeconds: 0, + }, + { + name: "unexpected time unit", + rate: Rate{ + Limit: 5, Duration: 2, Unit: TimeUnit("unknown"), + }, + expectedMaxValue: 5, + expectedSeconds: 0, + }, + } - t.Run("No limits defined", func(subT *testing.T) { - r := testBuildBasicHTTPRouteRLP(name, nil) - assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, map[string]Limit(nil)) - }) - t.Run("Defaults defined", func(subT *testing.T) { - r := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) { - policy.Spec.Defaults = &RateLimitPolicyCommonSpec{ - Limits: defaultLimits, + for _, tc := range testCases { + t.Run(tc.name, func(subT *testing.T) { + maxValue, seconds := tc.rate.ToSeconds() + if maxValue != tc.expectedMaxValue { + subT.Errorf("maxValue does not match, expected(%d), got (%d)", tc.expectedMaxValue, maxValue) } - }) - assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, defaultLimits) - }) - t.Run("Implicit rules defined", func(subT *testing.T) { - r := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) { - policy.Spec.Limits = implicitLimits - }) - assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, implicitLimits) - }) - t.Run("Default rules takes precedence over implicit rules if validation is somehow bypassed", func(subT *testing.T) { - r := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) { - policy.Spec.Defaults = &RateLimitPolicyCommonSpec{ - Limits: defaultLimits, + if seconds != tc.expectedSeconds { + subT.Errorf("seconds does not match, expected(%d), got (%d)", tc.expectedSeconds, seconds) } - policy.Spec.Limits = implicitLimits }) - assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, defaultLimits) - }) + } } diff --git a/api/v1beta3/topology.go b/api/v1beta3/topology.go deleted file mode 100644 index aa6672910..000000000 --- a/api/v1beta3/topology.go +++ /dev/null @@ -1,38 +0,0 @@ -package v1beta3 - -// Contains of this file allow the AuthPolicy and RateLimitPolicy to adhere to the machinery.Policy interface - -import ( - "github.com/kuadrant/policy-machinery/machinery" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - RateLimitPoliciesResource = GroupVersion.WithResource("ratelimitpolicies") - RateLimitPolicyGroupKind = schema.GroupKind{Group: GroupVersion.Group, Kind: "RateLimitPolicy"} -) - -var _ machinery.Policy = &RateLimitPolicy{} - -func (r *RateLimitPolicy) GetTargetRefs() []machinery.PolicyTargetReference { - return []machinery.PolicyTargetReference{ - machinery.LocalPolicyTargetReference{ - LocalPolicyTargetReference: r.Spec.TargetRef, - PolicyNamespace: r.Namespace, - }, - } -} - -func (r *RateLimitPolicy) GetMergeStrategy() machinery.MergeStrategy { - return func(policy machinery.Policy, _ machinery.Policy) machinery.Policy { - return policy - } -} - -func (r *RateLimitPolicy) Merge(other machinery.Policy) machinery.Policy { - return other -} - -func (r *RateLimitPolicy) GetLocator() string { - return machinery.LocatorFromObject(r) -} diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go index 1af900e7b..2b2265ee7 100644 --- a/api/v1beta3/zz_generated.deepcopy.go +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -55,6 +55,22 @@ func (in *Limit) DeepCopy() *Limit { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableRateLimitPolicySpec) DeepCopyInto(out *MergeableRateLimitPolicySpec) { + *out = *in + in.RateLimitPolicySpecProper.DeepCopyInto(&out.RateLimitPolicySpecProper) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableRateLimitPolicySpec. +func (in *MergeableRateLimitPolicySpec) DeepCopy() *MergeableRateLimitPolicySpec { + if in == nil { + return nil + } + out := new(MergeableRateLimitPolicySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Rate) DeepCopyInto(out *Rate) { *out = *in @@ -97,28 +113,6 @@ func (in *RateLimitPolicy) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RateLimitPolicyCommonSpec) DeepCopyInto(out *RateLimitPolicyCommonSpec) { - *out = *in - if in.Limits != nil { - in, out := &in.Limits, &out.Limits - *out = make(map[string]Limit, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitPolicyCommonSpec. -func (in *RateLimitPolicyCommonSpec) DeepCopy() *RateLimitPolicyCommonSpec { - if in == nil { - return nil - } - out := new(RateLimitPolicyCommonSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimitPolicyList) DeepCopyInto(out *RateLimitPolicyList) { *out = *in @@ -154,18 +148,18 @@ func (in *RateLimitPolicyList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimitPolicySpec) DeepCopyInto(out *RateLimitPolicySpec) { *out = *in - out.TargetRef = in.TargetRef + in.TargetRef.DeepCopyInto(&out.TargetRef) if in.Defaults != nil { in, out := &in.Defaults, &out.Defaults - *out = new(RateLimitPolicyCommonSpec) + *out = new(MergeableRateLimitPolicySpec) (*in).DeepCopyInto(*out) } if in.Overrides != nil { in, out := &in.Overrides, &out.Overrides - *out = new(RateLimitPolicyCommonSpec) + *out = new(MergeableRateLimitPolicySpec) (*in).DeepCopyInto(*out) } - in.RateLimitPolicyCommonSpec.DeepCopyInto(&out.RateLimitPolicyCommonSpec) + in.RateLimitPolicySpecProper.DeepCopyInto(&out.RateLimitPolicySpecProper) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitPolicySpec. @@ -178,10 +172,31 @@ func (in *RateLimitPolicySpec) DeepCopy() *RateLimitPolicySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimitPolicySpecProper) DeepCopyInto(out *RateLimitPolicySpecProper) { + *out = *in + if in.Limits != nil { + in, out := &in.Limits, &out.Limits + *out = make(map[string]Limit, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitPolicySpecProper. +func (in *RateLimitPolicySpecProper) DeepCopy() *RateLimitPolicySpecProper { + if in == nil { + return nil + } + out := new(RateLimitPolicySpecProper) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimitPolicyStatus) DeepCopyInto(out *RateLimitPolicyStatus) { *out = *in - out.StatusMeta = in.StatusMeta if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index 93eccaa51..b1fce89ab 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -106,7 +106,7 @@ metadata: capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/kuadrant-operator:latest - createdAt: "2024-10-18T15:08:50Z" + createdAt: "2024-10-18T16:21:01Z" description: A Kubernetes Operator to manage the lifecycle of the Kuadrant system operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 diff --git a/bundle/manifests/kuadrant.io_ratelimitpolicies.yaml b/bundle/manifests/kuadrant.io_ratelimitpolicies.yaml index 043dec0fa..01c303782 100644 --- a/bundle/manifests/kuadrant.io_ratelimitpolicies.yaml +++ b/bundle/manifests/kuadrant.io_ratelimitpolicies.yaml @@ -28,14 +28,19 @@ spec: name: Enforced priority: 2 type: string - - description: Type of the referenced Gateway API resource + - description: Kind of the object to which the policy aaplies jsonPath: .spec.targetRef.kind - name: TargetRefKind + name: TargetKind priority: 2 type: string - - description: Name of the referenced Gateway API resource + - description: Name of the object to which the policy applies jsonPath: .spec.targetRef.name - name: TargetRefName + name: TargetName + priority: 2 + type: string + - description: 'Name of the section within the object to which the policy applies ' + jsonPath: .spec.targetRef.sectionName + name: TargetSection priority: 2 type: string - jsonPath: .metadata.creationTimestamp @@ -65,12 +70,11 @@ spec: metadata: type: object spec: - description: RateLimitPolicySpec defines the desired state of RateLimitPolicy properties: defaults: description: |- - Defaults define explicit default values for this policy and for policies inheriting this policy. - Defaults are mutually exclusive with implicit defaults defined by RateLimitPolicyCommonSpec. + Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: limits: additionalProperties: @@ -162,8 +166,15 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string type: object limits: additionalProperties: @@ -255,12 +266,11 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object overrides: description: |- - Overrides define override values for this policy and for policies inheriting this policy. - Overrides are mutually exclusive with implicit defaults and explicit Defaults defined by RateLimitPolicyCommonSpec. + Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: limits: additionalProperties: @@ -352,11 +362,18 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string type: object targetRef: - description: TargetRef identifies an API object to apply policy to. + description: Reference to the object to which this policy applies. properties: group: description: Group is the group of the target resource. @@ -374,6 +391,25 @@ spec: maxLength: 253 minLength: 1 type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string required: - group - kind @@ -395,11 +431,7 @@ spec: rule: '!(has(self.defaults) && has(self.overrides))' - message: Overrides and implicit defaults are mutually exclusive rule: '!(has(self.overrides) && has(self.limits))' - - message: Overrides are only allowed for policies targeting a Gateway - resource - rule: '!(has(self.overrides) && self.targetRef.kind != ''Gateway'')' status: - description: RateLimitPolicyStatus defines the observed state of RateLimitPolicy properties: conditions: description: |- diff --git a/charts/kuadrant-operator/templates/manifests.yaml b/charts/kuadrant-operator/templates/manifests.yaml index 969d0f0ba..c66340076 100644 --- a/charts/kuadrant-operator/templates/manifests.yaml +++ b/charts/kuadrant-operator/templates/manifests.yaml @@ -13920,14 +13920,19 @@ spec: name: Enforced priority: 2 type: string - - description: Type of the referenced Gateway API resource + - description: Kind of the object to which the policy aaplies jsonPath: .spec.targetRef.kind - name: TargetRefKind + name: TargetKind priority: 2 type: string - - description: Name of the referenced Gateway API resource + - description: Name of the object to which the policy applies jsonPath: .spec.targetRef.name - name: TargetRefName + name: TargetName + priority: 2 + type: string + - description: 'Name of the section within the object to which the policy applies ' + jsonPath: .spec.targetRef.sectionName + name: TargetSection priority: 2 type: string - jsonPath: .metadata.creationTimestamp @@ -13957,12 +13962,11 @@ spec: metadata: type: object spec: - description: RateLimitPolicySpec defines the desired state of RateLimitPolicy properties: defaults: description: |- - Defaults define explicit default values for this policy and for policies inheriting this policy. - Defaults are mutually exclusive with implicit defaults defined by RateLimitPolicyCommonSpec. + Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: limits: additionalProperties: @@ -14054,8 +14058,15 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string type: object limits: additionalProperties: @@ -14147,12 +14158,11 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object overrides: description: |- - Overrides define override values for this policy and for policies inheriting this policy. - Overrides are mutually exclusive with implicit defaults and explicit Defaults defined by RateLimitPolicyCommonSpec. + Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: limits: additionalProperties: @@ -14244,11 +14254,18 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string type: object targetRef: - description: TargetRef identifies an API object to apply policy to. + description: Reference to the object to which this policy applies. properties: group: description: Group is the group of the target resource. @@ -14266,6 +14283,25 @@ spec: maxLength: 253 minLength: 1 type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string required: - group - kind @@ -14287,11 +14323,7 @@ spec: rule: '!(has(self.defaults) && has(self.overrides))' - message: Overrides and implicit defaults are mutually exclusive rule: '!(has(self.overrides) && has(self.limits))' - - message: Overrides are only allowed for policies targeting a Gateway - resource - rule: '!(has(self.overrides) && self.targetRef.kind != ''Gateway'')' status: - description: RateLimitPolicyStatus defines the observed state of RateLimitPolicy properties: conditions: description: |- diff --git a/config/crd/bases/kuadrant.io_ratelimitpolicies.yaml b/config/crd/bases/kuadrant.io_ratelimitpolicies.yaml index 88b591b84..4b1b4b76b 100644 --- a/config/crd/bases/kuadrant.io_ratelimitpolicies.yaml +++ b/config/crd/bases/kuadrant.io_ratelimitpolicies.yaml @@ -27,14 +27,19 @@ spec: name: Enforced priority: 2 type: string - - description: Type of the referenced Gateway API resource + - description: Kind of the object to which the policy aaplies jsonPath: .spec.targetRef.kind - name: TargetRefKind + name: TargetKind priority: 2 type: string - - description: Name of the referenced Gateway API resource + - description: Name of the object to which the policy applies jsonPath: .spec.targetRef.name - name: TargetRefName + name: TargetName + priority: 2 + type: string + - description: 'Name of the section within the object to which the policy applies ' + jsonPath: .spec.targetRef.sectionName + name: TargetSection priority: 2 type: string - jsonPath: .metadata.creationTimestamp @@ -64,12 +69,11 @@ spec: metadata: type: object spec: - description: RateLimitPolicySpec defines the desired state of RateLimitPolicy properties: defaults: description: |- - Defaults define explicit default values for this policy and for policies inheriting this policy. - Defaults are mutually exclusive with implicit defaults defined by RateLimitPolicyCommonSpec. + Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: limits: additionalProperties: @@ -161,8 +165,15 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string type: object limits: additionalProperties: @@ -254,12 +265,11 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object overrides: description: |- - Overrides define override values for this policy and for policies inheriting this policy. - Overrides are mutually exclusive with implicit defaults and explicit Defaults defined by RateLimitPolicyCommonSpec. + Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: limits: additionalProperties: @@ -351,11 +361,18 @@ spec: type: object description: Limits holds the struct of limits indexed by a unique name - maxProperties: 14 type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string type: object targetRef: - description: TargetRef identifies an API object to apply policy to. + description: Reference to the object to which this policy applies. properties: group: description: Group is the group of the target resource. @@ -373,6 +390,25 @@ spec: maxLength: 253 minLength: 1 type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string required: - group - kind @@ -394,11 +430,7 @@ spec: rule: '!(has(self.defaults) && has(self.overrides))' - message: Overrides and implicit defaults are mutually exclusive rule: '!(has(self.overrides) && has(self.limits))' - - message: Overrides are only allowed for policies targeting a Gateway - resource - rule: '!(has(self.overrides) && self.targetRef.kind != ''Gateway'')' status: - description: RateLimitPolicyStatus defines the observed state of RateLimitPolicy properties: conditions: description: |- diff --git a/controllers/authorino_reconciler.go b/controllers/authorino_reconciler.go index 23bf8ce08..f8983f839 100644 --- a/controllers/authorino_reconciler.go +++ b/controllers/authorino_reconciler.go @@ -40,9 +40,9 @@ func (r *AuthorinoReconciler) Reconcile(ctx context.Context, _ []controller.Reso logger.Info("reconciling authorino resource", "status", "started") defer logger.Info("reconciling authorino resource", "status", "completed") - kobj, err := GetKuadrant(topology) + kobj, err := GetKuadrantFromTopology(topology) if err != nil { - if errors.Is(err, ErrNoKandrantResource) { + if errors.Is(err, ErrMissingKuadrant) { logger.Info("kuadrant resource not found, ignoring", "status", "skipping") return err } diff --git a/controllers/consoleplugin_reconciler.go b/controllers/consoleplugin_reconciler.go index a9cf15d7a..b1718b963 100644 --- a/controllers/consoleplugin_reconciler.go +++ b/controllers/consoleplugin_reconciler.go @@ -5,6 +5,8 @@ import ( "sync" "github.com/go-logr/logr" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" consolev1 "github.com/openshift/api/console/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -12,9 +14,6 @@ import ( "k8s.io/utils/ptr" ctrlruntime "sigs.k8s.io/controller-runtime" - "github.com/kuadrant/policy-machinery/controller" - "github.com/kuadrant/policy-machinery/machinery" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/library/utils" "github.com/kuadrant/kuadrant-operator/pkg/log" @@ -37,9 +36,10 @@ type ConsolePluginReconciler struct { func NewConsolePluginReconciler(mgr ctrlruntime.Manager, namespace string) *ConsolePluginReconciler { return &ConsolePluginReconciler{ BaseReconciler: reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("consoleplugin"), - mgr.GetEventRecorderFor("ConsolePlugin"), ), namespace: namespace, } diff --git a/controllers/consoleplugin_reconciler_test.go b/controllers/consoleplugin_reconciler_test.go index 13a84bc6e..cb7c7fbdd 100644 --- a/controllers/consoleplugin_reconciler_test.go +++ b/controllers/consoleplugin_reconciler_test.go @@ -6,6 +6,8 @@ import ( "context" "testing" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" consolev1 "github.com/openshift/api/console/v1" "gotest.tools/assert" is "gotest.tools/assert/cmp" @@ -24,8 +26,6 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/openshift" "github.com/kuadrant/kuadrant-operator/pkg/openshift/consoleplugin" - "github.com/kuadrant/policy-machinery/controller" - "github.com/kuadrant/policy-machinery/machinery" ) var ( diff --git a/controllers/effective_ratelimitpolicies_reconciler.go b/controllers/effective_ratelimitpolicies_reconciler.go new file mode 100644 index 000000000..641d0bbb0 --- /dev/null +++ b/controllers/effective_ratelimitpolicies_reconciler.go @@ -0,0 +1,92 @@ +package controllers + +import ( + "context" + "encoding/json" + "errors" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/client-go/dynamic" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" +) + +type EffectiveRateLimitPolicy struct { + Path []machinery.Targetable + Spec kuadrantv1beta3.RateLimitPolicy +} + +type EffectiveRateLimitPolicies map[string]EffectiveRateLimitPolicy + +type effectiveRateLimitPolicyReconciler struct { + client *dynamic.DynamicClient +} + +func (r *effectiveRateLimitPolicyReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: rateLimitEventMatchers, + } +} + +func (r *effectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("effectiveRateLimitPolicyReconciler") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + effectivePolicies := r.calculateEffectivePolicies(ctx, topology, kuadrant, state) + + state.Store(StateEffectiveRateLimitPolicies, effectivePolicies) + + return nil +} + +func (r *effectiveRateLimitPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveRateLimitPolicies { + logger := controller.LoggerFromContext(ctx).WithName("effectiveRateLimitPolicyReconciler").WithName("calculateEffectivePolicies") + + targetables := topology.Targetables() + gatewayClasses := targetables.Children(kuadrant) // assumes only and all valid gateway classes are linked to kuadrant in the topology + httpRouteRules := targetables.Items(func(o machinery.Object) bool { + _, ok := o.(*machinery.HTTPRouteRule) + return ok + }) + + logger.V(1).Info("calculating effective rate limit policies", "httpRouteRules", len(httpRouteRules)) + + effectivePolicies := EffectiveRateLimitPolicies{} + + for _, gatewayClass := range gatewayClasses { + for _, httpRouteRule := range httpRouteRules { + paths := targetables.Paths(gatewayClass, httpRouteRule) // this may be expensive in clusters with many gateway classes - an alternative is to deep search the topology for httprouterules from each gatewayclass, keeping record of the paths + for i := range paths { + if effectivePolicy := kuadrantv1.EffectivePolicyForPath[*kuadrantv1beta3.RateLimitPolicy](paths[i], isRateLimitPolicyAcceptedAndNotDeletedFunc(state)); effectivePolicy != nil { + pathID := kuadrantv1.PathID(paths[i]) + effectivePolicies[pathID] = EffectiveRateLimitPolicy{ + Path: paths[i], + Spec: **effectivePolicy, + } + if logger.V(1).Enabled() { + jsonEffectivePolicy, _ := json.Marshal(effectivePolicy) + pathLocators := lo.Map(paths[i], machinery.MapTargetableToLocatorFunc) + logger.V(1).Info("effective policy", "kind", kuadrantv1beta3.RateLimitPolicyGroupKind.Kind, "pathID", pathID, "path", pathLocators, "effectivePolicy", string(jsonEffectivePolicy)) + } + } + } + } + } + + logger.V(1).Info("finished calculating effective rate limit policies", "effectivePolicies", len(effectivePolicies)) + + return effectivePolicies +} diff --git a/controllers/envoy_gateway_extension_reconciler.go b/controllers/envoy_gateway_extension_reconciler.go new file mode 100644 index 000000000..9e8ad2fdf --- /dev/null +++ b/controllers/envoy_gateway_extension_reconciler.go @@ -0,0 +1,269 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" +) + +// envoyGatewayExtensionReconciler reconciles Envoy Gateway EnvoyExtensionPolicy custom resources +type envoyGatewayExtensionReconciler struct { + client *dynamic.DynamicClient +} + +func (r *envoyGatewayExtensionReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + }, + } +} + +func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayExtensionReconciler") + + logger.V(1).Info("building envoy gateway extension") + defer logger.V(1).Info("finished building envoy gateway extension") + + // build wasm plugin configs for each gateway + wasmConfigs, err := r.buildWasmConfigs(ctx, state) + if err != nil { + if errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { + logger.V(1).Info(err.Error()) + } else { + return err + } + } + + // reconcile for each gateway based on the desired wasm plugin policies calculated before + gateways := lo.Map(topology.Targetables().Items(func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == machinery.GatewayGroupKind + }), func(g machinery.Targetable, _ int) *machinery.Gateway { + return g.(*machinery.Gateway) + }) + + var modifiedGateways []string + + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyExtensionPolicy := buildEnvoyExtensionPolicyForGateway(gateway, wasmConfigs[gateway.GetLocator()]) + + resource := r.client.Resource(kuadrantenvoygateway.EnvoyExtensionPoliciesResource).Namespace(desiredEnvoyExtensionPolicy.GetNamespace()) + + existingEnvoyExtensionPolicyObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind && child.GetName() == desiredEnvoyExtensionPolicy.GetName() && child.GetNamespace() == desiredEnvoyExtensionPolicy.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyExtensionPolicy.GetLabels())) + }) + + // create + if !found { + if utils.IsObjectTaggedToDelete(desiredEnvoyExtensionPolicy) { + continue + } + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoyextensionpolicy is created, because updates won't change the status + desiredEnvoyExtensionPolicyUnstructured, err := controller.Destruct(desiredEnvoyExtensionPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoyextensionpolicy object", "gateway", gatewayKey.String(), "envoyextensionpolicy", desiredEnvoyExtensionPolicy) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyExtensionPolicyUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoyextensionpolicy object", "gateway", gatewayKey.String(), "envoyextensionpolicy", desiredEnvoyExtensionPolicyUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyExtensionPolicy := existingEnvoyExtensionPolicyObj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyExtensionPolicy) + + // delete + if utils.IsObjectTaggedToDelete(desiredEnvoyExtensionPolicy) && !utils.IsObjectTaggedToDelete(existingEnvoyExtensionPolicy) { + if err := resource.Delete(ctx, existingEnvoyExtensionPolicy.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoyextensionpolicy object", "gateway", gatewayKey.String(), "envoyextensionpolicy", fmt.Sprintf("%s/%s", existingEnvoyExtensionPolicy.GetNamespace(), existingEnvoyExtensionPolicy.GetName())) + // TODO: handle error + } + continue + } + + if equalEnvoyExtensionPolicies(existingEnvoyExtensionPolicy, desiredEnvoyExtensionPolicy) { + logger.V(1).Info("envoyextensionpolicy object is up to date, nothing to do") + continue + } + + // update + existingEnvoyExtensionPolicy.Spec.TargetRefs = desiredEnvoyExtensionPolicy.Spec.TargetRefs + existingEnvoyExtensionPolicy.Spec.Wasm = desiredEnvoyExtensionPolicy.Spec.Wasm + + existingEnvoyExtensionPolicyUnstructured, err := controller.Destruct(existingEnvoyExtensionPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoyextensionpolicy object", "gateway", gatewayKey.String(), "envoyextensionpolicy", existingEnvoyExtensionPolicy) + continue + } + if _, err = resource.Update(ctx, existingEnvoyExtensionPolicyUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoyextensionpolicy object", "gateway", gatewayKey.String(), "envoyextensionpolicy", existingEnvoyExtensionPolicyUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateEnvoyGatewayExtensionsModified, modifiedGateways) + + return nil +} + +// buildWasmConfigs returns a map of envoy gateway gateway locators to an ordered list of corresponding wasm policies +func (r *envoyGatewayExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { + logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayExtensionReconciler").WithName("buildWasmConfigs") + + effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + if !ok { + return nil, ErrMissingStateEffectiveRateLimitPolicies + } + + logger.V(1).Info("building wasm configs for envoy gateway extension", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + + wasmActionSets := kuadrantgatewayapi.GrouppedHTTPRouteMatchConfigs{} + + // build the wasm policies for each topological path that contains an effective rate limit policy affecting an envoy gateway gateway + for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + + // ignore if not an envoy gateway gateway + if gatewayClass.Spec.ControllerName != envoyGatewayGatewayControllerName { + continue + } + + wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, effectivePolicy.Path, effectivePolicy.Spec.Rules(), rateLimitWasmActionBuilder(pathID, effectivePolicy, state)) + if err != nil { + logger.Error(err, "failed to build wasm policies for path", "pathID", pathID) + continue + } + wasmActionSets.Add(gateway.GetLocator(), wasmActionSetsForPath...) + } + + wasmConfigs := lo.MapValues(wasmActionSets.Sorted(), func(configs kuadrantgatewayapi.SortableHTTPRouteMatchConfigs, _ string) wasm.Config { + return wasm.BuildConfigForActionSet(lo.Map(configs, func(c kuadrantgatewayapi.HTTPRouteMatchConfig, _ int) wasm.ActionSet { + return c.Config.(wasm.ActionSet) + })) + }) + + return wasmConfigs, nil +} + +// buildEnvoyExtensionPolicyForGateway builds a desired EnvoyExtensionPolicy custom resource for a given gateway and corresponding wasm config +func buildEnvoyExtensionPolicyForGateway(gateway *machinery.Gateway, wasmConfig wasm.Config) *envoygatewayv1alpha1.EnvoyExtensionPolicy { + envoyPolicy := &envoygatewayv1alpha1.EnvoyExtensionPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind.Kind, + APIVersion: envoygatewayv1alpha1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: wasm.ExtensionName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: KuadrantManagedObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: envoygatewayv1alpha1.EnvoyExtensionPolicySpec{ + PolicyTargetReferences: envoygatewayv1alpha1.PolicyTargetReferences{ + TargetRefs: []gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + { + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1alpha2.Group(machinery.GatewayGroupKind.Group), + Kind: gatewayapiv1alpha2.Kind(machinery.GatewayGroupKind.Kind), + Name: gatewayapiv1alpha2.ObjectName(gateway.GetName()), + }, + }, + }, + }, + Wasm: []envoygatewayv1alpha1.Wasm{ + { + Name: ptr.To("kuadrant-wasm-shim"), + RootID: ptr.To("kuadrant_wasm_shim"), + Code: envoygatewayv1alpha1.WasmCodeSource{ + Type: envoygatewayv1alpha1.ImageWasmCodeSourceType, + Image: &envoygatewayv1alpha1.ImageWasmCodeSource{ + URL: WASMFilterImageURL, + }, + }, + Config: nil, + // When a fatal error accurs during the initialization or the execution of the + // Wasm extension, if FailOpen is set to false the system blocks the traffic and returns + // an HTTP 5xx error. + FailOpen: ptr.To(false), + }, + }, + }, + } + + if len(wasmConfig.ActionSets) == 0 { + utils.TagObjectToDelete(envoyPolicy) + } else { + pluginConfigJSON, err := wasmConfig.ToJSON() + if err != nil { + return nil + } + envoyPolicy.Spec.Wasm[0].Config = pluginConfigJSON + } + + return envoyPolicy +} + +func equalEnvoyExtensionPolicies(a, b *envoygatewayv1alpha1.EnvoyExtensionPolicy) bool { + if !kuadrantgatewayapi.EqualLocalPolicyTargetReferencesWithSectionName(a.Spec.TargetRefs, b.Spec.TargetRefs) { + return false + } + + aWasms := a.Spec.Wasm + bWasms := b.Spec.Wasm + + return len(aWasms) == len(bWasms) && !lo.EveryBy(aWasms, func(aWasm envoygatewayv1alpha1.Wasm) bool { + return lo.SomeBy(bWasms, func(bWasm envoygatewayv1alpha1.Wasm) bool { + if ptr.Deref(aWasm.Name, "") != ptr.Deref(bWasm.Name, "") || ptr.Deref(aWasm.RootID, "") != ptr.Deref(bWasm.RootID, "") || ptr.Deref(aWasm.FailOpen, false) != ptr.Deref(bWasm.FailOpen, false) || aWasm.Code.Type != bWasm.Code.Type || aWasm.Code.Image.URL != bWasm.Code.Image.URL { + return false + } + aWasmConfigJSON, err := wasm.ConfigFromJSON(aWasm.Config) + if err != nil { + return false + } + bWasmConfigJSON, err := wasm.ConfigFromJSON(bWasm.Config) + if err != nil { + return false + } + return reflect.DeepEqual(aWasmConfigJSON, bWasmConfigJSON) + }) + }) +} diff --git a/controllers/envoy_gateway_rate_limit_cluster_reconciler.go b/controllers/envoy_gateway_rate_limit_cluster_reconciler.go new file mode 100644 index 000000000..b61eab34a --- /dev/null +++ b/controllers/envoy_gateway_rate_limit_cluster_reconciler.go @@ -0,0 +1,241 @@ +package controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" +) + +// envoyGatewayRateLimitClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources +type envoyGatewayRateLimitClusterReconciler struct { + client *dynamic.DynamicClient +} + +func (r *envoyGatewayRateLimitClusterReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + }, + } +} + +func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayRateLimitClusterReconciler") + + logger.V(1).Info("building envoy gateway rate limit clusters") + defer logger.V(1).Info("finished building envoy gateway rate limit clusters") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + limitadorObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.LimitadorGroupKind + }) + if !found { + logger.V(1).Info(ErrMissingLimitador.Error()) + return nil + } + limitador := limitadorObj.(*controller.RuntimeObject).Object.(*limitadorv1alpha1.Limitador) + + effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveRateLimitPolicies, "failed to get effective rate limit policies from state") + return nil + } + + gateways := lo.UniqBy(lo.FilterMap(lo.Values(effectivePolicies.(EffectiveRateLimitPolicies)), func(effectivePolicy EffectiveRateLimitPolicy, _ int) (*machinery.Gateway, bool) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + return gateway, gatewayClass.Spec.ControllerName == envoyGatewayGatewayControllerName + }), func(gateway *machinery.Gateway) string { + return gateway.GetLocator() + }) + + desiredEnvoyPatchPolicies := make(map[k8stypes.NamespacedName]struct{}) + var modifiedGateways []string + + // reconcile envoy gateway cluster for gateway + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyPatchPolicy, err := r.buildDesiredEnvoyPatchPolicy(limitador, gateway) + if err != nil { + logger.Error(err, "failed to build desired envoy patch policy") + continue + } + desiredEnvoyPatchPolicies[k8stypes.NamespacedName{Name: desiredEnvoyPatchPolicy.GetName(), Namespace: desiredEnvoyPatchPolicy.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantenvoygateway.EnvoyPatchPoliciesResource).Namespace(desiredEnvoyPatchPolicy.GetNamespace()) + + existingEnvoyPatchPolicyObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyPatchPolicyGroupKind && child.GetName() == desiredEnvoyPatchPolicy.GetName() && child.GetNamespace() == desiredEnvoyPatchPolicy.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyPatchPolicy.GetLabels())) + }) + + // create + if !found { + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoypatchpolicy is created, because updates won't change the status + desiredEnvoyPatchPolicyUnstructured, err := controller.Destruct(desiredEnvoyPatchPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", desiredEnvoyPatchPolicy) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyPatchPolicyUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", desiredEnvoyPatchPolicyUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyPatchPolicy := existingEnvoyPatchPolicyObj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy) + + if equalEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { + logger.V(1).Info("envoypatchpolicy object is up to date, nothing to do") + continue + } + + // update + existingEnvoyPatchPolicy.Spec = envoygatewayv1alpha1.EnvoyPatchPolicySpec{ + TargetRef: desiredEnvoyPatchPolicy.Spec.TargetRef, + Type: desiredEnvoyPatchPolicy.Spec.Type, + JSONPatches: desiredEnvoyPatchPolicy.Spec.JSONPatches, + Priority: desiredEnvoyPatchPolicy.Spec.Priority, + } + + existingEnvoyPatchPolicyUnstructured, err := controller.Destruct(existingEnvoyPatchPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", existingEnvoyPatchPolicy) + continue + } + if _, err = resource.Update(ctx, existingEnvoyPatchPolicyUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", existingEnvoyPatchPolicyUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateEnvoyGatewayRateLimitClustersModified, modifiedGateways) + + // cleanup envoy gateway clusters for gateways that are not in the effective policies + staleEnvoyPatchPolicies := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredEnvoyPatchPolicies[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyPatchPolicyGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(RateLimitObjectLabels()) && !desired + }) + + for _, envoyPatchPolicy := range staleEnvoyPatchPolicies { + if err := r.client.Resource(kuadrantenvoygateway.EnvoyPatchPoliciesResource).Namespace(envoyPatchPolicy.GetNamespace()).Delete(ctx, envoyPatchPolicy.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoypatchpolicy object", "envoypatchpolicy", fmt.Sprintf("%s/%s", envoyPatchPolicy.GetNamespace(), envoyPatchPolicy.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { + envoyPatchPolicy := &envoygatewayv1alpha1.EnvoyPatchPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantenvoygateway.EnvoyPatchPolicyGroupKind.Kind, + APIVersion: envoygatewayv1alpha1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: RateLimitClusterName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: RateLimitObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: envoygatewayv1alpha1.EnvoyPatchPolicySpec{ + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1alpha2.Group(machinery.GatewayGroupKind.Group), + Kind: gatewayapiv1alpha2.Kind(machinery.GatewayGroupKind.Kind), + Name: gatewayapiv1alpha2.ObjectName(gateway.GetName()), + }, + Type: envoygatewayv1alpha1.JSONPatchEnvoyPatchType, + }, + } + + jsonPatches, err := envoyGatewayEnvoyPatchPolicyClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) + if err != nil { + return nil, err + } + envoyPatchPolicy.Spec.JSONPatches = jsonPatches + + return envoyPatchPolicy, nil +} + +// envoyGatewayEnvoyPatchPolicyClusterPatch returns a set envoy config patch that defines the rate limit cluster for the gateway. +// The rate limit cluster configures the endpoint of the external rate limit service. +func envoyGatewayEnvoyPatchPolicyClusterPatch(host string, port int) ([]envoygatewayv1alpha1.EnvoyJSONPatchConfig, error) { + patchRaw, _ := json.Marshal(rateLimitClusterPatch(host, port)) + patch := &apiextensionsv1.JSON{} + if err := patch.UnmarshalJSON(patchRaw); err != nil { + return nil, err + } + + return []envoygatewayv1alpha1.EnvoyJSONPatchConfig{ + { + Type: envoygatewayv1alpha1.ClusterEnvoyResourceType, + Name: common.KuadrantRateLimitClusterName, + Operation: envoygatewayv1alpha1.JSONPatchOperation{ + Op: envoygatewayv1alpha1.JSONPatchOperationType("add"), + Path: "", + Value: patch, + }, + }, + }, nil +} + +func equalEnvoyPatchPolicies(a, b *envoygatewayv1alpha1.EnvoyPatchPolicy) bool { + if a.Spec.Priority != b.Spec.Priority || a.Spec.TargetRef != b.Spec.TargetRef { + return false + } + + aJSONPatches := a.Spec.JSONPatches + bJSONPatches := b.Spec.JSONPatches + if len(aJSONPatches) != len(bJSONPatches) { + return false + } + return lo.EveryBy(aJSONPatches, func(aJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { + return lo.SomeBy(bJSONPatches, func(bJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { + return aJSONPatch.Type == bJSONPatch.Type && aJSONPatch.Name == bJSONPatch.Name && aJSONPatch.Operation == bJSONPatch.Operation + }) + }) +} diff --git a/controllers/envoygateway_limitador_cluster_controller.go b/controllers/envoygateway_limitador_cluster_controller.go deleted file mode 100644 index 9d9eb7637..000000000 --- a/controllers/envoygateway_limitador_cluster_controller.go +++ /dev/null @@ -1,224 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/go-logr/logr" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - - kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" -) - -// EnvoyGatewayLimitadorClusterReconciler reconciles an EnvoyGateway EnvoyPatchPolicy object -// to setup limitador's cluster on the gateway. It is a requirement for the wasm module to work. -// https://gateway.envoyproxy.io/latest/api/extension_types/#envoypatchpolicy -type EnvoyGatewayLimitadorClusterReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=gateway.envoyproxy.io,resources=envoypatchpolicies,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=gateway.envoyproxy.io,resources=envoyextensionpolicies,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;update;patch - -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile -func (r *EnvoyGatewayLimitadorClusterReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("envoyExtensionPolicy", req.NamespacedName) - logger.V(1).Info("Reconciling limitador cluster") - ctx := logr.NewContext(eventCtx, logger) - - extPolicy := &egv1alpha1.EnvoyExtensionPolicy{} - if err := r.Client().Get(ctx, req.NamespacedName, extPolicy); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no envoygateway extension policy object found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get envoygateway extension policy object") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(extPolicy.Spec.PolicyTargetReferences, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - if extPolicy.DeletionTimestamp != nil { - // no need to handle deletion - // ownerrefs will do the job - return ctrl.Result{}, nil - } - - // - // Get kuadrant - // - kuadrantList := &kuadrantv1beta1.KuadrantList{} - err := r.Client().List(ctx, kuadrantList) - if err != nil { - return ctrl.Result{}, err - } - if len(kuadrantList.Items) == 0 { - logger.Info("kuadrant object not found. Nothing to do") - return ctrl.Result{}, nil - } - - kObj := kuadrantList.Items[0] - - // - // Get limitador - // - limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: kObj.Namespace} - limitador := &limitadorv1alpha1.Limitador{} - err = r.Client().Get(ctx, limitadorKey, limitador) - logger.V(1).Info("read limitador", "key", limitadorKey, "err", err) - if err != nil { - if apierrors.IsNotFound(err) { - logger.Info("limitador object not found. Nothing to do") - return ctrl.Result{}, nil - } - return ctrl.Result{}, err - } - - if !meta.IsStatusConditionTrue(limitador.Status.Conditions, "Ready") { - logger.Info("limitador status reports not ready. Retrying") - return ctrl.Result{Requeue: true}, nil - } - - limitadorClusterPatchPolicy, err := r.desiredLimitadorClusterPatchPolicy(extPolicy, limitador) - if err != nil { - return ctrl.Result{}, err - } - err = r.ReconcileResource(ctx, &egv1alpha1.EnvoyPatchPolicy{}, limitadorClusterPatchPolicy, reconcilers.CreateOnlyMutator) - if err != nil { - return ctrl.Result{}, err - } - - logger.V(1).Info("Envoygateway limitador cluster reconciled successfully") - - return ctrl.Result{}, nil -} - -func (r *EnvoyGatewayLimitadorClusterReconciler) desiredLimitadorClusterPatchPolicy( - extPolicy *egv1alpha1.EnvoyExtensionPolicy, - limitador *limitadorv1alpha1.Limitador) (*egv1alpha1.EnvoyPatchPolicy, error) { - patchPolicy := &egv1alpha1.EnvoyPatchPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: egv1alpha1.KindEnvoyPatchPolicy, - APIVersion: egv1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: LimitadorClusterEnvoyPatchPolicyName(extPolicy.GetName()), - Namespace: extPolicy.Namespace, - }, - Spec: egv1alpha1.EnvoyPatchPolicySpec{ - // Same target ref as the associated extension policy - TargetRef: extPolicy.Spec.PolicyTargetReferences.TargetRefs[0].LocalPolicyTargetReference, - Type: egv1alpha1.JSONPatchEnvoyPatchType, - JSONPatches: []egv1alpha1.EnvoyJSONPatchConfig{ - limitadorClusterPatch( - limitador.Status.Service.Host, - int(limitador.Status.Service.Ports.GRPC), - ), - }, - }, - } - - // controller reference - // patchPolicy has ownerref to extension policy - if err := r.SetOwnerReference(extPolicy, patchPolicy); err != nil { - return nil, err - } - - return patchPolicy, nil -} - -func LimitadorClusterEnvoyPatchPolicyName(targetName string) string { - return fmt.Sprintf("patch-for-%s", targetName) -} - -func limitadorClusterPatch(limitadorSvcHost string, limitadorGRPCPort int) egv1alpha1.EnvoyJSONPatchConfig { - // The patch defines the rate_limit_cluster, which provides the endpoint location of the external rate limit service. - // TODO(eguzki): Istio EnvoyFilter uses almost the same structure. DRY - patchUnstructured := map[string]any{ - "name": common.KuadrantRateLimitClusterName, - "type": "STRICT_DNS", - "connect_timeout": "1s", - "lb_policy": "ROUND_ROBIN", - "http2_protocol_options": map[string]any{}, - "load_assignment": map[string]any{ - "cluster_name": common.KuadrantRateLimitClusterName, - "endpoints": []map[string]any{ - { - "lb_endpoints": []map[string]any{ - { - "endpoint": map[string]any{ - "address": map[string]any{ - "socket_address": map[string]any{ - "address": limitadorSvcHost, - "port_value": limitadorGRPCPort, - }, - }, - }, - }, - }, - }, - }, - }, - } - - patchRaw, _ := json.Marshal(patchUnstructured) - value := &apiextensionsv1.JSON{} - value.UnmarshalJSON(patchRaw) - - return egv1alpha1.EnvoyJSONPatchConfig{ - Type: egv1alpha1.ClusterEnvoyResourceType, - Name: common.KuadrantRateLimitClusterName, - Operation: egv1alpha1.JSONPatchOperation{ - Op: egv1alpha1.JSONPatchOperationType("add"), - Path: "", - Value: value, - }, - } -} - -// SetupWithManager sets up the controller with the Manager. -func (r *EnvoyGatewayLimitadorClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantenvoygateway.IsEnvoyGatewayInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("EnvoyGateway limitador cluster controller disabled. EnvoyGateway API was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("EnvoyGateway limitador cluster disabled. GatewayAPI was not found") - return nil - } - - return ctrl.NewControllerManagedBy(mgr). - For(&egv1alpha1.EnvoyExtensionPolicy{}). - Owns(&egv1alpha1.EnvoyPatchPolicy{}). - Complete(r) -} diff --git a/controllers/envoygateway_wasm_controller.go b/controllers/envoygateway_wasm_controller.go deleted file mode 100644 index ce2881995..000000000 --- a/controllers/envoygateway_wasm_controller.go +++ /dev/null @@ -1,221 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/go-logr/logr" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" - "github.com/kuadrant/kuadrant-operator/pkg/kuadranttools" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" -) - -// EnvoyGatewayWasmReconciler reconciles an EnvoyGateway EnvoyExtensionPolicy object for the kuadrant's wasm module -type EnvoyGatewayWasmReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=gateway.envoyproxy.io,resources=envoyextensionpolicies,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;update;patch -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;update;patch -//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;update;patch - -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile -func (r *EnvoyGatewayWasmReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("kuadrant", req.NamespacedName) - logger.V(1).Info("Reconciling envoygateway wasm attachment") - ctx := logr.NewContext(eventCtx, logger) - - kObj := &kuadrantv1beta1.Kuadrant{} - if err := r.Client().Get(ctx, req.NamespacedName, kObj); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no kuadrant object found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get kuadrant object") - return ctrl.Result{}, err - } - - rawTopology, err := kuadranttools.TopologyForPolicies(ctx, r.Client(), kuadrantv1beta3.NewRateLimitPolicyType()) - if err != nil { - return ctrl.Result{}, err - } - - for _, gw := range rawTopology.Gateways() { - topology, err := rlptools.ApplyOverrides(rawTopology, gw.GetGateway()) - if err != nil { - return ctrl.Result{}, err - } - envoyPolicy, err := r.desiredEnvoyExtensionPolicy(ctx, gw, kObj, topology) - if err != nil { - return ctrl.Result{}, err - } - err = r.ReconcileResource(ctx, &egv1alpha1.EnvoyExtensionPolicy{}, envoyPolicy, kuadrantenvoygateway.EnvoyExtensionPolicyMutator) - if err != nil { - return ctrl.Result{}, err - } - } - - logger.V(1).Info("Envoygateway wasm attachment reconciled successfully") - return ctrl.Result{}, nil -} - -func (r *EnvoyGatewayWasmReconciler) desiredEnvoyExtensionPolicy( - ctx context.Context, gw kuadrantgatewayapi.GatewayNode, - kObj *kuadrantv1beta1.Kuadrant, - topology *kuadrantgatewayapi.Topology) (*egv1alpha1.EnvoyExtensionPolicy, error) { - baseLogger, err := logr.FromContext(ctx) - if err != nil { - return nil, err - } - envoyPolicy := &egv1alpha1.EnvoyExtensionPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: egv1alpha1.KindEnvoyExtensionPolicy, - APIVersion: egv1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: EnvoyExtensionPolicyName(gw.GetName()), - Namespace: gw.GetNamespace(), - }, - Spec: egv1alpha1.EnvoyExtensionPolicySpec{ - PolicyTargetReferences: egv1alpha1.PolicyTargetReferences{ - TargetRefs: []gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - { - LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.Group(gatewayapiv1.GroupVersion.Group), - Kind: gatewayapiv1.Kind("Gateway"), - Name: gatewayapiv1.ObjectName(gw.GetName()), - }, - }, - }, - }, - Wasm: []egv1alpha1.Wasm{ - { - Name: ptr.To("kuadrant-wasm-shim"), - RootID: ptr.To("kuadrant_wasm_shim"), - Code: egv1alpha1.WasmCodeSource{ - Type: egv1alpha1.ImageWasmCodeSourceType, - Image: &egv1alpha1.ImageWasmCodeSource{ - URL: WASMFilterImageURL, - }, - }, - Config: nil, - // When a fatal error accurs during the initialization or the execution of the - // Wasm extension, if FailOpen is set to false the system blocks the traffic and returns - // an HTTP 5xx error. - FailOpen: ptr.To(false), - }, - }, - }, - } - - logger := baseLogger.WithValues("envoyextensionpolicy", client.ObjectKeyFromObject(envoyPolicy)) - - config, err := wasm.ConfigForGateway(ctx, gw.GetGateway(), topology) - if err != nil { - return nil, err - } - - if config == nil || len(config.Policies) == 0 { - logger.V(1).Info("config is empty. EnvoyExtensionPolicy will be deleted if it exists") - utils.TagObjectToDelete(envoyPolicy) - return envoyPolicy, nil - } - - configJSON, err := config.ToJSON() - if err != nil { - return nil, err - } - - envoyPolicy.Spec.Wasm[0].Config = configJSON - - kuadrant.AnnotateObject(envoyPolicy, kObj.GetNamespace()) - - // controller reference - if err := r.SetOwnerReference(gw.GetGateway(), envoyPolicy); err != nil { - return nil, err - } - - return envoyPolicy, nil -} - -func EnvoyExtensionPolicyName(targetName string) string { - return fmt.Sprintf("kuadrant-wasm-for-%s", targetName) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *EnvoyGatewayWasmReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantenvoygateway.IsEnvoyGatewayInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("EnvoyGateway Wasm controller disabled. EnvoyGateway API was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("EnvoyGateway Wasm controller disabled. GatewayAPI was not found") - return nil - } - - kuadrantListEventMapper := mappers.NewKuadrantListEventMapper( - mappers.WithLogger(r.Logger().WithName("envoyExtensionPolicyToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - policyToKuadrantEventMapper := mappers.NewPolicyToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("policyToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - gatewayToKuadrantEventMapper := mappers.NewGatewayToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("gatewayToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - httpRouteToKuadrantEventMapper := mappers.NewHTTPRouteToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("httpRouteToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - - return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantv1beta1.Kuadrant{}). - Watches( - &egv1alpha1.EnvoyExtensionPolicy{}, - handler.EnqueueRequestsFromMapFunc(kuadrantListEventMapper.Map), - ). - Watches( - &kuadrantv1beta3.RateLimitPolicy{}, - handler.EnqueueRequestsFromMapFunc(policyToKuadrantEventMapper.Map), - ). - Watches( - &gatewayapiv1.Gateway{}, - handler.EnqueueRequestsFromMapFunc(gatewayToKuadrantEventMapper.Map), - ). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteToKuadrantEventMapper.Map), - ). - Complete(r) -} diff --git a/controllers/istio_extension_reconciler.go b/controllers/istio_extension_reconciler.go new file mode 100644 index 000000000..5ff22ac85 --- /dev/null +++ b/controllers/istio_extension_reconciler.go @@ -0,0 +1,258 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + istioextensionsv1alpha1 "istio.io/api/extensions/v1alpha1" + istiov1beta1 "istio.io/api/type/v1beta1" + istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" + kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" +) + +// istioExtensionReconciler reconciles Istio WasmPlugin custom resources +type istioExtensionReconciler struct { + client *dynamic.DynamicClient +} + +func (r *istioExtensionReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + }, + } +} + +func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("istioExtensionReconciler") + + logger.V(1).Info("building istio extension") + defer logger.V(1).Info("finished building istio extension") + + // build wasm plugin configs for each gateway + wasmConfigs, err := r.buildWasmConfigs(ctx, state) + if err != nil { + if errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { + logger.V(1).Info(err.Error()) + } else { + return err + } + } + + // reconcile for each gateway based on the desired wasm plugin policies calculated before + gateways := lo.Map(topology.Targetables().Items(func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == machinery.GatewayGroupKind + }), func(g machinery.Targetable, _ int) *machinery.Gateway { + return g.(*machinery.Gateway) + }) + + var modifiedGateways []string + + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredWasmPlugin := buildIstioWasmPluginForGateway(gateway, wasmConfigs[gateway.GetLocator()]) + + resource := r.client.Resource(kuadrantistio.WasmPluginsResource).Namespace(desiredWasmPlugin.GetNamespace()) + + existingWasmPluginObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantistio.WasmPluginGroupKind && child.GetName() == desiredWasmPlugin.GetName() && child.GetNamespace() == desiredWasmPlugin.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredWasmPlugin.GetLabels())) + }) + + // create + if !found { + if utils.IsObjectTaggedToDelete(desiredWasmPlugin) { + continue + } + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when a wasmplugin is created, because updates won't change the status + desiredWasmPluginUnstructured, err := controller.Destruct(desiredWasmPlugin) + if err != nil { + logger.Error(err, "failed to destruct wasmplugin object", "gateway", gatewayKey.String(), "wasmplugin", desiredWasmPlugin) + continue + } + if _, err = resource.Create(ctx, desiredWasmPluginUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create wasmplugin object", "gateway", gatewayKey.String(), "wasmplugin", desiredWasmPluginUnstructured.Object) + // TODO: handle error + } + continue + } + + existingWasmPlugin := existingWasmPluginObj.(*controller.RuntimeObject).Object.(*istioclientgoextensionv1alpha1.WasmPlugin) + + // delete + if utils.IsObjectTaggedToDelete(desiredWasmPlugin) && !utils.IsObjectTaggedToDelete(existingWasmPlugin) { + if err := resource.Delete(ctx, existingWasmPlugin.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete wasmplugin object", "gateway", gatewayKey.String(), "wasmplugin", fmt.Sprintf("%s/%s", existingWasmPlugin.GetNamespace(), existingWasmPlugin.GetName())) + // TODO: handle error + } + continue + } + + if equalWasmPlugins(existingWasmPlugin, desiredWasmPlugin) { + logger.V(1).Info("wasmplugin object is up to date, nothing to do") + continue + } + + // update + existingWasmPlugin.Spec.Url = desiredWasmPlugin.Spec.Url + existingWasmPlugin.Spec.Phase = desiredWasmPlugin.Spec.Phase + existingWasmPlugin.Spec.TargetRefs = desiredWasmPlugin.Spec.TargetRefs + existingWasmPlugin.Spec.PluginConfig = desiredWasmPlugin.Spec.PluginConfig + + existingWasmPluginUnstructured, err := controller.Destruct(existingWasmPlugin) + if err != nil { + logger.Error(err, "failed to destruct wasmplugin object", "gateway", gatewayKey.String(), "wasmplugin", existingWasmPlugin) + continue + } + if _, err = resource.Update(ctx, existingWasmPluginUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update wasmplugin object", "gateway", gatewayKey.String(), "wasmplugin", existingWasmPluginUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateIstioExtensionsModified, modifiedGateways) + + return nil +} + +// buildWasmConfigs returns a map of istio gateway locators to an ordered list of corresponding wasm policies +func (r *istioExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { + logger := controller.LoggerFromContext(ctx).WithName("istioExtensionReconciler").WithName("buildWasmConfigs") + + effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + if !ok { + return nil, ErrMissingStateEffectiveRateLimitPolicies + } + + logger.V(1).Info("building wasm configs for istio extension", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + + wasmActionSets := kuadrantgatewayapi.GrouppedHTTPRouteMatchConfigs{} + + // build the wasm policies for each topological path that contains an effective rate limit policy affecting an istio gateway + for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + + // ignore if not an istio gateway + if gatewayClass.Spec.ControllerName != istioGatewayControllerName { + continue + } + + wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, effectivePolicy.Path, effectivePolicy.Spec.Rules(), rateLimitWasmActionBuilder(pathID, effectivePolicy, state)) + if err != nil { + logger.Error(err, "failed to build wasm policies for path", "pathID", pathID) + continue + } + wasmActionSets.Add(gateway.GetLocator(), wasmActionSetsForPath...) + } + + wasmConfigs := lo.MapValues(wasmActionSets.Sorted(), func(configs kuadrantgatewayapi.SortableHTTPRouteMatchConfigs, _ string) wasm.Config { + return wasm.BuildConfigForActionSet(lo.Map(configs, func(c kuadrantgatewayapi.HTTPRouteMatchConfig, _ int) wasm.ActionSet { + return c.Config.(wasm.ActionSet) + })) + }) + + return wasmConfigs, nil +} + +// buildIstioWasmPluginForGateway builds a desired WasmPlugin custom resource for a given gateway and corresponding wasm config +func buildIstioWasmPluginForGateway(gateway *machinery.Gateway, wasmConfig wasm.Config) *istioclientgoextensionv1alpha1.WasmPlugin { + wasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantistio.WasmPluginGroupKind.Kind, + APIVersion: istioclientgoextensionv1alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: wasm.ExtensionName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: KuadrantManagedObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: istioextensionsv1alpha1.WasmPlugin{ + TargetRefs: []*istiov1beta1.PolicyTargetReference{ + { + Group: machinery.GatewayGroupKind.Group, + Kind: machinery.GatewayGroupKind.Kind, + Name: gateway.GetName(), + }, + }, + Url: WASMFilterImageURL, + PluginConfig: nil, + Phase: istioextensionsv1alpha1.PluginPhase_STATS, // insert the plugin before Istio stats filters and after Istio authorization filters. + }, + } + + if len(wasmConfig.ActionSets) == 0 { + utils.TagObjectToDelete(wasmPlugin) + } else { + pluginConfigStruct, err := wasmConfig.ToStruct() + if err != nil { + return nil + } + wasmPlugin.Spec.PluginConfig = pluginConfigStruct + } + + return wasmPlugin +} + +func equalWasmPlugins(a, b *istioclientgoextensionv1alpha1.WasmPlugin) bool { + if a.Spec.Url != b.Spec.Url || a.Spec.Phase != b.Spec.Phase || !kuadrantistio.EqualTargetRefs(a.Spec.TargetRefs, b.Spec.TargetRefs) { + return false + } + + if a.Spec.PluginConfig == nil && b.Spec.PluginConfig == nil { + return true + } + + var err error + + var aConfig *wasm.Config + var bConfig *wasm.Config + + if a.Spec.PluginConfig != nil { + aConfig, err = wasm.ConfigFromStruct(a.Spec.PluginConfig) + if err != nil { + return false + } + } + + if b.Spec.PluginConfig != nil { + bConfig, err = wasm.ConfigFromStruct(b.Spec.PluginConfig) + if err != nil { + return false + } + } + + return aConfig != nil && bConfig != nil && aConfig.EqualTo(bConfig) +} diff --git a/controllers/istio_rate_limit_cluster_reconciler.go b/controllers/istio_rate_limit_cluster_reconciler.go new file mode 100644 index 000000000..10c20b792 --- /dev/null +++ b/controllers/istio_rate_limit_cluster_reconciler.go @@ -0,0 +1,275 @@ +package controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sync" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" + istiov1beta1 "istio.io/api/type/v1beta1" + istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" +) + +// istioRateLimitClusterReconciler reconciles Istio EnvoyFilter custom resources +type istioRateLimitClusterReconciler struct { + client *dynamic.DynamicClient +} + +func (r *istioRateLimitClusterReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + }, + } +} + +func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("istioRateLimitClusterReconciler") + + logger.V(1).Info("building istio rate limit clusters") + defer logger.V(1).Info("finished building istio rate limit clusters") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + limitadorObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.LimitadorGroupKind + }) + if !found { + logger.V(1).Info(ErrMissingLimitador.Error()) + return nil + } + limitador := limitadorObj.(*controller.RuntimeObject).Object.(*limitadorv1alpha1.Limitador) + + effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveRateLimitPolicies, "failed to get effective rate limit policies from state") + return nil + } + + gateways := lo.UniqBy(lo.FilterMap(lo.Values(effectivePolicies.(EffectiveRateLimitPolicies)), func(effectivePolicy EffectiveRateLimitPolicy, _ int) (*machinery.Gateway, bool) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + return gateway, gatewayClass.Spec.ControllerName == istioGatewayControllerName + }), func(gateway *machinery.Gateway) string { + return gateway.GetLocator() + }) + + desiredEnvoyFilters := make(map[k8stypes.NamespacedName]struct{}) + var modifiedGateways []string + + // reconcile istio cluster for gateway + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyFilter, err := r.buildDesiredEnvoyFilter(limitador, gateway) + if err != nil { + logger.Error(err, "failed to build desired envoy filter") + continue + } + desiredEnvoyFilters[k8stypes.NamespacedName{Name: desiredEnvoyFilter.GetName(), Namespace: desiredEnvoyFilter.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(desiredEnvoyFilter.GetNamespace()) + + existingEnvoyFilterObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && child.GetName() == desiredEnvoyFilter.GetName() && child.GetNamespace() == desiredEnvoyFilter.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyFilter.GetLabels())) + }) + + // create + if !found { + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoyfilter is created, because updates won't change the status + desiredEnvoyFilterUnstructured, err := controller.Destruct(desiredEnvoyFilter) + if err != nil { + logger.Error(err, "failed to destruct envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", desiredEnvoyFilter) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyFilterUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", desiredEnvoyFilterUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyFilter := existingEnvoyFilterObj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) + + if equalEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { + logger.V(1).Info("envoyfilter object is up to date, nothing to do") + continue + } + + // update + existingEnvoyFilter.Spec = istioapinetworkingv1alpha3.EnvoyFilter{ + TargetRefs: desiredEnvoyFilter.Spec.TargetRefs, + ConfigPatches: desiredEnvoyFilter.Spec.ConfigPatches, + Priority: desiredEnvoyFilter.Spec.Priority, + } + + existingEnvoyFilterUnstructured, err := controller.Destruct(existingEnvoyFilter) + if err != nil { + logger.Error(err, "failed to destruct envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", existingEnvoyFilter) + continue + } + if _, err = resource.Update(ctx, existingEnvoyFilterUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", existingEnvoyFilterUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateIstioRateLimitClustersModified, modifiedGateways) + + // cleanup istio clusters for gateways that are not in the effective policies + staleEnvoyFilters := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredEnvoyFilters[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(RateLimitObjectLabels()) && !desired + }) + + for _, envoyFilter := range staleEnvoyFilters { + if err := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(envoyFilter.GetNamespace()).Delete(ctx, envoyFilter.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoyfilter object", "envoyfilter", fmt.Sprintf("%s/%s", envoyFilter.GetNamespace(), envoyFilter.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { + envoyFilter := &istioclientgonetworkingv1alpha3.EnvoyFilter{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantistio.EnvoyFilterGroupKind.Kind, + APIVersion: istioclientgonetworkingv1alpha3.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: RateLimitClusterName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: RateLimitObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: istioapinetworkingv1alpha3.EnvoyFilter{ + TargetRefs: []*istiov1beta1.PolicyTargetReference{ + { + Group: machinery.GatewayGroupKind.Group, + Kind: machinery.GatewayGroupKind.Kind, + Name: gateway.GetName(), + }, + }, + }, + } + + configPatches, err := istioEnvoyFilterClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) + if err != nil { + return nil, err + } + envoyFilter.Spec.ConfigPatches = configPatches + + return envoyFilter, nil +} + +// istioEnvoyFilterClusterPatch returns an envoy config patch that defines the rate limit cluster for the gateway. +// The rate limit cluster configures the endpoint of the external rate limit service. +func istioEnvoyFilterClusterPatch(host string, port int) ([]*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { + patchRaw, _ := json.Marshal(map[string]any{"operation": "ADD", "value": rateLimitClusterPatch(host, port)}) + patch := &istioapinetworkingv1alpha3.EnvoyFilter_Patch{} + if err := patch.UnmarshalJSON(patchRaw); err != nil { + return nil, err + } + + return []*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ + { + ApplyTo: istioapinetworkingv1alpha3.EnvoyFilter_CLUSTER, + Match: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{ + Cluster: &istioapinetworkingv1alpha3.EnvoyFilter_ClusterMatch{ + Service: host, + }, + }, + }, + Patch: patch, + }, + }, nil +} + +func equalEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { + if a.Spec.Priority != b.Spec.Priority || !kuadrantistio.EqualTargetRefs(a.Spec.TargetRefs, b.Spec.TargetRefs) { + return false + } + + aConfigPatches := a.Spec.ConfigPatches + bConfigPatches := b.Spec.ConfigPatches + if len(aConfigPatches) != len(bConfigPatches) { + return false + } + return lo.EveryBy(aConfigPatches, func(aConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { + return lo.SomeBy(bConfigPatches, func(bConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { + if aConfigPatch == nil && bConfigPatch == nil { + return true + } + if (aConfigPatch == nil && bConfigPatch != nil) || (aConfigPatch != nil && bConfigPatch == nil) { + return false + } + + // apply_to + if aConfigPatch.ApplyTo != bConfigPatch.ApplyTo { + return false + } + + // cluster match + aCluster := aConfigPatch.Match.GetCluster() + bCluster := bConfigPatch.Match.GetCluster() + if aCluster == nil || bCluster == nil { + return false + } + if aCluster.Service != bCluster.Service || aCluster.PortNumber != bCluster.PortNumber || aCluster.Subset != bCluster.Subset { + return false + } + + // patch + aPatch := aConfigPatch.Patch + bPatch := bConfigPatch.Patch + + if aPatch.Operation != bPatch.Operation || aPatch.FilterClass != bPatch.FilterClass { + return false + } + + aPatchJSON, _ := aPatch.Value.MarshalJSON() + bPatchJSON, _ := aPatch.Value.MarshalJSON() + return string(aPatchJSON) == string(bPatchJSON) + }) + }) +} diff --git a/controllers/limitador_cluster_envoyfilter_controller.go b/controllers/limitador_cluster_envoyfilter_controller.go deleted file mode 100644 index 689221dcf..000000000 --- a/controllers/limitador_cluster_envoyfilter_controller.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright 2021 Red Hat, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" - istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/predicate" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantistioutils "github.com/kuadrant/kuadrant-operator/pkg/istio" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -// LimitadorClusterEnvoyFilterReconciler reconciles a EnvoyFilter object with limitador's cluster -type LimitadorClusterEnvoyFilterReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=networking.istio.io,resources=envoyfilters,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/finalizers,verbs=update - -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile -func (r *LimitadorClusterEnvoyFilterReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("Gateway", req.NamespacedName) - logger.Info("Reconciling EnvoyFilter") - ctx := logr.NewContext(eventCtx, logger) - - gw := &gatewayapiv1.Gateway{} - if err := r.Client().Get(ctx, req.NamespacedName, gw); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no gateway found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get gateway") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(gw, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - err := r.reconcileRateLimitingClusterEnvoyFilter(ctx, gw) - - if err != nil { - return ctrl.Result{}, err - } - - logger.Info("EnvoyFilter reconciled successfully") - return ctrl.Result{}, nil -} - -func (r *LimitadorClusterEnvoyFilterReconciler) reconcileRateLimitingClusterEnvoyFilter(ctx context.Context, gw *gatewayapiv1.Gateway) error { - desired, err := r.desiredRateLimitingClusterEnvoyFilter(ctx, gw) - if err != nil { - return err - } - - err = r.ReconcileResource(ctx, &istioclientnetworkingv1alpha3.EnvoyFilter{}, desired, kuadrantistioutils.AlwaysUpdateEnvoyFilter) - if err != nil { - return err - } - - return nil -} - -func (r *LimitadorClusterEnvoyFilterReconciler) desiredRateLimitingClusterEnvoyFilter(ctx context.Context, gw *gatewayapiv1.Gateway) (*istioclientnetworkingv1alpha3.EnvoyFilter, error) { - logger, err := logr.FromContext(ctx) - if err != nil { - return nil, err - } - - ef := &istioclientnetworkingv1alpha3.EnvoyFilter{ - TypeMeta: metav1.TypeMeta{ - Kind: "EnvoyFilter", - APIVersion: "networking.istio.io/v1alpha3", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("kuadrant-ratelimiting-cluster-%s", gw.Name), - Namespace: gw.Namespace, - }, - Spec: istioapinetworkingv1alpha3.EnvoyFilter{ - WorkloadSelector: &istioapinetworkingv1alpha3.WorkloadSelector{ - Labels: kuadrantistioutils.WorkloadSelectorFromGateway(ctx, r.Client(), gw).MatchLabels, - }, - ConfigPatches: nil, - }, - } - - gateway := kuadrant.GatewayWrapper{Gateway: gw, Referrer: &kuadrantv1beta3.RateLimitPolicy{}} - rlpRefs := gateway.PolicyRefs() - logger.V(1).Info("desiredRateLimitingClusterEnvoyFilter", "rlpRefs", rlpRefs) - - if len(rlpRefs) < 1 { - utils.TagObjectToDelete(ef) - return ef, nil - } - - kuadrantNamespace, err := kuadrant.GetKuadrantNamespace(gw) - if err != nil { - return nil, err - } - - limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: kuadrantNamespace} - limitador := &limitadorv1alpha1.Limitador{} - err = r.Client().Get(ctx, limitadorKey, limitador) - logger.V(1).Info("desiredRateLimitingClusterEnvoyFilter", "get limitador", limitadorKey, "err", err) - if err != nil { - return nil, err - } - - if !meta.IsStatusConditionTrue(limitador.Status.Conditions, "Ready") { - return nil, fmt.Errorf("limitador Status not ready") - } - - configPatches, err := kuadrantistioutils.LimitadorClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) - if err != nil { - return nil, err - } - ef.Spec.ConfigPatches = configPatches - - // controller reference - if err := r.SetOwnerReference(gw, ef); err != nil { - return nil, err - } - - return ef, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *LimitadorClusterEnvoyFilterReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantistioutils.IsEnvoyFilterInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Istio EnvoyFilter controller disabled. Istio was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Istio EnvoyFilter controller disabled. GatewayAPI was not found") - return nil - } - - return ctrl.NewControllerManagedBy(mgr). - // Limitador cluster EnvoyFilter controller only cares about - // the annotation having references to RLP's - // kuadrant.io/ratelimitpolicies - For(&gatewayapiv1.Gateway{}, builder.WithPredicates(predicate.AnnotationChangedPredicate{})). - Owns(&istioclientnetworkingv1alpha3.EnvoyFilter{}). - Complete(r) -} diff --git a/controllers/limitador_limits_reconciler.go b/controllers/limitador_limits_reconciler.go new file mode 100644 index 000000000..1779fb56d --- /dev/null +++ b/controllers/limitador_limits_reconciler.go @@ -0,0 +1,122 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "sync" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" + "github.com/kuadrant/kuadrant-operator/pkg/ratelimit" +) + +type limitadorLimitsReconciler struct { + client *dynamic.DynamicClient +} + +func (r *limitadorLimitsReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: rateLimitEventMatchers, + } +} + +func (r *limitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("limitadorLimitsReconciler") + + limitador, err := GetLimitadorFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) || errors.Is(err, ErrMissingLimitador) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + desiredLimits, err := r.buildLimitadorLimits(ctx, state) + if err != nil { + logger.Error(err, "failed to build limitador limits") + return nil + } + + if ratelimit.LimitadorRateLimits(limitador.Spec.Limits).EqualTo(desiredLimits) { + logger.V(1).Info("limitador object is up to date, nothing to do") + return nil + } + + state.Store(StateLimitadorLimitsModified, true) + + limitador.Spec.Limits = desiredLimits + + obj, err := controller.Destruct(limitador) + if err != nil { + return err // should never happen + } + + logger.V(1).Info("updating limitador object", "limitador", obj.Object) + + if _, err := r.client.Resource(kuadrantv1beta1.LimitadorsResource).Namespace(limitador.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update limitador object") + // TODO: handle error + } + + logger.V(1).Info("finished updating limitador object", "limitador", (k8stypes.NamespacedName{Name: limitador.GetName(), Namespace: limitador.GetNamespace()}).String()) + + return nil +} + +func (r *limitadorLimitsReconciler) buildLimitadorLimits(ctx context.Context, state *sync.Map) ([]limitadorv1alpha1.RateLimit, error) { + logger := controller.LoggerFromContext(ctx).WithName("limitadorLimitsReconciler").WithName("buildLimitadorLimits") + + effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + if !ok { + return nil, ErrMissingStateEffectiveRateLimitPolicies + } + + logger.V(1).Info("building limitador limits", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + + rateLimitIndex := ratelimit.NewIndex() + + for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { + _, _, _, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + limitsNamespace := LimitsNamespaceFromRoute(httpRoute.HTTPRoute) + for limitKey, mergeableLimit := range effectivePolicy.Spec.Rules() { + policy, found := lo.Find(kuadrantv1.PoliciesInPath(effectivePolicy.Path, isRateLimitPolicyAcceptedAndNotDeletedFunc(state)), func(p machinery.Policy) bool { + return p.GetLocator() == mergeableLimit.Source + }) + if !found { // should never happen + logger.Error(fmt.Errorf("origin policy %s not found in path %s", mergeableLimit.Source, pathID), "failed to build limitador limit definition") + continue + } + limitIdentifier := LimitNameToLimitadorIdentifier(k8stypes.NamespacedName{Name: policy.GetName(), Namespace: policy.GetNamespace()}, limitKey) + limit := mergeableLimit.Spec.(kuadrantv1beta3.Limit) + rateLimits := lo.Map(limit.Rates, func(rate kuadrantv1beta3.Rate, _ int) limitadorv1alpha1.RateLimit { + maxValue, seconds := rate.ToSeconds() + return limitadorv1alpha1.RateLimit{ + Namespace: limitsNamespace, + MaxValue: maxValue, + Seconds: seconds, + Conditions: []string{fmt.Sprintf("%s == \"1\"", limitIdentifier)}, + Variables: utils.GetEmptySliceIfNil(limit.CountersAsStringList()), + } + }) + rateLimitIndex.Set(fmt.Sprintf("%s/%s", limitsNamespace, limitIdentifier), rateLimits) + } + } + + logger.V(1).Info("finished building limitador limits", "limits", rateLimitIndex.Len()) + + return rateLimitIndex.ToRateLimits(), nil +} diff --git a/controllers/limitador_reconciler.go b/controllers/limitador_reconciler.go index 7660d81d0..daf8e3e68 100644 --- a/controllers/limitador_reconciler.go +++ b/controllers/limitador_reconciler.go @@ -41,9 +41,9 @@ func (r *LimitadorReconciler) Reconcile(ctx context.Context, _ []controller.Reso logger.Info("reconciling limtador resource", "status", "started") defer logger.Info("reconciling limitador resource", "status", "completed") - kobj, err := GetKuadrant(topology) + kobj, err := GetKuadrantFromTopology(topology) if err != nil { - if errors.Is(err, ErrNoKandrantResource) { + if errors.Is(err, ErrMissingKuadrant) { logger.Info("kuadrant resource not found, ignoring", "status", "skipping") return err } diff --git a/controllers/rate_limiting_istio_wasmplugin_controller.go b/controllers/rate_limiting_istio_wasmplugin_controller.go deleted file mode 100644 index f0b84e54f..000000000 --- a/controllers/rate_limiting_istio_wasmplugin_controller.go +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright 2021 Red Hat, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - "github.com/google/uuid" - istioextensionsv1alpha1 "istio.io/api/extensions/v1alpha1" - istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/env" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantistioutils "github.com/kuadrant/kuadrant-operator/pkg/istio" - "github.com/kuadrant/kuadrant-operator/pkg/kuadranttools" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" -) - -var ( - WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") -) - -func WASMPluginName(gw *gatewayapiv1.Gateway) string { - return fmt.Sprintf("kuadrant-%s", gw.Name) -} - -// RateLimitingIstioWASMPluginReconciler reconciles a WASMPlugin object for rate limiting -type RateLimitingIstioWASMPluginReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=extensions.istio.io,resources=wasmplugins,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;update;patch -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;update;patch -//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;update;patch - -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile -func (r *RateLimitingIstioWASMPluginReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("Gateway", req.NamespacedName, "request id", uuid.NewString()) - logger.Info("Reconciling rate limiting WASMPlugin") - ctx := logr.NewContext(eventCtx, logger) - - gw := &gatewayapiv1.Gateway{} - if err := r.Client().Get(ctx, req.NamespacedName, gw); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no gateway found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get gateway") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(gw, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - desired, err := r.desiredRateLimitingWASMPlugin(ctx, gw) - if err != nil { - return ctrl.Result{}, err - } - - err = r.ReconcileResource(ctx, &istioclientgoextensionv1alpha1.WasmPlugin{}, desired, kuadrantistioutils.WASMPluginMutator) - if err != nil { - return ctrl.Result{}, err - } - - logger.Info("Rate limiting WASMPlugin reconciled successfully") - return ctrl.Result{}, nil -} - -func (r *RateLimitingIstioWASMPluginReconciler) desiredRateLimitingWASMPlugin(ctx context.Context, gw *gatewayapiv1.Gateway) (*istioclientgoextensionv1alpha1.WasmPlugin, error) { - baseLogger, err := logr.FromContext(ctx) - if err != nil { - return nil, err - } - - wasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{ - TypeMeta: metav1.TypeMeta{ - Kind: "WasmPlugin", - APIVersion: "extensions.istio.io/v1alpha1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: WASMPluginName(gw), - Namespace: gw.Namespace, - }, - Spec: istioextensionsv1alpha1.WasmPlugin{ - TargetRef: kuadrantistioutils.PolicyTargetRefFromGateway(gw), - Url: WASMFilterImageURL, - PluginConfig: nil, - // Insert plugin before Istio stats filters and after Istio authorization filters. - Phase: istioextensionsv1alpha1.PluginPhase_STATS, - }, - } - - logger := baseLogger.WithValues("wasmplugin", client.ObjectKeyFromObject(wasmPlugin)) - - pluginConfig, err := r.wasmPluginConfig(ctx, gw) - if err != nil { - return nil, err - } - - if pluginConfig == nil || len(pluginConfig.Policies) == 0 { - logger.V(1).Info("pluginConfig is empty. Wasmplugin will be deleted if it exists") - utils.TagObjectToDelete(wasmPlugin) - return wasmPlugin, nil - } - - pluginConfigStruct, err := pluginConfig.ToStruct() - if err != nil { - return nil, err - } - - wasmPlugin.Spec.PluginConfig = pluginConfigStruct - - // controller reference - if err := r.SetOwnerReference(gw, wasmPlugin); err != nil { - return nil, err - } - - return wasmPlugin, nil -} - -func (r *RateLimitingIstioWASMPluginReconciler) wasmPluginConfig(ctx context.Context, gw *gatewayapiv1.Gateway) (*wasm.Config, error) { - rawTopology, err := kuadranttools.TopologyFromGateway(ctx, r.Client(), gw, kuadrantv1beta3.NewRateLimitPolicyType()) - if err != nil { - return nil, err - } - - topology, err := rlptools.ApplyOverrides(rawTopology, gw) - if err != nil { - return nil, err - } - - config, err := wasm.ConfigForGateway(ctx, gw, topology) - if err != nil { - return nil, err - } - - return config, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *RateLimitingIstioWASMPluginReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantistioutils.IsWASMPluginInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Istio WasmPlugin controller disabled. Istio was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Istio WasmPlugin controller disabled. GatewayAPI was not found") - return nil - } - - httpRouteToParentGatewaysEventMapper := mappers.NewHTTPRouteToParentGatewaysEventMapper( - mappers.WithLogger(r.Logger().WithName("httpRouteToParentGatewaysEventMapper")), - ) - - rlpToParentGatewaysEventMapper := mappers.NewPolicyToParentGatewaysEventMapper( - mappers.WithLogger(r.Logger().WithName("ratelimitpolicyToParentGatewaysEventMapper")), - mappers.WithClient(r.Client()), - ) - - return ctrl.NewControllerManagedBy(mgr). - // Rate limiting WASMPlugin controller only cares about - // Gateway API Gateway - // Gateway API HTTPRoutes - // Kuadrant RateLimitPolicies - - // The type of object being *reconciled* is the Gateway. - // TODO(eguzki): consider having the WasmPlugin as the type of object being *reconciled* - For(&gatewayapiv1.Gateway{}). - Owns(&istioclientgoextensionv1alpha1.WasmPlugin{}). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteToParentGatewaysEventMapper.Map), - ). - Watches( - &kuadrantv1beta3.RateLimitPolicy{}, - handler.EnqueueRequestsFromMapFunc(rlpToParentGatewaysEventMapper.Map), - ). - Complete(r) -} diff --git a/controllers/ratelimit_workflow.go b/controllers/ratelimit_workflow.go index eeb4be09b..de4ec0ccc 100644 --- a/controllers/ratelimit_workflow.go +++ b/controllers/ratelimit_workflow.go @@ -1,7 +1,280 @@ package controllers -import "github.com/kuadrant/policy-machinery/controller" +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "sync" + "unicode" -func NewRateLimitWorkflow() *controller.Workflow { - return &controller.Workflow{} + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/env" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" +) + +const ( + rateLimitClusterLabelKey = "kuadrant.io/rate-limit-cluster" + + // make these configurable? + istioGatewayControllerName = "istio.io/gateway-controller" + envoyGatewayGatewayControllerName = "gateway.envoyproxy.io/gatewayclass-controller" +) + +var ( + WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") + + StateRateLimitPolicyValid = "RateLimitPolicyValid" + StateEffectiveRateLimitPolicies = "EffectiveRateLimitPolicies" + StateLimitadorLimitsModified = "LimitadorLimitsModified" + StateIstioRateLimitClustersModified = "IstioRateLimitClustersModified" + StateIstioExtensionsModified = "IstioExtensionsModified" + StateEnvoyGatewayRateLimitClustersModified = "EnvoyGatewayRateLimitClustersModified" + StateEnvoyGatewayExtensionsModified = "EnvoyGatewayExtensionsModified" + + ErrMissingLimitador = fmt.Errorf("missing limitador object in the topology") + ErrMissingStateEffectiveRateLimitPolicies = fmt.Errorf("missing rate limit effective policies stored in the reconciliation state") + + rateLimitEventMatchers = []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + } +) + +//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies/finalizers,verbs=update +//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors,verbs=get;list;watch;create;update;patch;delete + +func NewRateLimitWorkflow(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled bool) *controller.Workflow { + effectiveRateLimitPoliciesWorkflow := &controller.Workflow{ + Precondition: (&effectiveRateLimitPolicyReconciler{client: client}).Subscription().Reconcile, + Tasks: []controller.ReconcileFunc{ + (&limitadorLimitsReconciler{client: client}).Subscription().Reconcile, + }, + } + + if isIstioInstalled { + effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&istioRateLimitClusterReconciler{client: client}).Subscription().Reconcile) + effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&istioExtensionReconciler{client: client}).Subscription().Reconcile) + } + + if isEnvoyGatewayInstalled { + effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&envoyGatewayRateLimitClusterReconciler{client: client}).Subscription().Reconcile) + effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&envoyGatewayExtensionReconciler{client: client}).Subscription().Reconcile) + } + + return &controller.Workflow{ + Precondition: (&rateLimitPolicyValidator{}).Subscription().Reconcile, + Tasks: []controller.ReconcileFunc{effectiveRateLimitPoliciesWorkflow.Run}, + Postcondition: (&rateLimitPolicyStatusUpdater{client: client}).Subscription().Reconcile, + } +} + +func GetLimitadorFromTopology(topology *machinery.Topology) (*limitadorv1alpha1.Limitador, error) { + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + return nil, err + } + + limitadorObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.LimitadorGroupKind + }) + if !found { + return nil, ErrMissingLimitador + } + + limitador := limitadorObj.(*controller.RuntimeObject).Object.(*limitadorv1alpha1.Limitador) + return limitador, nil +} + +func LimitsNamespaceFromRoute(route *gatewayapiv1.HTTPRoute) string { + return k8stypes.NamespacedName{Name: route.GetName(), Namespace: route.GetNamespace()}.String() +} + +func LimitNameToLimitadorIdentifier(rlpKey k8stypes.NamespacedName, uniqueLimitName string) string { + identifier := "limit." + + // sanitize chars that are not allowed in limitador identifiers + for _, c := range uniqueLimitName { + if unicode.IsLetter(c) || unicode.IsDigit(c) || c == '_' { + identifier += string(c) + } else { + identifier += "_" + } + } + + // to avoid breaking the uniqueness of the limit name after sanitization, we add a hash of the original name + hash := sha256.Sum256([]byte(fmt.Sprintf("%s/%s", rlpKey.String(), uniqueLimitName))) + identifier += "__" + hex.EncodeToString(hash[:4]) + + return identifier +} + +func RateLimitObjectLabels() labels.Set { + m := KuadrantManagedObjectLabels() + m[rateLimitClusterLabelKey] = "true" + return m +} + +func RateLimitClusterName(gatewayName string) string { + return fmt.Sprintf("kuadrant-ratelimiting-%s", gatewayName) +} + +func rateLimitClusterPatch(host string, port int) map[string]any { + return map[string]any{ + "name": common.KuadrantRateLimitClusterName, + "type": "STRICT_DNS", + "connect_timeout": "1s", + "lb_policy": "ROUND_ROBIN", + "http2_protocol_options": map[string]any{}, + "load_assignment": map[string]any{ + "cluster_name": common.KuadrantRateLimitClusterName, + "endpoints": []map[string]any{ + { + "lb_endpoints": []map[string]any{ + { + "endpoint": map[string]any{ + "address": map[string]any{ + "socket_address": map[string]any{ + "address": host, + "port_value": port, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func rateLimitWasmActionBuilder(pathID string, effectivePolicy EffectiveRateLimitPolicy, state *sync.Map) wasm.ActionBuilderFunc { + policiesInPath := kuadrantv1.PoliciesInPath(effectivePolicy.Path, isRateLimitPolicyAcceptedAndNotDeletedFunc(state)) + _, _, _, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + limitsNamespace := LimitsNamespaceFromRoute(httpRoute.HTTPRoute) + return func(uniquePolicyRuleKey string, policyRule kuadrantv1.MergeableRule) (wasm.Action, error) { + source, found := lo.Find(policiesInPath, func(p machinery.Policy) bool { + return p.GetLocator() == policyRule.Source + }) + if !found { // should never happen + return wasm.Action{}, fmt.Errorf("could not find source policy %s in path %s", policyRule.Source, pathID) + } + limitIdentifier := LimitNameToLimitadorIdentifier(k8stypes.NamespacedName{Name: source.GetName(), Namespace: source.GetNamespace()}, uniquePolicyRuleKey) + limit := policyRule.Spec.(kuadrantv1beta3.Limit) + return wasmActionFromLimit(limit, limitIdentifier, limitsNamespace), nil + } +} + +// wasmActionFromLimit builds a wasm rate-limit action for a given limit. +// Conditions are built from the limit top-level conditions. +// +// The only action of the rule is the ratelimit service, whose data includes the activation of the limit +// and any counter qualifier of the limit. +func wasmActionFromLimit(limit kuadrantv1beta3.Limit, limitIdentifier, scope string) wasm.Action { + action := wasm.Action{ + ServiceName: wasm.RateLimitServiceName, + Scope: scope, + } + if conditions := wasm.PredicatesFromWhenConditions(limit.When...); len(conditions) > 0 { + action.Conditions = conditions + } + if data := wasmDataFromLimit(limitIdentifier, limit); len(data) > 0 { + action.Data = data + } + return action +} + +func wasmDataFromLimit(limitIdentifier string, limit kuadrantv1beta3.Limit) (data []wasm.DataType) { + // static key representing the limit + data = append(data, + wasm.DataType{ + Value: &wasm.Static{ + Static: wasm.StaticSpec{Key: limitIdentifier, Value: "1"}, + }, + }, + ) + + for _, counter := range limit.Counters { + data = append(data, + wasm.DataType{ + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{Selector: counter}, + }, + }, + ) + } + + return data +} + +func isRateLimitPolicyAcceptedAndNotDeletedFunc(state *sync.Map) func(machinery.Policy) bool { + f := isRateLimitPolicyAcceptedFunc(state) + return func(policy machinery.Policy) bool { + p, object := policy.(metav1.Object) + return object && f(policy) && p.GetDeletionTimestamp() == nil + } +} + +func isRateLimitPolicyAcceptedFunc(state *sync.Map) func(machinery.Policy) bool { + f := rateLimitPolicyAcceptedStatusFunc(state) + return func(policy machinery.Policy) bool { + accepted, _ := f(policy) + return accepted + } +} + +func rateLimitPolicyAcceptedStatusFunc(state *sync.Map) func(policy machinery.Policy) (bool, error) { + validatedPolicies, validated := state.Load(StateRateLimitPolicyValid) + if !validated { + return rateLimitPolicyAcceptedStatus + } + validatedPoliciesMap := validatedPolicies.(map[string]error) + return func(policy machinery.Policy) (bool, error) { + err, validated := validatedPoliciesMap[policy.GetLocator()] + if validated { + return err == nil, err + } + return rateLimitPolicyAcceptedStatus(policy) + } +} + +func rateLimitPolicyAcceptedStatus(policy machinery.Policy) (accepted bool, err error) { + p, ok := policy.(*kuadrantv1beta3.RateLimitPolicy) + if !ok { + return + } + if condition := meta.FindStatusCondition(p.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)); condition != nil { + accepted = condition.Status == metav1.ConditionTrue + if !accepted { + err = fmt.Errorf(condition.Message) + } + return + } + return } diff --git a/controllers/ratelimit_workflow_test.go b/controllers/ratelimit_workflow_test.go new file mode 100644 index 000000000..759babe5f --- /dev/null +++ b/controllers/ratelimit_workflow_test.go @@ -0,0 +1,180 @@ +//go:build unit + +package controllers + +import ( + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + k8stypes "k8s.io/apimachinery/pkg/types" + + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" +) + +func TestLimitNameToLimitadorIdentifier(t *testing.T) { + testCases := []struct { + name string + rlpKey k8stypes.NamespacedName + uniqueLimitName string + expected *regexp.Regexp + }{ + { + name: "prepends the limitador limit identifier prefix", + rlpKey: k8stypes.NamespacedName{Namespace: "testNS", Name: "rlpA"}, + uniqueLimitName: "foo", + expected: regexp.MustCompile(`^limit\.foo.+`), + }, + { + name: "sanitizes invalid chars", + rlpKey: k8stypes.NamespacedName{Namespace: "testNS", Name: "rlpA"}, + uniqueLimitName: "my/limit-0", + expected: regexp.MustCompile(`^limit\.my_limit_0.+$`), + }, + { + name: "sanitizes the dot char (.) even though it is a valid char in limitador identifiers", + rlpKey: k8stypes.NamespacedName{Namespace: "testNS", Name: "rlpA"}, + uniqueLimitName: "my.limit", + expected: regexp.MustCompile(`^limit\.my_limit.+$`), + }, + { + name: "appends a hash of the original name to avoid breaking uniqueness", + rlpKey: k8stypes.NamespacedName{Namespace: "testNS", Name: "rlpA"}, + uniqueLimitName: "foo", + expected: regexp.MustCompile(`^.+__1da6e70a$`), + }, + { + name: "different rlp keys result in different identifiers", + rlpKey: k8stypes.NamespacedName{Namespace: "testNS", Name: "rlpB"}, + uniqueLimitName: "foo", + expected: regexp.MustCompile(`^.+__2c1520b6$`), + }, + { + name: "empty string", + rlpKey: k8stypes.NamespacedName{Namespace: "testNS", Name: "rlpA"}, + uniqueLimitName: "", + expected: regexp.MustCompile(`^limit.__6d5e49dc$`), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(subT *testing.T) { + identifier := LimitNameToLimitadorIdentifier(tc.rlpKey, tc.uniqueLimitName) + if !tc.expected.MatchString(identifier) { + subT.Errorf("identifier does not match, expected(%s), got (%s)", tc.expected, identifier) + } + }) + } +} + +func TestWasmActionFromLimit(t *testing.T) { + testCases := []struct { + name string + limit kuadrantv1beta3.Limit + limitIdentifier string + scope string + expectedAction wasm.Action + }{ + { + name: "limit without conditions nor counters", + limit: kuadrantv1beta3.Limit{}, + limitIdentifier: "limit.myLimit__d681f6c3", + scope: "my-ns/my-route", + expectedAction: wasm.Action{ + ServiceName: wasm.RateLimitServiceName, + Scope: "my-ns/my-route", + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: "limit.myLimit__d681f6c3", + Value: "1", + }, + }, + }, + }, + }, + }, + { + name: "limit with counter qualifiers", + limit: kuadrantv1beta3.Limit{ + Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, + }, + limitIdentifier: "limit.myLimit__d681f6c3", + scope: "my-ns/my-route", + expectedAction: wasm.Action{ + ServiceName: wasm.RateLimitServiceName, + Scope: "my-ns/my-route", + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: "limit.myLimit__d681f6c3", + Value: "1", + }, + }, + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: "auth.identity.username", + }, + }, + }, + }, + }, + }, + { + name: "limit with counter qualifiers and when conditions", + limit: kuadrantv1beta3.Limit{ + Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, + When: []kuadrantv1beta3.WhenCondition{ + { + Selector: kuadrantv1beta3.ContextSelector("auth.identity.group"), + Operator: kuadrantv1beta3.NotEqualOperator, + Value: "admin", + }, + }, + }, + limitIdentifier: "limit.myLimit__d681f6c3", + scope: "my-ns/my-route", + expectedAction: wasm.Action{ + ServiceName: wasm.RateLimitServiceName, + Scope: "my-ns/my-route", + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: "limit.myLimit__d681f6c3", + Value: "1", + }, + }, + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: "auth.identity.username", + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + computedRule := wasmActionFromLimit(tc.limit, tc.limitIdentifier, tc.scope) + if diff := cmp.Diff(tc.expectedAction, computedRule); diff != "" { + t.Errorf("unexpected wasm rule (-want +got):\n%s", diff) + } + }) + } +} diff --git a/controllers/ratelimitpolicies_validator.go b/controllers/ratelimitpolicies_validator.go new file mode 100644 index 000000000..76a89d3da --- /dev/null +++ b/controllers/ratelimitpolicies_validator.go @@ -0,0 +1,59 @@ +package controllers + +import ( + "context" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + kuadrant "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type rateLimitPolicyValidator struct{} + +func (r *rateLimitPolicyValidator) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Validate, + Events: []controller.ResourceEventMatcher{ + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind, EventType: ptr.To(controller.CreateEvent)}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind, EventType: ptr.To(controller.UpdateEvent)}, + }, + } +} + +func (r *rateLimitPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("rateLimitPolicyValidator") + + policies := topology.Policies().Items(func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == kuadrantv1beta3.RateLimitPolicyGroupKind + }) + + logger.V(1).Info("validating rate limit policies", "policies", len(policies)) + defer logger.V(1).Info("finished validating rate limit policies") + + state.Store(StateRateLimitPolicyValid, lo.SliceToMap(policies, func(policy machinery.Policy) (string, error) { + var err error + if len(policy.GetTargetRefs()) > 0 && len(topology.Targetables().Children(policy)) == 0 { + ref := policy.GetTargetRefs()[0] + var res schema.GroupResource + switch ref.GroupVersionKind().Kind { + case machinery.GatewayGroupKind.Kind: + res = controller.GatewaysResource.GroupResource() + case machinery.HTTPRouteGroupKind.Kind: + res = controller.HTTPRoutesResource.GroupResource() + } + err = kuadrant.NewErrPolicyTargetNotFound(kuadrantv1beta3.RateLimitPolicyGroupKind.Kind, ref, apierrors.NewNotFound(res, ref.GetName())) + } + return policy.GetLocator(), err + })) + + return nil +} diff --git a/controllers/ratelimitpolicy_controller.go b/controllers/ratelimitpolicy_controller.go deleted file mode 100644 index 0a390eee2..000000000 --- a/controllers/ratelimitpolicy_controller.go +++ /dev/null @@ -1,253 +0,0 @@ -/* -Copyright 2021 Red Hat, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - "github.com/google/uuid" - apierrors "k8s.io/apimachinery/pkg/api/errors" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" -) - -const rateLimitPolicyFinalizer = "ratelimitpolicy.kuadrant.io/finalizer" - -// RateLimitPolicyReconciler reconciles a RateLimitPolicy object -type RateLimitPolicyReconciler struct { - *reconcilers.BaseReconciler - TargetRefReconciler reconcilers.TargetRefReconciler -} - -//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies/finalizers,verbs=update -//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors,verbs=get;list;watch;create;update;patch;delete - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the RateLimitPolicy object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile -func (r *RateLimitPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("RateLimitPolicy", req.NamespacedName, "request id", uuid.NewString()) - logger.Info("Reconciling RateLimitPolicy") - ctx := logr.NewContext(eventCtx, logger) - - // fetch the ratelimitpolicy - rlp := &kuadrantv1beta3.RateLimitPolicy{} - if err := r.Client().Get(ctx, req.NamespacedName, rlp); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no RateLimitPolicy found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get RateLimitPolicy") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(rlp, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - markedForDeletion := rlp.GetDeletionTimestamp() != nil - - // fetch the target network object - targetNetworkObject, err := reconcilers.FetchTargetRefObject(ctx, r.Client(), rlp.GetTargetRef(), rlp.Namespace, rlp.TargetProgrammedGatewaysOnly()) - if err != nil { - if !markedForDeletion { - if apierrors.IsNotFound(err) { - logger.V(1).Info("Network object not found. Cleaning up") - delResErr := r.deleteResources(ctx, rlp, nil) - if delResErr == nil { - delResErr = err - } - return r.reconcileStatus(ctx, rlp, kuadrant.NewErrTargetNotFound(rlp.Kind(), rlp.GetTargetRef(), delResErr)) - } - return ctrl.Result{}, err - } - targetNetworkObject = nil // we need the object set to nil when there's an error, otherwise deleting the resources (when marked for deletion) will panic - } - - // handle ratelimitpolicy marked for deletion - if markedForDeletion { - if controllerutil.ContainsFinalizer(rlp, rateLimitPolicyFinalizer) { - logger.V(1).Info("Handling removal of ratelimitpolicy object") - - if err := r.deleteResources(ctx, rlp, targetNetworkObject); err != nil { - return ctrl.Result{}, err - } - - logger.Info("removing finalizer") - if err := r.RemoveFinalizer(ctx, rlp, rateLimitPolicyFinalizer); err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil - } - - // add finalizer to the ratelimitpolicy - if !controllerutil.ContainsFinalizer(rlp, rateLimitPolicyFinalizer) { - controllerutil.AddFinalizer(rlp, rateLimitPolicyFinalizer) - if err := r.UpdateResource(ctx, rlp); client.IgnoreNotFound(err) != nil { - return ctrl.Result{Requeue: true}, err - } - } - - // reconcile the ratelimitpolicy spec - specErr := r.reconcileResources(ctx, rlp, targetNetworkObject) - - // reconcile ratelimitpolicy status - statusResult, statusErr := r.reconcileStatus(ctx, rlp, specErr) - - if specErr != nil { - return ctrl.Result{}, specErr - } - - if statusErr != nil { - return ctrl.Result{}, statusErr - } - - if statusResult.Requeue { - logger.V(1).Info("Reconciling status not finished. Requeueing.") - return statusResult, nil - } - - logger.Info("RateLimitPolicy reconciled successfully") - return ctrl.Result{}, nil -} - -// validate performs validation before proceeding with the reconcile loop, returning a common.ErrInvalid on failing validation -func (r *RateLimitPolicyReconciler) validate(rlp *kuadrantv1beta3.RateLimitPolicy, targetNetworkObject client.Object) error { - if err := kuadrant.ValidateHierarchicalRules(rlp, targetNetworkObject); err != nil { - return kuadrant.NewErrInvalid(rlp.Kind(), err) - } - - return nil -} - -func (r *RateLimitPolicyReconciler) reconcileResources(ctx context.Context, rlp *kuadrantv1beta3.RateLimitPolicy, targetNetworkObject client.Object) error { - if err := r.validate(rlp, targetNetworkObject); err != nil { - return err - } - - // reconcile based on gateway diffs - gatewayDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), rlp, targetNetworkObject) - if err != nil { - return err - } - - if err := r.reconcileLimits(ctx, rlp); err != nil { - return fmt.Errorf("reconcile Limitador error %w", err) - } - - // set direct back ref - i.e. claim the target network object as taken asap - if err := r.reconcileNetworkResourceDirectBackReference(ctx, rlp, targetNetworkObject); err != nil { - return fmt.Errorf("reconcile TargetBackReference error %w", err) - } - - // set annotation of policies affecting the gateway - should be the last step, only when all the reconciliation steps succeed - if err := r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, rlp, gatewayDiffObj); err != nil { - return fmt.Errorf("ReconcileGatewayPolicyReferences error %w", err) - } - - return nil -} - -func (r *RateLimitPolicyReconciler) deleteResources(ctx context.Context, rlp *kuadrantv1beta3.RateLimitPolicy, targetNetworkObject client.Object) error { - // delete based on gateway diffs - gatewayDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), rlp, targetNetworkObject) - if err != nil { - return err - } - - if err := r.deleteLimits(ctx, rlp); err != nil && !apierrors.IsNotFound(err) { - return err - } - - // remove direct back ref - if targetNetworkObject != nil { - if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject, rlp); err != nil { - return err - } - } - - // update annotation of policies affecting the gateway - return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, rlp, gatewayDiffObj) -} - -// Ensures only one RLP targets the network resource -func (r *RateLimitPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, policy *kuadrantv1beta3.RateLimitPolicy, targetNetworkObject client.Object) error { - return r.TargetRefReconciler.ReconcileTargetBackReference(ctx, policy, targetNetworkObject, policy.DirectReferenceAnnotationName()) -} - -func (r *RateLimitPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object, policy *kuadrantv1beta3.RateLimitPolicy) error { - return r.TargetRefReconciler.DeleteTargetBackReference(ctx, targetNetworkObject, policy.DirectReferenceAnnotationName()) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *RateLimitPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Ratelimitpolicy controller disabled. GatewayAPI was not found") - return nil - } - - httpRouteEventMapper := mappers.NewHTTPRouteEventMapper(mappers.WithLogger(r.Logger().WithName("httproute.mapper")), mappers.WithClient(mgr.GetClient())) - gatewayEventMapper := mappers.NewGatewayEventMapper( - kuadrantv1beta3.NewRateLimitPolicyType(), - mappers.WithLogger(r.Logger().WithName("gateway.mapper")), - mappers.WithClient(mgr.GetClient()), - ) - - return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantv1beta3.RateLimitPolicy{}). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { - return httpRouteEventMapper.MapToPolicy(ctx, object, kuadrantv1beta3.NewRateLimitPolicyType()) - }), - ). - // Currently the purpose is to generate events when rlp references change in gateways - // so the status of the rlps targeting a route can be keep in sync - Watches(&gatewayapiv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(gatewayEventMapper.Map)). - Complete(r) -} diff --git a/controllers/ratelimitpolicy_enforced_status_controller.go b/controllers/ratelimitpolicy_enforced_status_controller.go deleted file mode 100644 index d446262d0..000000000 --- a/controllers/ratelimitpolicy_enforced_status_controller.go +++ /dev/null @@ -1,311 +0,0 @@ -package controllers - -import ( - "context" - "errors" - "reflect" - "sort" - - "github.com/go-logr/logr" - "github.com/google/uuid" - "github.com/samber/lo" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/predicate" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/fieldindexers" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -type RateLimitPolicyEnforcedStatusReconciler struct { - *reconcilers.BaseReconciler -} - -func (r *RateLimitPolicyEnforcedStatusReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("gateway", req.NamespacedName, "request id", uuid.NewString()) - logger.Info("Reconciling policy status") - ctx := logr.NewContext(eventCtx, logger) - - gw := &gatewayapiv1.Gateway{} - if err := r.Client().Get(ctx, req.NamespacedName, gw); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no gateway found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get gateway") - return ctrl.Result{}, err - } - - topology, err := r.buildTopology(ctx, gw) - if err != nil { - return ctrl.Result{}, err - } - - indexes := kuadrantgatewayapi.NewTopologyIndexes(topology) - policies := indexes.PoliciesFromGateway(gw) - numRoutes := len(topology.Routes()) - numUntargetedRoutes := len(indexes.GetUntargetedRoutes(gw)) - - sort.Sort(kuadrantgatewayapi.PolicyByTargetRefKindAndCreationTimeStamp(policies)) - - // for each policy: - // if the policy is a gateway policy: - // and no route exists (numRoutes == 0) → set the Enforced condition of the gateway policy to 'false' (unknown) - // and the gateway contains routes (numRoutes > 0) - // and the gateway policy contains overrides → set the Enforced condition of the gateway policy to 'true' - // and the gateway policy contains defaults: - // and no routes have an attached policy (numUntargetedRoutes == numRoutes) → set the Enforced condition of the gateway policy to 'true' - // and some routes have attached policy (numUntargetedRoutes < numRoutes && numUntargetedRoutes > 1) → set the Enforced condition of the gateway policy to 'true' (partially enforced) - // and all routes have attached policy (numUntargetedRoutes == 0) → set the Enforced condition of the gateway policy to 'false' (overridden) - // if the policy is a route policy: - // and the route has no gateway parent (numGatewayParents == 0) → set the Enforced condition of the route policy to 'false' (unknown) - // and the route has gateway parents (numGatewayParents > 0) - // and all gateway parents of the route have gateway policies with overrides (numGatewayParentsWithOverrides == numGatewayParents) → set the Enforced condition of the route policy to 'false' (overridden) - // and some gateway parents of the route have gateway policies with overrides (numGatewayParentsWithOverrides < numGatewayParents && numGatewayParentsWithOverrides > 1) → set the Enforced condition of the route policy to 'true' (partially enforced) - // and no gateway parent of the route has gateway policies with overrides (numGatewayParentsWithOverrides == 0) → set the Enforced condition of the route policy to 'true' - - for i := range policies { - policy := policies[i] - rlpKey := client.ObjectKeyFromObject(policy) - rlp := policy.(*kuadrantv1beta3.RateLimitPolicy) - conditions := rlp.GetStatus().GetConditions() - - // skip policy if accepted condition is false - if meta.IsStatusConditionFalse(policy.GetStatus().GetConditions(), string(gatewayapiv1alpha2.PolicyConditionAccepted)) { - continue - } - - // ensure no error on underlying subresource (i.e. Limitador) - if condition := r.hasErrCondOnSubResource(ctx, rlp); condition != nil { - if err := r.setCondition(ctx, rlp, &conditions, *condition); err != nil { - return ctrl.Result{}, err - } - continue - } - - var condition *metav1.Condition - - if kuadrantgatewayapi.IsTargetRefGateway(rlp.GetTargetRef()) { // gateway policy - if numRoutes == 0 { - condition = kuadrant.EnforcedCondition(rlp, kuadrant.NewErrUnknown(rlp.Kind(), errors.New("no free routes to enforce policy")), true) // unknown - } else { - if rlp.Spec.Overrides != nil { - condition = kuadrant.EnforcedCondition(rlp, nil, true) // fully enforced - } else { - if numUntargetedRoutes == numRoutes { - condition = kuadrant.EnforcedCondition(rlp, nil, true) // fully enforced - } else if numUntargetedRoutes > 0 { - condition = kuadrant.EnforcedCondition(rlp, nil, false) // partially enforced - } else { - otherPolicies := lo.FilterMap(policies, func(p kuadrantgatewayapi.Policy, _ int) (client.ObjectKey, bool) { - key := client.ObjectKeyFromObject(p) - return key, key != rlpKey - }) - condition = kuadrant.EnforcedCondition(rlp, kuadrant.NewErrOverridden(rlp.Kind(), otherPolicies), true) // overridden - } - } - } - } else { // route policy - route := indexes.GetPolicyHTTPRoute(rlp) - gatewayParents := lo.FilterMap(kuadrantgatewayapi.GetRouteAcceptedGatewayParentKeys(route), func(parentKey client.ObjectKey, _ int) (*gatewayapiv1.Gateway, bool) { - g, found := utils.Find(topology.Gateways(), func(g kuadrantgatewayapi.GatewayNode) bool { return client.ObjectKeyFromObject(g.Gateway) == parentKey }) - if !found { - return nil, false - } - return g.Gateway, true - }) - numGatewayParents := len(gatewayParents) - if numGatewayParents == 0 { - condition = kuadrant.EnforcedCondition(rlp, kuadrant.NewErrUnknown(rlp.Kind(), errors.New("the targeted route has not been accepted by any gateway parent")), true) // unknown - } else { - var gatewayParentOverridePolicies []kuadrantgatewayapi.Policy - gatewayParentsWithOverrides := utils.Filter(gatewayParents, func(gatewayParent *gatewayapiv1.Gateway) bool { - _, found := utils.Find(indexes.PoliciesFromGateway(gatewayParent), func(p kuadrantgatewayapi.Policy) bool { - rlp := p.(*kuadrantv1beta3.RateLimitPolicy) - if kuadrantgatewayapi.IsTargetRefGateway(p.GetTargetRef()) && rlp != nil && rlp.Spec.Overrides != nil { - gatewayParentOverridePolicies = append(gatewayParentOverridePolicies, p) - return true - } - return false - }) - return found - }) - numGatewayParentsWithOverrides := len(gatewayParentsWithOverrides) - if numGatewayParentsWithOverrides == numGatewayParents { - sort.Sort(kuadrantgatewayapi.PolicyByTargetRefKindAndCreationTimeStamp(gatewayParentOverridePolicies)) - condition = kuadrant.EnforcedCondition(rlp, kuadrant.NewErrOverridden(rlp.Kind(), utils.Map(gatewayParentOverridePolicies, func(p kuadrantgatewayapi.Policy) client.ObjectKey { return client.ObjectKeyFromObject(p) })), true) // overridden - } else if numGatewayParentsWithOverrides > 0 { - condition = kuadrant.EnforcedCondition(rlp, nil, false) // partially enforced - } else { - condition = kuadrant.EnforcedCondition(rlp, nil, true) // fully enforced - } - } - } - - if err := r.setCondition(ctx, rlp, &conditions, *condition); err != nil { - return ctrl.Result{}, err - } - } - - logger.Info("Policy status reconciled successfully") - return ctrl.Result{}, nil -} - -func (r *RateLimitPolicyEnforcedStatusReconciler) buildTopology(ctx context.Context, gw *gatewayapiv1.Gateway) (*kuadrantgatewayapi.Topology, error) { - logger, err := logr.FromContext(ctx) - if err != nil { - return nil, err - } - - gatewayList := &gatewayapiv1.GatewayList{} - err = r.Client().List(ctx, gatewayList) - logger.V(1).Info("list gateways", "#gateways", len(gatewayList.Items), "err", err) - if err != nil { - return nil, err - } - - routeList := &gatewayapiv1.HTTPRouteList{} - // Get all the routes having the gateway as parent - err = r.Client().List( - ctx, - routeList, - client.MatchingFields{ - fieldindexers.HTTPRouteGatewayParentField: client.ObjectKeyFromObject(gw).String(), - }) - logger.V(1).Info("list routes by gateway", "#routes", len(routeList.Items), "err", err) - if err != nil { - return nil, err - } - - policyList := &kuadrantv1beta3.RateLimitPolicyList{} - err = r.Client().List(ctx, policyList) - logger.V(1).Info("list rate limit policies", "#policies", len(policyList.Items), "err", err) - if err != nil { - return nil, err - } - - return kuadrantgatewayapi.NewTopology( - kuadrantgatewayapi.WithAcceptedRoutesLinkedOnly(), - kuadrantgatewayapi.WithGateways(utils.Map(gatewayList.Items, ptr.To[gatewayapiv1.Gateway])), - kuadrantgatewayapi.WithRoutes(utils.Map(routeList.Items, ptr.To[gatewayapiv1.HTTPRoute])), - kuadrantgatewayapi.WithPolicies(utils.Map(policyList.Items, func(p kuadrantv1beta3.RateLimitPolicy) kuadrantgatewayapi.Policy { return &p })), - kuadrantgatewayapi.WithLogger(logger), - ) -} - -func (r *RateLimitPolicyEnforcedStatusReconciler) hasErrCondOnSubResource(ctx context.Context, p *kuadrantv1beta3.RateLimitPolicy) *metav1.Condition { - logger, err := logr.FromContext(ctx) - logger.WithName("hasErrCondOnSubResource") - if err != nil { - logger = r.Logger() - } - - limitador, err := GetLimitador(ctx, r.Client(), p) - if err != nil { - logger.V(1).Error(err, "failed to get limitador") - return kuadrant.EnforcedCondition(p, kuadrant.NewErrUnknown(p.Kind(), err), false) - } - if meta.IsStatusConditionFalse(limitador.Status.Conditions, "Ready") { - logger.V(1).Info("Limitador is not ready") - return kuadrant.EnforcedCondition(p, kuadrant.NewErrUnknown(p.Kind(), errors.New("limitador is not ready")), false) - } - - logger.V(1).Info("limitador is ready and enforcing limits") - return nil -} - -func (r *RateLimitPolicyEnforcedStatusReconciler) setCondition(ctx context.Context, p *kuadrantv1beta3.RateLimitPolicy, conditions *[]metav1.Condition, cond metav1.Condition) error { - logger, err := logr.FromContext(ctx) - logger.WithName("setCondition") - if err != nil { - logger = r.Logger() - } - - idx := utils.Index(*conditions, func(c metav1.Condition) bool { - return c.Type == cond.Type && c.Status == cond.Status && c.Reason == cond.Reason && c.Message == cond.Message - }) - if idx == -1 { - meta.SetStatusCondition(conditions, cond) - p.Status.Conditions = *conditions - if err := r.Client().Status().Update(ctx, p); err != nil { - logger.Error(err, "failed to update policy status") - return err - } - return nil - } - - logger.V(1).Info("skipping policy enforced condition status update - already up to date") - return nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *RateLimitPolicyEnforcedStatusReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("RateLimitPolicyEnforcedStatus controller disabled. GatewayAPI was not found") - return nil - } - - httpRouteToParentGatewaysEventMapper := mappers.NewHTTPRouteToParentGatewaysEventMapper( - mappers.WithLogger(r.Logger().WithName("httpRouteToParentGatewaysEventMapper")), - ) - - policyToParentGatewaysEventMapper := mappers.NewPolicyToParentGatewaysEventMapper( - mappers.WithLogger(r.Logger().WithName("policyToParentGatewaysEventMapper")), - mappers.WithClient(r.Client()), - ) - - limitadorStatusToParentGatewayEventMapper := limitadorStatusRLPGatewayEventHandler{ - Client: r.Client(), - Logger: r.Logger().WithName("limitadorStatusToRLPsEventHandler"), - } - - policyStatusChangedPredicate := predicate.Funcs{ - UpdateFunc: func(ev event.UpdateEvent) bool { - oldPolicy, ok := ev.ObjectOld.(kuadrantgatewayapi.Policy) - if !ok { - return false - } - newPolicy, ok := ev.ObjectNew.(kuadrantgatewayapi.Policy) - if !ok { - return false - } - oldStatus := oldPolicy.GetStatus() - newStatus := newPolicy.GetStatus() - return !reflect.DeepEqual(oldStatus, newStatus) - }, - } - - return ctrl.NewControllerManagedBy(mgr). - For(&gatewayapiv1.Gateway{}). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteToParentGatewaysEventMapper.Map), - ). - Watches( - &kuadrantv1beta3.RateLimitPolicy{}, - handler.EnqueueRequestsFromMapFunc(policyToParentGatewaysEventMapper.Map), - builder.WithPredicates(policyStatusChangedPredicate), - ). - Watches(&limitadorv1alpha1.Limitador{}, limitadorStatusToParentGatewayEventMapper). - Complete(r) -} diff --git a/controllers/ratelimitpolicy_limits.go b/controllers/ratelimitpolicy_limits.go deleted file mode 100644 index 442df9b87..000000000 --- a/controllers/ratelimitpolicy_limits.go +++ /dev/null @@ -1,198 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - "slices" - "sort" - - "github.com/go-logr/logr" - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "github.com/samber/lo" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools" -) - -func (r *RateLimitPolicyReconciler) reconcileLimits(ctx context.Context, rlp *kuadrantv1beta3.RateLimitPolicy) error { - policies, err := r.getPolicies(ctx) - if err != nil { - return err - } - topology, err := r.buildTopology(ctx, policies) - if err != nil { - return err - } - return r.reconcileLimitador(ctx, rlp, topology) -} - -func (r *RateLimitPolicyReconciler) deleteLimits(ctx context.Context, rlp *kuadrantv1beta3.RateLimitPolicy) error { - policies, err := r.getPolicies(ctx) - if err != nil { - return err - } - policiesWithoutRLP := utils.Filter(policies, func(policy kuadrantgatewayapi.Policy) bool { - return client.ObjectKeyFromObject(policy) != client.ObjectKeyFromObject(rlp) - }) - topology, err := r.buildTopology(ctx, policiesWithoutRLP) - if err != nil { - return err - } - return r.reconcileLimitador(ctx, rlp, topology) -} - -func (r *RateLimitPolicyReconciler) reconcileLimitador(ctx context.Context, rlp *kuadrantv1beta3.RateLimitPolicy, topology *kuadrantgatewayapi.Topology) error { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("reconcileLimitador") - - rateLimitIndex := r.buildRateLimitIndex(ctx, topology) - - // get the current limitador cr for the kuadrant instance so we can compare if it needs to be updated - limitador, err := GetLimitador(ctx, r.Client(), rlp) - if err != nil { - return err - } - // return if limitador is up-to-date - if rlptools.Equal(rateLimitIndex.ToRateLimits(), limitador.Spec.Limits) { - logger.V(1).Info("limitador is up to date, skipping update") - return nil - } - - // update limitador - limitador.Spec.Limits = rateLimitIndex.ToRateLimits() - err = r.UpdateResource(ctx, limitador) - logger.V(1).Info("update limitador", "limitador", client.ObjectKeyFromObject(limitador), "err", err) - if err != nil { - return err - } - - return nil -} - -func GetLimitador(ctx context.Context, k8sclient client.Client, rlp *kuadrantv1beta3.RateLimitPolicy) (*limitadorv1alpha1.Limitador, error) { - logger, _ := logr.FromContext(ctx) - - logger.V(1).Info("get kuadrant namespace") - var kuadrantNamespace string - kuadrantNamespace, isSet := kuadrant.GetKuadrantNamespaceFromPolicy(rlp) - if !isSet { - var err error - kuadrantNamespace, err = kuadrant.GetKuadrantNamespaceFromPolicyTargetRef(ctx, k8sclient, rlp) - if err != nil { - logger.Error(err, "failed to get kuadrant namespace") - return nil, err - } - kuadrant.AnnotateObject(rlp, kuadrantNamespace) - err = k8sclient.Update(ctx, rlp) // @guicassolato: not sure if this belongs to here - if err != nil { - logger.Error(err, "failed to update policy, re-queuing") - return nil, err - } - } - limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: kuadrantNamespace} - limitador := &limitadorv1alpha1.Limitador{} - err := k8sclient.Get(ctx, limitadorKey, limitador) - logger.V(1).Info("get limitador", "limitador", limitadorKey, "err", err) - if err != nil { - return nil, err - } - - return limitador, nil -} - -func (r *RateLimitPolicyReconciler) getPolicies(ctx context.Context) ([]kuadrantgatewayapi.Policy, error) { - logger, _ := logr.FromContext(ctx) - - rlpList := &kuadrantv1beta3.RateLimitPolicyList{} - err := r.Client().List(ctx, rlpList) - logger.V(1).Info("topology: list rate limit policies", "#RLPS", len(rlpList.Items), "err", err) - if err != nil { - return nil, err - } - - policies := utils.Map(rlpList.Items, func(p kuadrantv1beta3.RateLimitPolicy) kuadrantgatewayapi.Policy { return &p }) - - return policies, nil -} - -func (r *RateLimitPolicyReconciler) buildTopology(ctx context.Context, policies []kuadrantgatewayapi.Policy) (*kuadrantgatewayapi.Topology, error) { - logger, _ := logr.FromContext(ctx) - - gwList := &gatewayapiv1.GatewayList{} - err := r.Client().List(ctx, gwList) - logger.V(1).Info("topology: list gateways", "#Gateways", len(gwList.Items), "err", err) - if err != nil { - return nil, err - } - - routeList := &gatewayapiv1.HTTPRouteList{} - err = r.Client().List(ctx, routeList) - logger.V(1).Info("topology: list httproutes", "#HTTPRoutes", len(routeList.Items), "err", err) - if err != nil { - return nil, err - } - - return kuadrantgatewayapi.NewTopology( - kuadrantgatewayapi.WithAcceptedRoutesLinkedOnly(), - kuadrantgatewayapi.WithProgrammedGatewaysOnly(), - kuadrantgatewayapi.WithGateways(utils.Map(gwList.Items, ptr.To[gatewayapiv1.Gateway])), - kuadrantgatewayapi.WithRoutes(utils.Map(routeList.Items, ptr.To[gatewayapiv1.HTTPRoute])), - kuadrantgatewayapi.WithPolicies(policies), - kuadrantgatewayapi.WithLogger(logger), - ) -} - -func (r *RateLimitPolicyReconciler) buildRateLimitIndex(ctx context.Context, topology *kuadrantgatewayapi.Topology) *rlptools.RateLimitIndex { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("buildRateLimitIndex") - - gateways := lo.KeyBy(topology.Gateways(), func(gateway kuadrantgatewayapi.GatewayNode) string { - return client.ObjectKeyFromObject(gateway.Gateway).String() - }) - - // sort the gateways for deterministic output and consistent comparison against existing objects - gatewayNames := lo.Keys(gateways) - slices.Sort(gatewayNames) - - rateLimitIndex := rlptools.NewRateLimitIndex() - - for _, gatewayName := range gatewayNames { - gateway := gateways[gatewayName].Gateway - topologyWithOverrides, err := rlptools.ApplyOverrides(topology, gateway) - if err != nil { - logger.Error(err, "failed to apply overrides") - return nil - } - - // sort the policies for deterministic output and consistent comparison against existing objects - indexes := kuadrantgatewayapi.NewTopologyIndexes(topologyWithOverrides) - policies := indexes.PoliciesFromGateway(gateway) - sort.Sort(kuadrantgatewayapi.PolicyByTargetRefKindAndCreationTimeStamp(policies)) - - logger.V(1).Info("new rate limit index", "gateway", client.ObjectKeyFromObject(gateway), "policies", lo.Map(policies, func(p kuadrantgatewayapi.Policy, _ int) string { return client.ObjectKeyFromObject(p).String() })) - - for _, policy := range policies { - rlpKey := client.ObjectKeyFromObject(policy) - gatewayKey := client.ObjectKeyFromObject(gateway) - key := rlptools.RateLimitIndexKey{ - RateLimitPolicyKey: rlpKey, - GatewayKey: gatewayKey, - } - if _, ok := rateLimitIndex.Get(key); ok { // should never happen - logger.Error(fmt.Errorf("unexpected duplicate rate limit policy key found"), "failed do add rate limit policy to index", "RateLimitPolicy", rlpKey.String(), "Gateway", gatewayKey) - continue - } - rlp := policy.(*kuadrantv1beta3.RateLimitPolicy) - rateLimitIndex.Set(key, rlptools.LimitadorRateLimitsFromRLP(rlp)) - } - } - - return rateLimitIndex -} diff --git a/controllers/ratelimitpolicy_status.go b/controllers/ratelimitpolicy_status.go deleted file mode 100644 index 6ece9605b..000000000 --- a/controllers/ratelimitpolicy_status.go +++ /dev/null @@ -1,59 +0,0 @@ -package controllers - -import ( - "context" - "slices" - - "github.com/go-logr/logr" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" -) - -func (r *RateLimitPolicyReconciler) reconcileStatus(ctx context.Context, rlp *kuadrantv1beta3.RateLimitPolicy, specErr error) (ctrl.Result, error) { - logger, _ := logr.FromContext(ctx) - newStatus := r.calculateStatus(rlp, specErr) - if err := r.ReconcileResourceStatus( - ctx, - client.ObjectKeyFromObject(rlp), - &kuadrantv1beta3.RateLimitPolicy{}, - kuadrantv1beta3.RateLimitPolicyStatusMutator(newStatus, logger), - ); err != nil { - // Ignore conflicts, resource might just be outdated. - if apierrors.IsConflict(err) { - logger.V(1).Info("Failed to update status: resource might just be outdated") - return reconcile.Result{Requeue: true}, nil - } - - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil -} - -func (r *RateLimitPolicyReconciler) calculateStatus(rlp *kuadrantv1beta3.RateLimitPolicy, specErr error) *kuadrantv1beta3.RateLimitPolicyStatus { - newStatus := &kuadrantv1beta3.RateLimitPolicyStatus{ - // Copy initial conditions. Otherwise, status will always be updated - Conditions: slices.Clone(rlp.Status.Conditions), - } - - newStatus.SetObservedGeneration(rlp.GetGeneration()) - - acceptedCond := kuadrant.AcceptedCondition(rlp, specErr) - - meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) - - // Do not set enforced condition if Accepted condition is false - if meta.IsStatusConditionFalse(newStatus.Conditions, string(gatewayapiv1alpha2.PolicyReasonAccepted)) { - meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) - return newStatus - } - - return newStatus -} diff --git a/controllers/ratelimitpolicy_status_test.go b/controllers/ratelimitpolicy_status_test.go deleted file mode 100644 index d1ebe7ea9..000000000 --- a/controllers/ratelimitpolicy_status_test.go +++ /dev/null @@ -1,70 +0,0 @@ -//go:build unit - -package controllers - -import ( - "errors" - "reflect" - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" -) - -func TestRateLimitPolicyReconciler_calculateStatus(t *testing.T) { - type args struct { - rlp *kuadrantv1beta3.RateLimitPolicy - specErr error - } - tests := []struct { - name string - args args - want *kuadrantv1beta3.RateLimitPolicyStatus - }{ - { - name: "Enforced status block removed if policy not Accepted. (Regression test)", // https://github.com/Kuadrant/kuadrant-operator/issues/588 - args: args{ - rlp: &kuadrantv1beta3.RateLimitPolicy{ - Status: kuadrantv1beta3.RateLimitPolicyStatus{ - Conditions: []metav1.Condition{ - { - Message: "not accepted", - Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapiv1alpha2.PolicyReasonTargetNotFound), - }, - { - Message: "RateLimitPolicy has been successfully enforced", - Type: string(kuadrant.PolicyConditionEnforced), - Status: metav1.ConditionTrue, - Reason: string(kuadrant.PolicyConditionEnforced), - }, - }, - }, - }, - specErr: kuadrant.NewErrInvalid("RateLimitPolicy", errors.New("policy Error")), - }, - want: &kuadrantv1beta3.RateLimitPolicyStatus{ - Conditions: []metav1.Condition{ - { - Message: "RateLimitPolicy target is invalid: policy Error", - Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapiv1alpha2.PolicyReasonInvalid), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &RateLimitPolicyReconciler{} - if got := r.calculateStatus(tt.args.rlp, tt.args.specErr); !reflect.DeepEqual(got, tt.want) { - t.Errorf("calculateStatus() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/controllers/ratelimitpolicy_status_updater.go b/controllers/ratelimitpolicy_status_updater.go new file mode 100644 index 000000000..727d731c1 --- /dev/null +++ b/controllers/ratelimitpolicy_status_updater.go @@ -0,0 +1,236 @@ +package controllers + +import ( + "context" + "fmt" + "slices" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" + kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type rateLimitPolicyStatusUpdater struct { + client *dynamic.DynamicClient +} + +func (r *rateLimitPolicyStatusUpdater) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.UpdateStatus, + Events: rateLimitEventMatchers, + } +} + +func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("rateLimitPolicyStatusUpdater") + + policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1beta3.RateLimitPolicy, bool) { + p, ok := item.(*kuadrantv1beta3.RateLimitPolicy) + return p, ok + }) + + policyAcceptedFunc := rateLimitPolicyAcceptedStatusFunc(state) + + logger.V(1).Info("updating rate limit policy statuses", "policies", len(policies)) + defer logger.V(1).Info("finished updating rate limit policy statuses") + + for _, policy := range policies { + if policy.GetDeletionTimestamp() != nil { + logger.V(1).Info("ratelimitpolicy is marked for deletion, skipping", "name", policy.Name, "namespace", policy.Namespace) + continue + } + + // copy initial conditions, otherwise status will always be updated + newStatus := &kuadrantv1beta3.RateLimitPolicyStatus{ + Conditions: slices.Clone(policy.Status.Conditions), + ObservedGeneration: policy.Status.ObservedGeneration, + } + + accepted, err := policyAcceptedFunc(policy) + meta.SetStatusCondition(&newStatus.Conditions, *kuadrant.AcceptedCondition(policy, err)) + + // do not set enforced condition if Accepted condition is false + if !accepted { + meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) + } else { + enforcedCond := r.enforcedCondition(policy, topology, state) + meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) + } + + equalStatus := equality.Semantic.DeepEqual(newStatus, policy.Status) + if equalStatus && policy.Generation == policy.Status.ObservedGeneration { + logger.V(1).Info("policy status unchanged, skipping update") + continue + } + newStatus.ObservedGeneration = policy.Generation + policy.Status = *newStatus + + obj, err := controller.Destruct(policy) + if err != nil { + logger.Error(err, "unable to destruct policy") // should never happen + continue + } + + _, err = r.client.Resource(kuadrantv1beta3.RateLimitPoliciesResource).Namespace(policy.GetNamespace()).UpdateStatus(ctx, obj, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "unable to update status for ratelimitpolicy", "name", policy.GetName(), "namespace", policy.GetNamespace()) + // TODO: handle error + } + } + + return nil +} + +func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.RateLimitPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { + policyKind := kuadrantv1beta3.RateLimitPolicyGroupKind.Kind + + effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + if !ok { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, ErrMissingStateEffectiveRateLimitPolicies), false) + } + + // check the state of the rules of the policy in the effective policies + policyRuleKeys := lo.Keys(policy.Rules()) + affectedPaths := map[string][][]machinery.Targetable{} // policyRuleKey → topological paths affected by the policy rule + overridingPolicies := map[string][]string{} // policyRuleKey → locators of policies overriding the policy rule + for _, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { + if len(kuadrantv1.PoliciesInPath(effectivePolicy.Path, func(p machinery.Policy) bool { return p.GetLocator() == policy.GetLocator() })) == 0 { + continue + } + gatewayClass, gateway, listener, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + if !kuadrantgatewayapi.IsListenerReady(listener.Listener, gateway.Gateway) || !kuadrantgatewayapi.IsHTTPRouteReady(httpRoute.HTTPRoute, gateway.Gateway, gatewayClass.GatewayClass.Spec.ControllerName) { + continue + } + effectivePolicyRules := effectivePolicy.Spec.Rules() + for _, policyRuleKey := range policyRuleKeys { + if effectivePolicyRule, ok := effectivePolicyRules[policyRuleKey]; !ok || (ok && effectivePolicyRule.Source != policy.GetLocator()) { + var overriddenBy string + if ok { // TODO(guicassolato): !ok → we cannot tell which policy is overriding the rule, this information is lost when the policy rule is dropped during an atomic override + overriddenBy = effectivePolicyRule.Source + } + overridingPolicies[policyRuleKey] = append(overridingPolicies[policyRuleKey], overriddenBy) + continue + } + if affectedPaths[policyRuleKey] == nil { + affectedPaths[policyRuleKey] = [][]machinery.Targetable{} + } + affectedPaths[policyRuleKey] = append(affectedPaths[policyRuleKey], effectivePolicy.Path) + } + } + + // no rules of the policy found in the effective policies + if len(affectedPaths) == 0 { + // no rules of the policy have been overridden by any other policy + if len(overridingPolicies) == 0 { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrNoRoutes(policyKind), false) + } + // all rules of the policy have been overridden by at least one other policy + overridingPoliciesKeys := lo.FilterMap(lo.Uniq(lo.Flatten(lo.Values(overridingPolicies))), func(policyLocator string, _ int) (k8stypes.NamespacedName, bool) { + policyKey, err := common.NamespacedNameFromLocator(policyLocator) + return policyKey, err == nil + }) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOverridden(policyKind, overridingPoliciesKeys), false) + } + + var componentsToSync []string + + // check the status of Limitador + if limitadorLimitsModified, stateLimitadorLimitsModifiedPresent := state.Load(StateLimitadorLimitsModified); stateLimitadorLimitsModifiedPresent && limitadorLimitsModified.(bool) { + componentsToSync = append(componentsToSync, kuadrantv1beta1.LimitadorGroupKind.Kind) + } else { + limitador, err := GetLimitadorFromTopology(topology) + if err != nil { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, err), false) + } + if !meta.IsStatusConditionTrue(limitador.Status.Conditions, limitadorv1alpha1.StatusConditionReady) { + componentsToSync = append(componentsToSync, kuadrantv1beta1.LimitadorGroupKind.Kind) + } + } + + type affectedGateway struct { + gateway *machinery.Gateway + gatewayClass *machinery.GatewayClass + } + + // check the status of the gateways' configuration resources + affectedGateways := lo.UniqBy(lo.Map(lo.Flatten(lo.Values(affectedPaths)), func(path []machinery.Targetable, _ int) affectedGateway { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) + return affectedGateway{ + gateway: gateway, + gatewayClass: gatewayClass, + } + }), func(g affectedGateway) string { + return g.gateway.GetLocator() + }) + for _, g := range affectedGateways { + switch g.gatewayClass.Spec.ControllerName { + case istioGatewayControllerName: + // EnvoyFilter + istioRateLimitClustersModifiedGateways, _ := state.Load(StateIstioRateLimitClustersModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantistio.EnvoyFilterGroupKind, istioRateLimitClustersModifiedGateways, topology, func(obj machinery.Object) bool { + // return meta.IsStatusConditionTrue(lo.Map(obj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter).Status.Conditions, kuadrantistio.ConditionToProperConditionFunc), "Ready") + return true // Istio won't ever populate the status stanza of EnvoyFilter resources, so we cannot expect to find a given a condition there + })...) + // WasmPlugin + istioExtensionsModifiedGateways, _ := state.Load(StateIstioExtensionsModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantistio.WasmPluginGroupKind, istioExtensionsModifiedGateways, topology, func(obj machinery.Object) bool { + // return meta.IsStatusConditionTrue(lo.Map(obj.(*controller.RuntimeObject).Object.(*istioclientgoextensionv1alpha1.WasmPlugin).Status.Conditions, kuadrantistio.ConditionToProperConditionFunc), "Ready") + return true // Istio won't ever populate the status stanza of WasmPlugin resources, so we cannot expect to find a given a condition there + })...) + case envoyGatewayGatewayControllerName: + gatewayAncestor := gatewayapiv1.ParentReference{Name: gatewayapiv1.ObjectName(g.gateway.GetName()), Namespace: ptr.To(gatewayapiv1.Namespace(g.gateway.GetNamespace()))} + // EnvoyPatchPolicy + envoyGatewayRateLimitClustersModifiedGateways, _ := state.Load(StateEnvoyGatewayRateLimitClustersModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantenvoygateway.EnvoyPatchPolicyGroupKind, envoyGatewayRateLimitClustersModifiedGateways, topology, func(obj machinery.Object) bool { + return meta.IsStatusConditionTrue(kuadrantgatewayapi.PolicyStatusConditionsFromAncestor(obj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy).Status, envoyGatewayGatewayControllerName, gatewayAncestor, gatewayapiv1.Namespace(obj.GetNamespace())), string(envoygatewayv1alpha1.PolicyConditionProgrammed)) + })...) + // EnvoyExtensionPolicy + envoyGatewayExtensionsModifiedGateways, _ := state.Load(StateEnvoyGatewayExtensionsModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind, envoyGatewayExtensionsModifiedGateways, topology, func(obj machinery.Object) bool { + return meta.IsStatusConditionTrue(kuadrantgatewayapi.PolicyStatusConditionsFromAncestor(obj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyExtensionPolicy).Status, envoyGatewayGatewayControllerName, gatewayAncestor, gatewayapiv1.Namespace(obj.GetNamespace())), string(gatewayapiv1alpha2.PolicyConditionAccepted)) + })...) + default: + componentsToSync = append(componentsToSync, fmt.Sprintf("%s (%s/%s)", machinery.GatewayGroupKind.Kind, g.gateway.GetNamespace(), g.gateway.GetName())) + } + } + + if len(componentsToSync) > 0 { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOutOfSync(policyKind, componentsToSync), false) + } + + return kuadrant.EnforcedCondition(policy, nil, len(overridingPolicies) == 0) +} + +func gatewayComponentsToSync(gateway *machinery.Gateway, componentGroupKind schema.GroupKind, modifiedGatewayLocators any, topology *machinery.Topology, requiredCondition func(machinery.Object) bool) []string { + missingConditionInTopologyFunc := func() bool { + obj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == componentGroupKind + }) + return !found || !requiredCondition(obj) + } + if (modifiedGatewayLocators != nil && lo.Contains(modifiedGatewayLocators.([]string), gateway.GetLocator())) || missingConditionInTopologyFunc() { + return []string{fmt.Sprintf("%s (%s/%s)", componentGroupKind.Kind, gateway.GetNamespace(), gateway.GetName())} + } + return nil +} diff --git a/controllers/state_of_the_world.go b/controllers/state_of_the_world.go index 945216ca9..4b5801f52 100644 --- a/controllers/state_of_the_world.go +++ b/controllers/state_of_the_world.go @@ -19,6 +19,7 @@ import ( istioclientgosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/utils/env" @@ -41,12 +42,23 @@ import ( ) var ( + operatorNamespace = env.GetString("OPERATOR_NAMESPACE", "kuadrant-system") + kuadrantManagedLabelKey = "kuadrant.io/managed" + ConfigMapGroupKind = schema.GroupKind{Group: corev1.GroupName, Kind: "ConfigMap"} - operatorNamespace = env.GetString("OPERATOR_NAMESPACE", "kuadrant-system") ) +// gateway-api permissions //+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses,verbs=list;watch +// istio permissions +//+kubebuilder:rbac:groups=networking.istio.io,resources=envoyfilters,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=extensions.istio.io,resources=wasmplugins,verbs=get;list;watch;create;update;patch;delete + +// envoy gateway permissions +//+kubebuilder:rbac:groups=gateway.envoyproxy.io,resources=envoypatchpolicies,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=gateway.envoyproxy.io,resources=envoyextensionpolicies,verbs=get;list;watch;create;update;patch;delete + func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.DynamicClient, logger logr.Logger) *controller.Controller { // Base options controllerOpts := []controller.ControllerOption{ @@ -203,11 +215,13 @@ func (b *BootOptionsBuilder) getEnvoyGatewayOptions() []controller.ControllerOpt &egv1alpha1.EnvoyPatchPolicy{}, envoygateway.EnvoyPatchPoliciesResource, metav1.NamespaceAll, + controller.FilterResourcesByLabel[*egv1alpha1.EnvoyPatchPolicy](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), controller.WithRunnable("envoyextensionpolicy watcher", controller.Watch( &egv1alpha1.EnvoyExtensionPolicy{}, envoygateway.EnvoyExtensionPoliciesResource, metav1.NamespaceAll, + controller.FilterResourcesByLabel[*egv1alpha1.EnvoyExtensionPolicy](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), controller.WithRunnable("envoysecuritypolicy watcher", controller.Watch( &egv1alpha1.SecurityPolicy{}, @@ -219,7 +233,10 @@ func (b *BootOptionsBuilder) getEnvoyGatewayOptions() []controller.ControllerOpt envoygateway.EnvoyExtensionPolicyGroupKind, envoygateway.SecurityPolicyGroupKind, ), - // TODO: add object links + controller.WithObjectLinks( + envoygateway.LinkGatewayToEnvoyPatchPolicy, + envoygateway.LinkGatewayToEnvoyExtensionPolicy, + ), ) // TODO: add specific tasks to workflow } @@ -239,11 +256,13 @@ func (b *BootOptionsBuilder) getIstioOptions() []controller.ControllerOption { &istioclientnetworkingv1alpha3.EnvoyFilter{}, istio.EnvoyFiltersResource, metav1.NamespaceAll, + controller.FilterResourcesByLabel[*istioclientnetworkingv1alpha3.EnvoyFilter](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), controller.WithRunnable("wasmplugin watcher", controller.Watch( &istioclientgoextensionv1alpha1.WasmPlugin{}, istio.WasmPluginsResource, metav1.NamespaceAll, + controller.FilterResourcesByLabel[*istioclientgoextensionv1alpha1.WasmPlugin](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), controller.WithRunnable("authorizationpolicy watcher", controller.Watch( &istioclientgosecurityv1beta1.AuthorizationPolicy{}, @@ -255,7 +274,10 @@ func (b *BootOptionsBuilder) getIstioOptions() []controller.ControllerOption { istio.WasmPluginGroupKind, istio.AuthorizationPolicyGroupKind, ), - // TODO: add object links + controller.WithObjectLinks( + istio.LinkGatewayToEnvoyFilter, + istio.LinkGatewayToWasmPlugin, + ), ) // TODO: add istio specific tasks to workflow } @@ -303,7 +325,7 @@ func (b *BootOptionsBuilder) Reconciler() controller.ReconcileFunc { NewDNSWorkflow().Run, NewTLSWorkflow(b.client, b.isCertManagerInstalled).Run, NewAuthWorkflow().Run, - NewRateLimitWorkflow().Run, + NewRateLimitWorkflow(b.client, b.isIstioInstalled, b.isEnvoyGatewayInstalled).Run, }, Postcondition: finalStepsWorkflow(b.client, b.isIstioInstalled, b.isGatewayAPIInstalled).Run, } @@ -407,23 +429,25 @@ func finalStepsWorkflow(client *dynamic.DynamicClient, isIstioInstalled, isEnvoy return workflow } -var ErrNoKandrantResource = fmt.Errorf("no kuadrant resources in topology") +var ErrMissingKuadrant = fmt.Errorf("missing kuadrant object in topology") -// GetKuadrant returns the oldest Kuadrant from the root objects in the topology -func GetKuadrant(topology *machinery.Topology) (*kuadrantv1beta1.Kuadrant, error) { - kuadrantList := lo.FilterMap(topology.Objects().Roots(), func(item machinery.Object, _ int) (controller.Object, bool) { - k, ok := item.(controller.Object) - if ok && k.GetObjectKind().GroupVersionKind().GroupKind() == kuadrantv1beta1.KuadrantGroupKind && k.GetDeletionTimestamp() == nil { - return k, true - } - return nil, false +func GetKuadrantFromTopology(topology *machinery.Topology) (*kuadrantv1beta1.Kuadrant, error) { + kuadrants := lo.FilterMap(topology.Objects().Roots(), func(root machinery.Object, _ int) (controller.Object, bool) { + o, isSortable := root.(controller.Object) + return o, isSortable && root.GroupVersionKind().GroupKind() == kuadrantv1beta1.KuadrantGroupKind && o.GetDeletionTimestamp() == nil }) - if len(kuadrantList) == 0 { - return nil, ErrNoKandrantResource + if len(kuadrants) == 0 { + return nil, ErrMissingKuadrant } - sort.Sort(controller.ObjectsByCreationTimestamp(kuadrantList)) - k, _ := kuadrantList[0].(*kuadrantv1beta1.Kuadrant) - return k, nil + sort.Sort(controller.ObjectsByCreationTimestamp(kuadrants)) + kuadrant, _ := kuadrants[0].(*kuadrantv1beta1.Kuadrant) + return kuadrant, nil +} + +func KuadrantManagedObjectLabels() labels.Set { + return labels.Set(map[string]string{ + kuadrantManagedLabelKey: "true", + }) } func isObjectOwnedByGroupKind(o client.Object, groupKind schema.GroupKind) bool { diff --git a/controllers/state_of_the_world_test.go b/controllers/state_of_the_world_test.go index f1ad3ab8b..2ab3c0d93 100644 --- a/controllers/state_of_the_world_test.go +++ b/controllers/state_of_the_world_test.go @@ -109,13 +109,13 @@ func TestGetKuadrant(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetKuadrant(tt.args.topology) + got, err := GetKuadrantFromTopology(tt.args.topology) if (err != nil) != tt.wantErr { - t.Errorf("GetKuadrant() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetKuadrantFromTopology() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetKuadrant() got = %v, want %v", got, tt.want) + t.Errorf("GetKuadrantFromTopology() got = %v, want %v", got, tt.want) } }) } diff --git a/controllers/test_common.go b/controllers/test_common.go index 43d355ab9..e1a978d10 100644 --- a/controllers/test_common.go +++ b/controllers/test_common.go @@ -75,9 +75,10 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).ToNot(HaveOccurred()) authPolicyBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("authpolicy"), - mgr.GetEventRecorderFor("AuthPolicy"), ) err = (&AuthPolicyReconciler{ @@ -87,23 +88,11 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - rateLimitPolicyBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy"), - mgr.GetEventRecorderFor("RateLimitPolicy"), - ) - - err = (&RateLimitPolicyReconciler{ - BaseReconciler: rateLimitPolicyBaseReconciler, - TargetRefReconciler: reconcilers.TargetRefReconciler{Client: mgr.GetClient()}, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - tlsPolicyBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("tlspolicy"), - mgr.GetEventRecorderFor("TLSPolicy"), ) err = (&TLSPolicyReconciler{ @@ -115,9 +104,10 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) dnsPolicyBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("dnspolicy"), - mgr.GetEventRecorderFor("DNSPolicy"), ) err = (&DNSPolicyReconciler{ @@ -128,9 +118,10 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) kuadrantBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("kuadrant-controller"), - mgr.GetEventRecorderFor("Kuadrant"), ) err = (&KuadrantReconciler{ @@ -140,22 +131,11 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) - limitadorClusterEnvoyFilterBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy").WithName("envoyfilter"), - mgr.GetEventRecorderFor("LimitadorClusterEnvoyFilter"), - ) - - err = (&LimitadorClusterEnvoyFilterReconciler{ - BaseReconciler: limitadorClusterEnvoyFilterBaseReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - gatewayKuadrantBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("kuadrant").WithName("gateway"), - mgr.GetEventRecorderFor("GatewayKuadrant"), ) err = (&GatewayKuadrantReconciler{ @@ -164,22 +144,11 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) - rateLimitingIstioWASMPluginBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy").WithName("wasmplugin"), - mgr.GetEventRecorderFor("RateLimitingIstioWASMPlugin"), - ) - - err = (&RateLimitingIstioWASMPluginReconciler{ - BaseReconciler: rateLimitingIstioWASMPluginBaseReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - authPolicyIstioAuthorizationPolicyReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("authpolicy").WithName("istioauthorizationpolicy"), - mgr.GetEventRecorderFor("AuthPolicyIstioAuthorizationPolicy"), ) err = (&AuthPolicyIstioAuthorizationPolicyReconciler{ @@ -189,9 +158,10 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) targetStatusBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("targetstatus"), - mgr.GetEventRecorderFor("PolicyTargetStatus"), ) err = (&TargetStatusReconciler{ @@ -200,21 +170,11 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) - policyStatusBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy").WithName("status"), - mgr.GetEventRecorderFor("RateLimitPolicyStatus"), - ) - err = (&RateLimitPolicyEnforcedStatusReconciler{ - BaseReconciler: policyStatusBaseReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - authPolicyEnvoySecurityPolicyReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("authpolicy").WithName("securitypolicy"), - mgr.GetEventRecorderFor("AuthPolicyEnvoySecurityPolicy"), ) err = (&AuthPolicyEnvoySecurityPolicyReconciler{ @@ -224,9 +184,10 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) envoySecurityPolicyReferenceGrantReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetAPIReader(), log.Log.WithName("authpolicy").WithName("referencegrant"), - mgr.GetEventRecorderFor("EnvoySecurityPolicyReferenceGrant"), ) err = (&EnvoySecurityPolicyReferenceGrantReconciler{ @@ -235,30 +196,6 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) - envoyGatewayWasmReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("envoyGatewayWasmReconciler"), - mgr.GetEventRecorderFor("EnvoyGatewayWasmReconciler"), - ) - - err = (&EnvoyGatewayWasmReconciler{ - BaseReconciler: envoyGatewayWasmReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - - envoyGatewayLimitadorClusterReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("envoyGatewayLimitadorClusterReconciler"), - mgr.GetEventRecorderFor("EnvoyGatewayLimitadorClusterReconciler"), - ) - - err = (&EnvoyGatewayLimitadorClusterReconciler{ - BaseReconciler: envoyGatewayLimitadorClusterReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - dClient, err := dynamic.NewForConfig(mgr.GetConfig()) Expect(err).NotTo(HaveOccurred()) diff --git a/main.go b/main.go index e9aa559e4..6aef9b119 100644 --- a/main.go +++ b/main.go @@ -162,7 +162,6 @@ func main() { kuadrantBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("kuadrant"), - mgr.GetEventRecorderFor("Kuadrant"), ) if err = (&controllers.KuadrantReconciler{ @@ -173,24 +172,9 @@ func main() { os.Exit(1) } - rateLimitPolicyBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy"), - mgr.GetEventRecorderFor("RateLimitPolicy"), - ) - - if err = (&controllers.RateLimitPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{Client: mgr.GetClient()}, - BaseReconciler: rateLimitPolicyBaseReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RateLimitPolicy") - os.Exit(1) - } - authPolicyBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("authpolicy"), - mgr.GetEventRecorderFor("AuthPolicy"), ) if err = (&controllers.AuthPolicyReconciler{ @@ -205,7 +189,6 @@ func main() { dnsPolicyBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("dnspolicy"), - mgr.GetEventRecorderFor("DNSPolicy"), ) if err = (&controllers.DNSPolicyReconciler{ @@ -219,7 +202,6 @@ func main() { tlsPolicyBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("tlspolicy"), - mgr.GetEventRecorderFor("TLSPolicy"), ) if err = (&controllers.TLSPolicyReconciler{ @@ -231,23 +213,9 @@ func main() { os.Exit(1) } - limitadorClusterEnvoyFilterBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy").WithName("envoyfilter"), - mgr.GetEventRecorderFor("LimitadorClusterEnvoyFilter"), - ) - - if err = (&controllers.LimitadorClusterEnvoyFilterReconciler{ - BaseReconciler: limitadorClusterEnvoyFilterBaseReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "EnvoyFilter") - os.Exit(1) - } - gatewayKuadrantBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("kuadrant").WithName("gateway"), - mgr.GetEventRecorderFor("GatewayKuadrant"), ) if err = (&controllers.GatewayKuadrantReconciler{ @@ -257,23 +225,9 @@ func main() { os.Exit(1) } - rateLimitingIstioWASMPluginBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy").WithName("wasmplugin"), - mgr.GetEventRecorderFor("RateLimitingIstioWASMPlugin"), - ) - - if err = (&controllers.RateLimitingIstioWASMPluginReconciler{ - BaseReconciler: rateLimitingIstioWASMPluginBaseReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RateLimitingIstioWASMPlugin") - os.Exit(1) - } - authPolicyIstioAuthorizationPolicyReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("authpolicy").WithName("istioauthorizationpolicy"), - mgr.GetEventRecorderFor("AuthPolicyIstioAuthorizationPolicy"), ) if err = (&controllers.AuthPolicyIstioAuthorizationPolicyReconciler{ BaseReconciler: authPolicyIstioAuthorizationPolicyReconciler, @@ -285,7 +239,6 @@ func main() { targetStatusBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("targetstatus"), - mgr.GetEventRecorderFor("PolicyTargetStatus"), ) if err = (&controllers.TargetStatusReconciler{ BaseReconciler: targetStatusBaseReconciler, @@ -294,22 +247,9 @@ func main() { os.Exit(1) } - policyStatusBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimitpolicy").WithName("status"), - mgr.GetEventRecorderFor("RateLimitPolicyStatus"), - ) - if err = (&controllers.RateLimitPolicyEnforcedStatusReconciler{ - BaseReconciler: policyStatusBaseReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RateLimitPolicyEnforcedStatusReconciler") - os.Exit(1) - } - authPolicyEnvoySecurityPolicyReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("authpolicy").WithName("securitypolicy"), - mgr.GetEventRecorderFor("AuthPolicyEnvoySecurityPolicy"), ) if err = (&controllers.AuthPolicyEnvoySecurityPolicyReconciler{ BaseReconciler: authPolicyEnvoySecurityPolicyReconciler, @@ -321,7 +261,6 @@ func main() { envoySecurityPolicyReferenceGrantReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("authpolicy").WithName("referencegrant"), - mgr.GetEventRecorderFor("EnvoySecurityPolicyReferenceGrant"), ) if err = (&controllers.EnvoySecurityPolicyReferenceGrantReconciler{ BaseReconciler: envoySecurityPolicyReferenceGrantReconciler, @@ -330,30 +269,6 @@ func main() { os.Exit(1) } - envoyGatewayWasmReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("envoyGatewayWasmReconciler"), - mgr.GetEventRecorderFor("EnvoyGatewayWasmReconciler"), - ) - if err = (&controllers.EnvoyGatewayWasmReconciler{ - BaseReconciler: envoyGatewayWasmReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "EnvoyGatewayWasmReconciler") - os.Exit(1) - } - - envoyGatewayLimitadorClusterReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("envoyGatewayLimitadorClusterReconciler"), - mgr.GetEventRecorderFor("EnvoyGatewayLimitadorClusterReconciler"), - ) - if err = (&controllers.EnvoyGatewayLimitadorClusterReconciler{ - BaseReconciler: envoyGatewayLimitadorClusterReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "EnvoyGatewayLimitadorClusterReconciler") - os.Exit(1) - } - //+kubebuilder:scaffold:builder if err = mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/make/istio.mk b/make/istio.mk index d9df5202a..9a2dc63f4 100644 --- a/make/istio.mk +++ b/make/istio.mk @@ -15,10 +15,7 @@ endif # istioctl tool ISTIOCTL=$(shell pwd)/bin/istioctl -# OSSM 2.6 is based on Istio 1.20.8 -# https://github.com/maistra/istio/blob/maistra-2.6/go.mod#L116-L117 -# https://github.com/maistra/istio/pull/1039 -ISTIOVERSION = 1.20.8 +ISTIOVERSION = 1.22.5 $(ISTIOCTL): mkdir -p $(shell pwd)/bin $(eval TMP := $(shell mktemp -d)) diff --git a/pkg/common/policy_machinery_helpers.go b/pkg/common/policy_machinery_helpers.go new file mode 100644 index 000000000..ccf096e2f --- /dev/null +++ b/pkg/common/policy_machinery_helpers.go @@ -0,0 +1,111 @@ +// TODO: Move to github.com/kuadrant/policy-machinery + +package common + +import ( + "fmt" + "strings" + + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +func NewErrInvalidPath(message string) error { + return ErrInvalidPath{message: message} +} + +type ErrInvalidPath struct { + message string +} + +func (e ErrInvalidPath) Error() string { + return fmt.Sprintf("invalid path: %s", e.message) +} + +// ObjectsInRequestPath returns the objects in a data plane path converted to their respective types +// The last returned value is an error that indicates if the path is valid if present. +func ObjectsInRequestPath(path []machinery.Targetable) (*machinery.GatewayClass, *machinery.Gateway, *machinery.Listener, *machinery.HTTPRoute, *machinery.HTTPRouteRule, error) { + if len(path) == 0 { + return nil, nil, nil, nil, nil, NewErrInvalidPath("empty path") + } + + gatewayClass, ok := path[0].(*machinery.GatewayClass) + if !ok { + return nil, nil, nil, nil, nil, NewErrInvalidPath("index 0 is not a GatewayClass") + } + + gateway, ok := path[1].(*machinery.Gateway) + if !ok { + return gatewayClass, nil, nil, nil, nil, NewErrInvalidPath("index 1 is not a Gateway") + } + if gateway.Spec.GatewayClassName != gatewayapiv1.ObjectName(gatewayClass.GetName()) { + return gatewayClass, gateway, nil, nil, nil, NewErrInvalidPath("gateway does not belong to the gateway class") + } + + listener, ok := path[2].(*machinery.Listener) + if !ok { + return gatewayClass, gateway, nil, nil, nil, NewErrInvalidPath("index 2 is not a Listener") + } + if listener.Gateway == nil || listener.Gateway.GetNamespace() != gateway.GetNamespace() || listener.Gateway.GetName() != gateway.GetName() { + return gatewayClass, gateway, listener, nil, nil, NewErrInvalidPath("listener does not belong to the gateway") + } + + httpRoute, ok := path[3].(*machinery.HTTPRoute) + if !ok { + return gatewayClass, gateway, listener, nil, nil, NewErrInvalidPath("index 3 is not a HTTPRoute") + } + if !lo.ContainsBy(httpRoute.Spec.ParentRefs, func(ref gatewayapiv1.ParentReference) bool { + gateway := listener.Gateway + defaultGroup := gatewayapiv1.Group(gatewayapiv1.GroupName) + defaultKind := gatewayapiv1.Kind(machinery.GatewayGroupKind.Kind) + defaultNamespace := gatewayapiv1.Namespace(httpRoute.GetNamespace()) + if ptr.Deref(ref.Group, defaultGroup) != gatewayapiv1.Group(gateway.GroupVersionKind().Group) || ptr.Deref(ref.Kind, defaultKind) != gatewayapiv1.Kind(gateway.GroupVersionKind().Kind) || ptr.Deref(ref.Namespace, defaultNamespace) != gatewayapiv1.Namespace(gateway.GetNamespace()) || ref.Name != gatewayapiv1.ObjectName(gateway.GetName()) { + return false + } + if sectionName := ptr.Deref(ref.SectionName, gatewayapiv1.SectionName("")); sectionName != "" && sectionName != listener.Name { + return false + } + hostnameSupersets := []gatewayapiv1.Hostname{"*"} + if listener.Hostname != nil { + hostnameSupersets = []gatewayapiv1.Hostname{*(listener.Hostname)} + } + if len(httpRoute.Spec.Hostnames) > 0 { + return lo.SomeBy(httpRoute.Spec.Hostnames, func(routeHostname gatewayapiv1.Hostname) bool { + return lo.SomeBy(hostnameSupersets, func(hostnameSuperset gatewayapiv1.Hostname) bool { + return utils.Name(routeHostname).SubsetOf(utils.Name(hostnameSuperset)) + }) + }) + } + return true + }) { + return gatewayClass, gateway, listener, httpRoute, nil, NewErrInvalidPath("http route does not belong to the listener") + } + + httpRouteRule, ok := path[4].(*machinery.HTTPRouteRule) + if !ok { + return gatewayClass, gateway, listener, httpRoute, nil, NewErrInvalidPath("index 4 is not a HTTPRouteRule") + } + if httpRouteRule.HTTPRoute == nil || httpRouteRule.HTTPRoute.GetNamespace() != httpRoute.GetNamespace() || httpRouteRule.HTTPRoute.GetName() != httpRoute.GetName() { + return gatewayClass, gateway, listener, httpRoute, httpRouteRule, NewErrInvalidPath("http route rule does not belong to the http route") + } + + return gatewayClass, gateway, listener, httpRoute, httpRouteRule, nil +} + +// NamespacedNameFromLocator returns a k8s namespaced name from a Policy Machinery object locator +func NamespacedNameFromLocator(locator string) (k8stypes.NamespacedName, error) { + parts := strings.SplitN(locator, ":", 2) // : + if len(parts) != 2 { + return k8stypes.NamespacedName{}, fmt.Errorf("invalid locator: %s", locator) + } + namespacedName := strings.SplitN(parts[1], string(k8stypes.Separator), 2) + if len(namespacedName) == 1 { + return k8stypes.NamespacedName{Name: namespacedName[0]}, nil + } + return k8stypes.NamespacedName{Namespace: namespacedName[0], Name: namespacedName[1]}, nil +} diff --git a/pkg/common/policy_machinery_helpers_test.go b/pkg/common/policy_machinery_helpers_test.go new file mode 100644 index 000000000..2a7992edb --- /dev/null +++ b/pkg/common/policy_machinery_helpers_test.go @@ -0,0 +1,378 @@ +//go:build unit + +package common + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/policy-machinery/machinery" +) + +func TestObjectsInRequestPath(t *testing.T) { + gatewayClass := func(mutate ...func(*machinery.GatewayClass)) *machinery.GatewayClass { + o := &machinery.GatewayClass{ + GatewayClass: &gatewayapiv1.GatewayClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayapiv1.SchemeGroupVersion.String(), + Kind: machinery.GatewayClassGroupKind.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kuadrant", + }, + Spec: gatewayapiv1.GatewayClassSpec{ + ControllerName: "kuadrant.io/policy-controller", + }, + }, + } + for _, m := range mutate { + m(o) + } + return o + } + + gateway := func(gc *machinery.GatewayClass, mutate ...func(*machinery.Gateway)) *machinery.Gateway { + if gc == nil { + gc = gatewayClass() + } + o := &machinery.Gateway{ + Gateway: &gatewayapiv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayapiv1.SchemeGroupVersion.String(), + Kind: machinery.GatewayGroupKind.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kuadrant", + }, + Spec: gatewayapiv1.GatewaySpec{ + GatewayClassName: gatewayapiv1.ObjectName(gc.GetName()), + Listeners: []gatewayapiv1.Listener{ + { + Name: "example", + Hostname: ptr.To(gatewayapiv1.Hostname("*.example.com")), + Protocol: gatewayapiv1.ProtocolType("HTTP"), + Port: gatewayapiv1.PortNumber(80), + }, + { + Name: "catch-all", + Protocol: gatewayapiv1.ProtocolType("HTTP"), + Port: gatewayapiv1.PortNumber(80), + }, + }, + }, + }, + } + for _, m := range mutate { + m(o) + } + return o + } + + listener := func(g *machinery.Gateway, mutate ...func(*machinery.Listener)) *machinery.Listener { + if g == nil { + g = gateway(nil) + } + o := &machinery.Listener{ + Listener: &g.Spec.Listeners[0], + Gateway: g, + } + for _, m := range mutate { + m(o) + } + return o + } + + httpRoute := func(parent machinery.Targetable, mutate ...func(*machinery.HTTPRoute)) *machinery.HTTPRoute { + if parent == nil { + parent = gateway(nil) + } + parentRef := gatewayapiv1.ParentReference{ + Name: gatewayapiv1.ObjectName(parent.GetName()), + Namespace: ptr.To(gatewayapiv1.Namespace(parent.GetNamespace())), + } + if l, ok := parent.(*machinery.Listener); ok { + parentRef.Name = gatewayapiv1.ObjectName(l.Gateway.GetName()) + parentRef.Namespace = ptr.To(gatewayapiv1.Namespace(l.Gateway.GetNamespace())) + parentRef.SectionName = ptr.To(gatewayapiv1.SectionName(l.Name)) + } + o := &machinery.HTTPRoute{ + HTTPRoute: &gatewayapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayapiv1.SchemeGroupVersion.String(), + Kind: machinery.HTTPRouteGroupKind.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "default", + }, + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{parentRef}, + }, + Hostnames: []gatewayapiv1.Hostname{"*.example.com"}, + Rules: []gatewayapiv1.HTTPRouteRule{ + { + Matches: []gatewayapiv1.HTTPRouteMatch{ + { + Path: &gatewayapiv1.HTTPPathMatch{Value: ptr.To("/foo")}, + }, + }, + BackendRefs: []gatewayapiv1.HTTPBackendRef{ + { + BackendRef: gatewayapiv1.BackendRef{ + BackendObjectReference: gatewayapiv1.BackendObjectReference{ + Name: "foo", + }, + }, + }, + }, + }, + { + Matches: []gatewayapiv1.HTTPRouteMatch{ + { + Path: &gatewayapiv1.HTTPPathMatch{Value: ptr.To("/bar")}, + }, + }, + BackendRefs: []gatewayapiv1.HTTPBackendRef{ + { + BackendRef: gatewayapiv1.BackendRef{ + BackendObjectReference: gatewayapiv1.BackendObjectReference{ + Name: "bar", + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, m := range mutate { + m(o) + } + return o + } + + httpRouteRule := func(r *machinery.HTTPRoute, mutate ...func(*machinery.HTTPRouteRule)) *machinery.HTTPRouteRule { + if r == nil { + r = httpRoute(nil) + } + o := &machinery.HTTPRouteRule{ + Name: "rule-1", + HTTPRoute: r, + HTTPRouteRule: &r.Spec.Rules[0], + } + for _, m := range mutate { + m(o) + } + return o + } + + gc := gatewayClass() + g := gateway(gc) + l := listener(g) + r := httpRoute(g) + rr := httpRouteRule(r) + + routeWithSectionName := httpRoute(l) + routeRuleWithSectionName := httpRouteRule(routeWithSectionName) + + otherGateway := gateway(nil, func(g *machinery.Gateway) { + g.ObjectMeta.Name = "other" + g.Spec.GatewayClassName = "other" + }) + otherListener := listener(otherGateway) + otherRoute := httpRoute(otherGateway, func(r *machinery.HTTPRoute) { + r.ObjectMeta.Name = "other" + }) + otherRouteRule := httpRouteRule(otherRoute) + + routeWithGatewayMatchingHostname := httpRoute(g, func(r *machinery.HTTPRoute) { + r.Spec.Hostnames = []gatewayapiv1.Hostname{"foo.example.com"} + }) + routeRuleWithGatewayMatchingHostname := httpRouteRule(routeWithGatewayMatchingHostname) + routeWithStrictListenerMatchingHostname := httpRoute(l, func(r *machinery.HTTPRoute) { + r.Spec.Hostnames = []gatewayapiv1.Hostname{"foo.example.com"} + }) + routeRuleWithStrictListenerMatchingHostname := httpRouteRule(routeWithStrictListenerMatchingHostname) + permissiveListener := listener(g, func(l *machinery.Listener) { + l.Listener = &g.Spec.Listeners[1] + }) + routeWithPermissiveListenerMatchingHostname := httpRoute(permissiveListener, func(r *machinery.HTTPRoute) { + r.Spec.Hostnames = []gatewayapiv1.Hostname{"other.org"} + }) + routeRuleWithPermissiveListenerMatchingHostname := httpRouteRule(routeWithPermissiveListenerMatchingHostname) + routeWithUnmatchingHostname := httpRoute(l, func(r *machinery.HTTPRoute) { + r.Spec.Hostnames = []gatewayapiv1.Hostname{"other.org"} + }) + + testCase := []struct { + name string + path []machinery.Targetable + expectedGatewayClass *machinery.GatewayClass + expectedGateway *machinery.Gateway + expectedListener *machinery.Listener + expectedHTTPRoute *machinery.HTTPRoute + expectedHTTPRouteRule *machinery.HTTPRouteRule + expectedError error + }{ + { + name: "nil path", + expectedError: NewErrInvalidPath("empty path"), + }, + { + name: "empty path", + path: []machinery.Targetable{}, + expectedError: NewErrInvalidPath("empty path"), + }, + { + name: "valid path", + path: []machinery.Targetable{gc, g, l, r, rr}, + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: r, + expectedHTTPRouteRule: rr, + }, + { + name: "valid path with route with section name", + path: []machinery.Targetable{gc, g, l, routeWithSectionName, routeRuleWithSectionName}, + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: routeWithSectionName, + expectedHTTPRouteRule: routeRuleWithSectionName, + }, + { + name: "gateway does not belong to the gateway class", + path: []machinery.Targetable{gc, otherGateway, l, r, rr}, + expectedError: NewErrInvalidPath("gateway does not belong to the gateway class"), + expectedGatewayClass: gc, + expectedGateway: otherGateway, + }, + { + name: "listener does not belong to the gateway", + path: []machinery.Targetable{gc, g, otherListener, r, rr}, + expectedError: NewErrInvalidPath("listener does not belong to the gateway"), + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: otherListener, + }, + { + name: "http route does not belong to the listener", + path: []machinery.Targetable{gc, g, l, otherRoute, otherRouteRule}, + expectedError: NewErrInvalidPath("http route does not belong to the listener"), + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: otherRoute, + }, + { + name: "route with gateway matching hostname", + path: []machinery.Targetable{gc, g, l, routeWithGatewayMatchingHostname, routeRuleWithGatewayMatchingHostname}, + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: routeWithGatewayMatchingHostname, + expectedHTTPRouteRule: routeRuleWithGatewayMatchingHostname, + }, + { + name: "route with strict listener matching hostname", + path: []machinery.Targetable{gc, g, l, routeWithStrictListenerMatchingHostname, routeRuleWithStrictListenerMatchingHostname}, + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: routeWithStrictListenerMatchingHostname, + expectedHTTPRouteRule: routeRuleWithStrictListenerMatchingHostname, + }, + { + name: "route with permissive listener matching hostname", + path: []machinery.Targetable{gc, g, permissiveListener, routeWithPermissiveListenerMatchingHostname, routeRuleWithPermissiveListenerMatchingHostname}, + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: permissiveListener, + expectedHTTPRoute: routeWithPermissiveListenerMatchingHostname, + expectedHTTPRouteRule: routeRuleWithPermissiveListenerMatchingHostname, + }, + { + name: "route with unmatching hostname", + path: []machinery.Targetable{gc, g, l, routeWithUnmatchingHostname, rr}, + expectedError: NewErrInvalidPath("http route does not belong to the listener"), + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: routeWithUnmatchingHostname, + }, + { + name: "http route rule does not belong to the http route", + path: []machinery.Targetable{gc, g, l, r, otherRouteRule}, + expectedError: NewErrInvalidPath("http route rule does not belong to the http route"), + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: r, + expectedHTTPRouteRule: otherRouteRule, + }, + { + name: "invalid gateway class", + path: []machinery.Targetable{rr, g, l, r, rr}, + expectedError: NewErrInvalidPath("index 0 is not a GatewayClass"), + }, + { + name: "invalid gateway", + path: []machinery.Targetable{gc, rr, l, r, rr}, + expectedError: NewErrInvalidPath("index 1 is not a Gateway"), + expectedGatewayClass: gc, + }, + { + name: "invalid listener", + path: []machinery.Targetable{gc, g, rr, r, rr}, + expectedError: NewErrInvalidPath("index 2 is not a Listener"), + expectedGatewayClass: gc, + expectedGateway: g, + }, + { + name: "invalid http route", + path: []machinery.Targetable{gc, g, l, rr, rr}, + expectedError: NewErrInvalidPath("index 3 is not a HTTPRoute"), + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + }, + { + name: "invalid http route rule", + path: []machinery.Targetable{gc, g, l, r, gc}, + expectedError: NewErrInvalidPath("index 4 is not a HTTPRouteRule"), + expectedGatewayClass: gc, + expectedGateway: g, + expectedListener: l, + expectedHTTPRoute: r, + }, + } + for _, tc := range testCase { + t.Run(tc.name, func(subT *testing.T) { + gatewayClass, gateway, listener, httpRoute, httpRouteRule, err := ObjectsInRequestPath(tc.path) + if err != tc.expectedError { + t.Errorf("expected error %v, got %v", tc.expectedError, err) + } + if gatewayClass != tc.expectedGatewayClass { + t.Errorf("expected gatewayClass %v, got %v", tc.expectedGatewayClass, gatewayClass) + } + if gateway != tc.expectedGateway { + t.Errorf("expected gateway %v, got %v", tc.expectedGateway, gateway) + } + if listener != tc.expectedListener { + t.Errorf("expected listener %v, got %v", tc.expectedListener, listener) + } + if httpRoute != tc.expectedHTTPRoute { + t.Errorf("expected httpRoute %v, got %v", tc.expectedHTTPRoute, httpRoute) + } + if httpRouteRule != tc.expectedHTTPRouteRule { + t.Errorf("expected httpRouteRule %v, got %v", tc.expectedHTTPRouteRule, httpRouteRule) + } + }) + } +} diff --git a/pkg/envoygateway/mutators.go b/pkg/envoygateway/mutators.go index b1f59ffc5..10847dead 100644 --- a/pkg/envoygateway/mutators.go +++ b/pkg/envoygateway/mutators.go @@ -5,75 +5,10 @@ import ( "reflect" egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" - - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" ) -func EnvoyExtensionPolicyMutator(existingObj, desiredObj client.Object) (bool, error) { - existing, ok := existingObj.(*egv1alpha1.EnvoyExtensionPolicy) - if !ok { - return false, fmt.Errorf("%T is not an *egapi.EnvoyExtensionPolicy", existingObj) - } - desired, ok := desiredObj.(*egv1alpha1.EnvoyExtensionPolicy) - if !ok { - return false, fmt.Errorf("%T is not an *egapi.EnvoyExtensionPolicy", desiredObj) - } - - var update bool - - if len(existing.Spec.Wasm) != len(desired.Spec.Wasm) { - update = true - existing.Spec.Wasm = desired.Spec.Wasm - } - - for idx := range desired.Spec.Wasm { - opts := cmp.Options{ - cmpopts.IgnoreFields(egv1alpha1.Wasm{}, "Config"), - cmpopts.IgnoreMapEntries(func(k string, _ any) bool { - return k == "config" - }), - } - - // Compare all except config (which is serialized into bytes) - if cmp.Equal(desired.Spec.Wasm[idx], existing.Spec.Wasm[idx], opts) { - update = true - existing.Spec.Wasm[idx] = desired.Spec.Wasm[idx] - } - - existingWasmConfig, err := wasm.ConfigFromJSON(existing.Spec.Wasm[idx].Config) - if err != nil { - return false, err - } - - desiredWasmConfig, err := wasm.ConfigFromJSON(desired.Spec.Wasm[idx].Config) - if err != nil { - return false, err - } - - // TODO(eastizle): reflect.DeepEqual does not work well with lists without order - if !reflect.DeepEqual(desiredWasmConfig, existingWasmConfig) { - update = true - existing.Spec.Wasm[idx].Config = desired.Spec.Wasm[idx].Config - } - } - - if !reflect.DeepEqual(existing.Spec.PolicyTargetReferences.TargetRefs, desired.Spec.PolicyTargetReferences.TargetRefs) { - update = true - existing.Spec.PolicyTargetReferences.TargetRefs = desired.Spec.PolicyTargetReferences.TargetRefs - } - - if !reflect.DeepEqual(existing.Annotations, desired.Annotations) { - update = true - existing.Annotations = desired.Annotations - } - - return update, nil -} - func EnvoySecurityPolicyMutator(existingObj, desiredObj client.Object) (bool, error) { existing, ok := existingObj.(*egv1alpha1.SecurityPolicy) if !ok { diff --git a/pkg/envoygateway/utils.go b/pkg/envoygateway/utils.go index 629bc3eff..34037c9cf 100644 --- a/pkg/envoygateway/utils.go +++ b/pkg/envoygateway/utils.go @@ -2,8 +2,13 @@ package envoygateway import ( egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) @@ -70,3 +75,70 @@ func IsEnvoyGatewayInstalled(restMapper meta.RESTMapper) (bool, error) { // EnvoyGateway found return true, nil } + +func LinkGatewayToEnvoyPatchPolicy(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), func(obj controller.Object, _ int) machinery.Object { + return &machinery.Gateway{Gateway: obj.(*gatewayapiv1.Gateway)} + }) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: EnvoyPatchPolicyGroupKind, + Func: func(child machinery.Object) []machinery.Object { + envoyPatchPolicy := child.(*controller.RuntimeObject).Object.(*egv1alpha1.EnvoyPatchPolicy) + namespace := envoyPatchPolicy.GetNamespace() + targetRef := envoyPatchPolicy.Spec.TargetRef + group := string(targetRef.Group) + if group == "" { + group = machinery.GatewayGroupKind.Group + } + kind := string(targetRef.Kind) + if kind == "" { + kind = machinery.GatewayGroupKind.Kind + } + name := string(targetRef.Name) + if group != machinery.GatewayGroupKind.Group || kind != machinery.GatewayGroupKind.Kind || name == "" { + return []machinery.Object{} + } + return lo.Filter(gateways, func(gateway machinery.Object, _ int) bool { + return gateway.GetName() == name && gateway.GetNamespace() == namespace + }) + }, + } +} + +func LinkGatewayToEnvoyExtensionPolicy(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), func(obj controller.Object, _ int) machinery.Object { + return &machinery.Gateway{Gateway: obj.(*gatewayapiv1.Gateway)} + }) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: EnvoyExtensionPolicyGroupKind, + Func: func(child machinery.Object) []machinery.Object { + envoyExtensionPolicy := child.(*controller.RuntimeObject).Object.(*egv1alpha1.EnvoyExtensionPolicy) + return lo.Filter(gateways, func(gateway machinery.Object, _ int) bool { + if gateway.GetNamespace() != envoyExtensionPolicy.GetNamespace() { + return false + } + return lo.SomeBy(envoyExtensionPolicy.Spec.TargetRefs, func(targetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { + group := string(targetRef.Group) + if group == "" { + group = machinery.GatewayGroupKind.Group + } + kind := string(targetRef.Kind) + if kind == "" { + kind = machinery.GatewayGroupKind.Kind + } + name := string(targetRef.Name) + if name == "" { + return false + } + return group == machinery.GatewayGroupKind.Group && + kind == machinery.GatewayGroupKind.Kind && + name == gateway.GetName() + }) + }) + }, + } +} diff --git a/pkg/istio/envoy_filters.go b/pkg/istio/envoy_filters.go deleted file mode 100644 index dc8741fb6..000000000 --- a/pkg/istio/envoy_filters.go +++ /dev/null @@ -1,86 +0,0 @@ -package istio - -import ( - "encoding/json" - "fmt" - - istioapiv1alpha3 "istio.io/api/networking/v1alpha3" - istionetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/kuadrant/kuadrant-operator/pkg/common" -) - -// LimitadorClusterPatch returns an EnvoyFilter patch that adds a custom cluster entry to compensate for kuadrant/limitador#53. -// Note: This should be removed once the mentioned issue is fixed but that will take some time. -func LimitadorClusterPatch(limitadorSvcHost string, limitadorGRPCPort int) ([]*istioapiv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { - // The patch defines the rate_limit_cluster, which provides the endpoint location of the external rate limit service. - patchUnstructured := map[string]any{ - "operation": "ADD", - "value": map[string]any{ - "name": common.KuadrantRateLimitClusterName, - "type": "STRICT_DNS", - "connect_timeout": "1s", - "lb_policy": "ROUND_ROBIN", - "http2_protocol_options": map[string]any{}, - "load_assignment": map[string]any{ - "cluster_name": common.KuadrantRateLimitClusterName, - "endpoints": []map[string]any{ - { - "lb_endpoints": []map[string]any{ - { - "endpoint": map[string]any{ - "address": map[string]any{ - "socket_address": map[string]any{ - "address": limitadorSvcHost, - "port_value": limitadorGRPCPort, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - patchRaw, _ := json.Marshal(patchUnstructured) - - patch := &istioapiv1alpha3.EnvoyFilter_Patch{} - err := patch.UnmarshalJSON(patchRaw) - if err != nil { - return nil, err - } - - return []*istioapiv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ - { - ApplyTo: istioapiv1alpha3.EnvoyFilter_CLUSTER, - Match: &istioapiv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ - ObjectTypes: &istioapiv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{ - Cluster: &istioapiv1alpha3.EnvoyFilter_ClusterMatch{ - Service: limitadorSvcHost, - }, - }, - }, - Patch: patch, - }, - }, nil -} - -func AlwaysUpdateEnvoyFilter(existingObj, desiredObj client.Object) (bool, error) { - existing, ok := existingObj.(*istionetworkingv1alpha3.EnvoyFilter) - if !ok { - return false, fmt.Errorf("%T is not a *istionetworkingv1alpha3.EnvoyFilter", existingObj) - } - desired, ok := desiredObj.(*istionetworkingv1alpha3.EnvoyFilter) - if !ok { - return false, fmt.Errorf("%T is not a *istionetworkingv1alpha3.EnvoyFilter", desiredObj) - } - existing.Spec = istioapiv1alpha3.EnvoyFilter{ - WorkloadSelector: desired.Spec.WorkloadSelector, - ConfigPatches: desired.Spec.ConfigPatches, - Priority: desired.Spec.Priority, - } - return true, nil -} diff --git a/pkg/istio/mutators.go b/pkg/istio/mutators.go index b332ee2d6..7a70d1cb0 100644 --- a/pkg/istio/mutators.go +++ b/pkg/istio/mutators.go @@ -8,7 +8,7 @@ import ( istiov1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) func WASMPluginMutator(existingObj, desiredObj client.Object) (bool, error) { diff --git a/pkg/istio/utils.go b/pkg/istio/utils.go index e0cf22688..6a3a01762 100644 --- a/pkg/istio/utils.go +++ b/pkg/istio/utils.go @@ -1,58 +1,63 @@ package istio import ( - "context" - - "github.com/go-logr/logr" - istiocommon "istio.io/api/type/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + istioapimetav1alpha1 "istio.io/api/meta/v1alpha1" + istioapiv1beta1 "istio.io/api/type/v1beta1" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" - istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istioclientgosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) var ( - EnvoyFiltersResource = istioclientnetworkingv1alpha3.SchemeGroupVersion.WithResource("envoyfilters") + EnvoyFiltersResource = istioclientgonetworkingv1alpha3.SchemeGroupVersion.WithResource("envoyfilters") WasmPluginsResource = istioclientgoextensionv1alpha1.SchemeGroupVersion.WithResource("wasmplugins") AuthorizationPoliciesResource = istioclientgosecurityv1beta1.SchemeGroupVersion.WithResource("authorizationpolicies") - EnvoyFilterGroupKind = schema.GroupKind{Group: istioclientnetworkingv1alpha3.GroupName, Kind: "EnvoyFilter"} + EnvoyFilterGroupKind = schema.GroupKind{Group: istioclientgonetworkingv1alpha3.GroupName, Kind: "EnvoyFilter"} WasmPluginGroupKind = schema.GroupKind{Group: istioclientgoextensionv1alpha1.GroupName, Kind: "WasmPlugin"} AuthorizationPolicyGroupKind = schema.GroupKind{Group: istioclientgosecurityv1beta1.GroupName, Kind: "AuthorizationPolicy"} ) -func WorkloadSelectorFromGateway(ctx context.Context, k8sClient client.Client, gateway *gatewayapiv1.Gateway) *istiocommon.WorkloadSelector { - logger, _ := logr.FromContext(ctx) - gatewayWorkloadSelector, err := kuadrantgatewayapi.GetGatewayWorkloadSelector(ctx, k8sClient, gateway) - if err != nil { - logger.V(1).Info("failed to build Istio WorkloadSelector from Gateway service - falling back to Gateway labels") - gatewayWorkloadSelector = gateway.Labels - } - return &istiocommon.WorkloadSelector{ - MatchLabels: gatewayWorkloadSelector, - } -} - -func PolicyTargetRefFromGateway(gateway *gatewayapiv1.Gateway) *istiocommon.PolicyTargetReference { - return &istiocommon.PolicyTargetReference{ +func PolicyTargetRefFromGateway(gateway *gatewayapiv1.Gateway) *istioapiv1beta1.PolicyTargetReference { + return &istioapiv1beta1.PolicyTargetReference{ Group: gatewayapiv1.GroupName, Kind: "Gateway", Name: gateway.Name, } } +func EqualTargetRefs(a, b []*istioapiv1beta1.PolicyTargetReference) bool { + return len(a) == len(b) && lo.EveryBy(a, func(aTargetRef *istioapiv1beta1.PolicyTargetReference) bool { + return lo.SomeBy(b, func(bTargetRef *istioapiv1beta1.PolicyTargetReference) bool { + return aTargetRef.Group == bTargetRef.Group && aTargetRef.Kind == bTargetRef.Kind && aTargetRef.Name == bTargetRef.Name && aTargetRef.Namespace == bTargetRef.Namespace + }) + }) +} + +func ConditionToProperConditionFunc(istioCondition *istioapimetav1alpha1.IstioCondition, _ int) metav1.Condition { + return metav1.Condition{ + Type: istioCondition.GetType(), + Status: metav1.ConditionStatus(istioCondition.GetStatus()), + Reason: istioCondition.GetReason(), + Message: istioCondition.GetMessage(), + } +} + func IsEnvoyFilterInstalled(restMapper meta.RESTMapper) (bool, error) { return utils.IsCRDInstalled( restMapper, - istioclientnetworkingv1alpha3.GroupName, + istioclientgonetworkingv1alpha3.GroupName, "EnvoyFilter", - istioclientnetworkingv1alpha3.SchemeGroupVersion.Version) + istioclientgonetworkingv1alpha3.SchemeGroupVersion.Version) } func IsWASMPluginInstalled(restMapper meta.RESTMapper) (bool, error) { @@ -99,3 +104,64 @@ func IsIstioInstalled(restMapper meta.RESTMapper) (bool, error) { // Istio found return true, nil } + +func LinkGatewayToWasmPlugin(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), func(obj controller.Object, _ int) machinery.Object { + return &machinery.Gateway{Gateway: obj.(*gatewayapiv1.Gateway)} + }) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: WasmPluginGroupKind, + Func: func(child machinery.Object) []machinery.Object { + wasmPlugin := child.(*controller.RuntimeObject).Object.(*istioclientgoextensionv1alpha1.WasmPlugin) + return lo.Filter(gateways, istioTargetRefsIncludeObjectFunc(wasmPlugin.Spec.TargetRefs, wasmPlugin.GetNamespace())) + }, + } +} + +func LinkGatewayToEnvoyFilter(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), func(obj controller.Object, _ int) machinery.Object { + return &machinery.Gateway{Gateway: obj.(*gatewayapiv1.Gateway)} + }) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: EnvoyFilterGroupKind, + Func: func(child machinery.Object) []machinery.Object { + envoyFilter := child.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) + return lo.Filter(gateways, istioTargetRefsIncludeObjectFunc(envoyFilter.Spec.TargetRefs, envoyFilter.GetNamespace())) + }, + } +} + +func istioTargetRefsIncludeObjectFunc(targetRefs []*istioapiv1beta1.PolicyTargetReference, defaultNamespace string) func(machinery.Object, int) bool { + return func(obj machinery.Object, _ int) bool { + groupKind := obj.GroupVersionKind().GroupKind() + return lo.SomeBy(targetRefs, func(targetRef *istioapiv1beta1.PolicyTargetReference) bool { + if targetRef == nil { + return false + } + group := targetRef.GetGroup() + if group == "" { + group = machinery.GatewayGroupKind.Group + } + kind := targetRef.GetKind() + if kind == "" { + kind = machinery.GatewayGroupKind.Kind + } + name := targetRef.GetName() + if name == "" { + return false + } + namespace := targetRef.GetNamespace() + if namespace == "" { + namespace = defaultNamespace + } + return group == groupKind.Group && + kind == groupKind.Kind && + name == obj.GetName() && + namespace == obj.GetNamespace() + }) + } +} diff --git a/pkg/istio/utils_test.go b/pkg/istio/utils_test.go index 09ea560bf..4baee1961 100644 --- a/pkg/istio/utils_test.go +++ b/pkg/istio/utils_test.go @@ -3,109 +3,12 @@ package istio import ( - "context" "testing" - "github.com/go-logr/logr" - istiocommon "istio.io/api/type/v1beta1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/kuadrant/kuadrant-operator/pkg/log" ) -func TestWorkloadSelectorFromGateway(t *testing.T) { - hostnameAddress := gatewayapiv1.AddressType("Hostname") - gateway := &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "my-ns", - Name: "my-gw", - Labels: map[string]string{ - "app": "foo", - "control-plane": "kuadrant", - }, - }, - Status: gatewayapiv1.GatewayStatus{ - Addresses: []gatewayapiv1.GatewayStatusAddress{ - { - Type: &hostnameAddress, - Value: "my-gw-svc.my-ns.svc.cluster.local:80", - }, - }, - }, - } - - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "my-ns", - Name: "my-gw-svc", - Labels: map[string]string{ - "a-label": "irrelevant", - }, - }, - Spec: corev1.ServiceSpec{ - Selector: map[string]string{ - "a-selector": "what-we-are-looking-for", - }, - }, - } - - scheme := runtime.NewScheme() - _ = corev1.AddToScheme(scheme) - _ = gatewayapiv1.AddToScheme(scheme) - k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gateway, service).Build() - - var selector *istiocommon.WorkloadSelector - - selector = WorkloadSelectorFromGateway(context.TODO(), k8sClient, gateway) - if selector == nil || len(selector.MatchLabels) != 1 || selector.MatchLabels["a-selector"] != "what-we-are-looking-for" { - t.Error("should have built the istio workload selector from the gateway service") - } -} - -func TestWorkloadSelectorFromGatewayMissingHostnameAddress(t *testing.T) { - gateway := &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "my-ns", - Name: "my-gw", - Labels: map[string]string{ - "app": "foo", - "control-plane": "kuadrant", - }, - }, - } - - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "my-ns", - Name: "my-gw-svc", - Labels: map[string]string{ - "a-label": "irrelevant", - }, - }, - Spec: corev1.ServiceSpec{ - Selector: map[string]string{ - "a-selector": "what-we-are-looking-for", - }, - }, - } - - scheme := runtime.NewScheme() - _ = corev1.AddToScheme(scheme) - _ = gatewayapiv1.AddToScheme(scheme) - k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gateway, service).Build() - - var selector *istiocommon.WorkloadSelector - - selector = WorkloadSelectorFromGateway(logr.NewContext(context.TODO(), log.Log), k8sClient, gateway) - if selector == nil || len(selector.MatchLabels) != 2 || selector.MatchLabels["app"] != "foo" || selector.MatchLabels["control-plane"] != "kuadrant" { - t.Error("should have built the istio workload selector from the gateway labels") - } -} - func TestPolicyTargetRefFromGateway(t *testing.T) { gateway := &gatewayapiv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/library/gatewayapi/types.go b/pkg/library/gatewayapi/types.go index 61e5bd930..104aa3e7c 100644 --- a/pkg/library/gatewayapi/types.go +++ b/pkg/library/gatewayapi/types.go @@ -2,7 +2,10 @@ package gatewayapi import ( "context" + "fmt" + "sort" + "github.com/samber/lo" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -10,6 +13,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) type PolicyClass int @@ -168,3 +173,119 @@ func (h httpRouteType) GetInstance() client.Object { func NewHTTPRouteType() Type { return &httpRouteType{} } + +// HTTPRouteMatchConfig stores any config associated to an HTTPRouteRule +type HTTPRouteMatchConfig struct { + Hostname string + HTTPRouteMatch gatewayapiv1.HTTPRouteMatch + CreationTimestamp metav1.Time + Namespace string + Name string + Config any +} + +// SortableHTTPRouteMatchConfigs is a slice of HTTPRouteMatch that implements sort.Interface +type SortableHTTPRouteMatchConfigs []HTTPRouteMatchConfig + +func (c SortableHTTPRouteMatchConfigs) Len() int { return len(c) } +func (c SortableHTTPRouteMatchConfigs) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c SortableHTTPRouteMatchConfigs) Less(i, j int) bool { + // Hostname + if c[i].Hostname != c[j].Hostname { + return utils.CompareHostnamesSpecificity(c[i].Hostname, c[j].Hostname) + } + + // HTTPRouteMatch (https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule) + // HTTPRouteMatch - based on path match type (Exact > RegularExpression > PathPrefix) + if c[i].HTTPRouteMatch.Path != nil && c[i].HTTPRouteMatch.Path.Type != nil && *c[i].HTTPRouteMatch.Path.Type == gatewayapiv1.PathMatchExact { + if c[j].HTTPRouteMatch.Path != nil && c[j].HTTPRouteMatch.Path.Type != nil { + switch *c[j].HTTPRouteMatch.Path.Type { + case gatewayapiv1.PathMatchRegularExpression, gatewayapiv1.PathMatchPathPrefix: + return true + } + } + } + if c[i].HTTPRouteMatch.Path != nil && c[i].HTTPRouteMatch.Path.Type != nil && *c[i].HTTPRouteMatch.Path.Type == gatewayapiv1.PathMatchRegularExpression { + if c[j].HTTPRouteMatch.Path != nil && c[j].HTTPRouteMatch.Path.Type != nil { + switch *c[j].HTTPRouteMatch.Path.Type { + case gatewayapiv1.PathMatchExact: + return false + case gatewayapiv1.PathMatchPathPrefix: + return true + } + } + } + if c[i].HTTPRouteMatch.Path != nil && c[i].HTTPRouteMatch.Path.Type != nil && *c[i].HTTPRouteMatch.Path.Type == gatewayapiv1.PathMatchPathPrefix { + if c[j].HTTPRouteMatch.Path != nil && c[j].HTTPRouteMatch.Path.Type != nil { + switch *c[j].HTTPRouteMatch.Path.Type { + case gatewayapiv1.PathMatchExact, gatewayapiv1.PathMatchRegularExpression: + return false + } + } + } + + // HTTPRouteMatch - based on number of characters in a matching path + pCountI := pathMatchCount(c[i].HTTPRouteMatch.Path) + pCountJ := pathMatchCount(c[j].HTTPRouteMatch.Path) + if pCountI != pCountJ { + return pCountI > pCountJ + } + + // HTTPRouteMatch - based on method match type + hasMethodI := c[i].HTTPRouteMatch.Method != nil + hasMethodJ := c[j].HTTPRouteMatch.Method != nil + if hasMethodI != hasMethodJ { + return !hasMethodI + } + + // HTTPRouteMatch - based on the number of header match type + hCountI := len(c[i].HTTPRouteMatch.Headers) + hCountJ := len(c[j].HTTPRouteMatch.Headers) + if hCountI != hCountJ { + return hCountI > hCountJ + } + + // HTTPRouteMatch - based on the number of query param match type + qCountI := len(c[i].HTTPRouteMatch.QueryParams) + qCountJ := len(c[j].HTTPRouteMatch.QueryParams) + if qCountI != qCountJ { + return qCountI > qCountJ + } + + // Creation Timestamp + p1Time := ptr.To(c[i].CreationTimestamp) + p2Time := ptr.To(c[j].CreationTimestamp) + if !p1Time.Equal(p2Time) { + return p1Time.Before(p2Time) + } + + // Lexicographically by "{namespace}/{name}" + return fmt.Sprintf("%s/%s", c[i].Namespace, c[i].Name) < fmt.Sprintf("%s/%s", c[j].Namespace, c[j].Name) +} + +func pathMatchCount(pathMatch *gatewayapiv1.HTTPPathMatch) int { + if pathMatch != nil && pathMatch.Value != nil { + return len(*pathMatch.Value) + } + return 0 +} + +type GrouppedHTTPRouteMatchConfigs map[string]SortableHTTPRouteMatchConfigs + +func (g *GrouppedHTTPRouteMatchConfigs) Add(key string, configs ...HTTPRouteMatchConfig) { + for _, config := range configs { + (*g)[key] = append((*g)[key], config) + } +} + +func (g *GrouppedHTTPRouteMatchConfigs) Sorted() GrouppedHTTPRouteMatchConfigs { + if g == nil { + return nil + } + return lo.MapValues(*g, func(configs SortableHTTPRouteMatchConfigs, _ string) SortableHTTPRouteMatchConfigs { + sortedConfigs := make(SortableHTTPRouteMatchConfigs, len(configs)) + copy(sortedConfigs, configs) + sort.Sort(sortedConfigs) + return sortedConfigs + }) +} diff --git a/pkg/library/gatewayapi/utils.go b/pkg/library/gatewayapi/utils.go index f592103cf..c2190dcd9 100644 --- a/pkg/library/gatewayapi/utils.go +++ b/pkg/library/gatewayapi/utils.go @@ -9,6 +9,8 @@ import ( "github.com/cert-manager/cert-manager/pkg/apis/certmanager" certmanv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "github.com/go-logr/logr" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -19,6 +21,20 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) +func HostnamesFromListenerAndHTTPRoute(listener *gatewayapiv1.Listener, httpRoute *gatewayapiv1.HTTPRoute) []gatewayapiv1.Hostname { + hostname := listener.Hostname + if hostname == nil { + hostname = ptr.To(gatewayapiv1.Hostname("*")) + } + hostnames := []gatewayapiv1.Hostname{*hostname} + if routeHostnames := httpRoute.Spec.Hostnames; len(routeHostnames) > 0 { + hostnames = lo.Filter(httpRoute.Spec.Hostnames, func(h gatewayapiv1.Hostname, _ int) bool { + return utils.Name(h).SubsetOf(utils.Name(*hostname)) + }) + } + return hostnames +} + func IsTargetRefHTTPRoute(targetRef gatewayapiv1alpha2.LocalPolicyTargetReference) bool { return targetRef.Group == (gatewayapiv1.GroupName) && targetRef.Kind == ("HTTPRoute") } @@ -194,3 +210,55 @@ func GetGatewayParentKeys(route *gatewayapiv1.HTTPRoute) []client.ObjectKey { } }) } + +func EqualLocalPolicyTargetReferencesWithSectionName(a, b []gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { + return len(a) == len(b) && !lo.EveryBy(a, func(aTargetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { + return lo.SomeBy(b, func(bTargetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { + return aTargetRef.Group == bTargetRef.Group && aTargetRef.Kind == bTargetRef.Kind && aTargetRef.Name == bTargetRef.Name && ptr.Deref(aTargetRef.SectionName, gatewayapiv1alpha2.SectionName("")) == ptr.Deref(bTargetRef.SectionName, gatewayapiv1alpha2.SectionName("")) + }) + }) +} + +// PolicyStatusConditionsFromAncestor returns the conditions from a policy status for a given ancestor +func PolicyStatusConditionsFromAncestor(policyStatus gatewayapiv1alpha2.PolicyStatus, controllerName gatewayapiv1.GatewayController, ancestor gatewayapiv1.ParentReference, defaultNamespace gatewayapiv1.Namespace) []metav1.Condition { + if status, found := lo.Find(policyStatus.Ancestors, func(a gatewayapiv1alpha2.PolicyAncestorStatus) bool { + defaultGroup := gatewayapiv1alpha2.Group(gatewayapiv1.GroupName) + defaultKind := gatewayapiv1alpha2.Kind(machinery.GatewayGroupKind.Kind) + defaultSectionName := gatewayapiv1.SectionName("") + ref := a.AncestorRef + return a.ControllerName == controllerName && + ptr.Deref(ref.Group, defaultGroup) == ptr.Deref(ancestor.Group, defaultGroup) && + ptr.Deref(ref.Kind, defaultKind) == ptr.Deref(ancestor.Kind, defaultKind) && + ptr.Deref(ref.Namespace, defaultNamespace) == ptr.Deref(ancestor.Namespace, defaultNamespace) && + ref.Name == ancestor.Name && + ptr.Deref(ref.SectionName, defaultSectionName) == ptr.Deref(ancestor.SectionName, defaultSectionName) + }); found { + return status.Conditions + } + return nil +} + +func IsListenerReady(listener *gatewayapiv1.Listener, gateway *gatewayapiv1.Gateway) bool { + listenerStatus, found := lo.Find(gateway.Status.Listeners, func(s gatewayapiv1.ListenerStatus) bool { + return s.Name == listener.Name + }) + if !found { + return false + } + return meta.IsStatusConditionTrue(listenerStatus.Conditions, string(gatewayapiv1.ListenerConditionProgrammed)) +} + +func IsHTTPRouteReady(httpRoute *gatewayapiv1.HTTPRoute, gateway *gatewayapiv1.Gateway, controllerName gatewayapiv1.GatewayController) bool { + routeStatus, found := lo.Find(httpRoute.Status.Parents, func(s gatewayapiv1.RouteParentStatus) bool { + ref := s.ParentRef + return s.ControllerName == controllerName && + ptr.Deref(ref.Group, gatewayapiv1.Group(gatewayapiv1.GroupName)) == gatewayapiv1.Group(gateway.GroupVersionKind().Group) && + ptr.Deref(ref.Kind, gatewayapiv1.Kind(machinery.GatewayGroupKind.Kind)) == gatewayapiv1.Kind(gateway.GroupVersionKind().Kind) && + ptr.Deref(ref.Namespace, gatewayapiv1.Namespace(httpRoute.GetNamespace())) == gatewayapiv1.Namespace(gateway.GetNamespace()) && + ref.Name == gatewayapiv1.ObjectName(gateway.GetName()) + }) + if !found { + return false + } + return meta.IsStatusConditionTrue(routeStatus.Conditions, string(gatewayapiv1.RouteConditionAccepted)) +} diff --git a/pkg/library/kuadrant/errors.go b/pkg/library/kuadrant/errors.go index da996b634..5b0a2935b 100644 --- a/pkg/library/kuadrant/errors.go +++ b/pkg/library/kuadrant/errors.go @@ -4,8 +4,9 @@ import ( "errors" "fmt" + "github.com/kuadrant/policy-machinery/machinery" apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" + k8stypes "k8s.io/apimachinery/pkg/types" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) @@ -42,6 +43,34 @@ func NewErrTargetNotFound(kind string, targetRef gatewayapiv1alpha2.LocalPolicyT } } +var _ PolicyError = ErrPolicyTargetNotFound{} + +type ErrPolicyTargetNotFound struct { + Kind string + TargetRef machinery.PolicyTargetReference + Err error +} + +func (e ErrPolicyTargetNotFound) Error() string { + if apierrors.IsNotFound(e.Err) { + return fmt.Sprintf("%s target %s was not found", e.Kind, e.TargetRef.GetName()) + } + + return fmt.Sprintf("%s target %s was not found: %s", e.Kind, e.TargetRef.GetName(), e.Err.Error()) +} + +func (e ErrPolicyTargetNotFound) Reason() gatewayapiv1alpha2.PolicyConditionReason { + return gatewayapiv1alpha2.PolicyReasonTargetNotFound +} + +func NewErrPolicyTargetNotFound(kind string, targetRef machinery.PolicyTargetReference, err error) ErrPolicyTargetNotFound { + return ErrPolicyTargetNotFound{ + Kind: kind, + TargetRef: targetRef, + Err: err, + } +} + var _ PolicyError = ErrInvalid{} type ErrInvalid struct { @@ -112,9 +141,29 @@ func NewErrUnknown(kind string, err error) ErrUnknown { var _ PolicyError = ErrOverridden{} +type ErrNoRoutes struct { + Kind string +} + +func (e ErrNoRoutes) Error() string { + return fmt.Sprintf("%s is not in the path to any existing routes", e.Kind) +} + +func (e ErrNoRoutes) Reason() gatewayapiv1alpha2.PolicyConditionReason { + return PolicyReasonUnknown +} + +func NewErrNoRoutes(kind string) ErrNoRoutes { + return ErrNoRoutes{ + Kind: kind, + } +} + +var _ PolicyError = ErrOverridden{} + type ErrOverridden struct { Kind string - OverridingPolicies []client.ObjectKey + OverridingPolicies []k8stypes.NamespacedName } func (e ErrOverridden) Error() string { @@ -125,13 +174,35 @@ func (e ErrOverridden) Reason() gatewayapiv1alpha2.PolicyConditionReason { return PolicyReasonOverridden } -func NewErrOverridden(kind string, overridingPolicies []client.ObjectKey) ErrOverridden { +func NewErrOverridden(kind string, overridingPolicies []k8stypes.NamespacedName) ErrOverridden { return ErrOverridden{ Kind: kind, OverridingPolicies: overridingPolicies, } } +var _ PolicyError = ErrOutOfSync{} + +type ErrOutOfSync struct { + Kind string + Components []string +} + +func (e ErrOutOfSync) Error() string { + return fmt.Sprintf("%s waiting for the following components to sync: %s", e.Kind, e.Components) +} + +func (e ErrOutOfSync) Reason() gatewayapiv1alpha2.PolicyConditionReason { + return PolicyReasonUnknown +} + +func NewErrOutOfSync(kind string, components []string) ErrOutOfSync { + return ErrOutOfSync{ + Kind: kind, + Components: components, + } +} + // IsTargetNotFound returns true if the specified error was created by NewErrTargetNotFound. func IsTargetNotFound(err error) bool { return reasonForError(err) == gatewayapiv1alpha2.PolicyReasonTargetNotFound diff --git a/pkg/library/mappers/gateway_test.go b/pkg/library/mappers/gateway_test.go index 6de310348..91b60c6c6 100644 --- a/pkg/library/mappers/gateway_test.go +++ b/pkg/library/mappers/gateway_test.go @@ -8,7 +8,9 @@ import ( "gotest.tools/assert" appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -31,7 +33,7 @@ func TestNewGatewayEventMapper(t *testing.T) { t.Fatal(err) } - err = gatewayapiv1.AddToScheme(testScheme) + err = gatewayapiv1.Install(testScheme) if err != nil { t.Fatal(err) } @@ -63,7 +65,7 @@ func TestNewGatewayEventMapper(t *testing.T) { t.Run("not gateway related event", func(subT *testing.T) { objs := []runtime.Object{} cl := clientBuilder(objs) - em := NewGatewayEventMapper(kuadrantv1beta3.NewRateLimitPolicyType(), WithClient(cl), WithLogger(log.NewLogger())) + em := NewGatewayEventMapper(&rateLimitPolicyType{}, WithClient(cl), WithLogger(log.NewLogger())) requests := em.Map(ctx, &gatewayapiv1.HTTPRoute{}) assert.DeepEqual(subT, []reconcile.Request{}, requests) }) @@ -71,7 +73,7 @@ func TestNewGatewayEventMapper(t *testing.T) { t.Run("gateway related event - no policies - no requests", func(subT *testing.T) { objs := []runtime.Object{} cl := clientBuilder(objs) - em := NewGatewayEventMapper(kuadrantv1beta3.NewRateLimitPolicyType(), WithClient(cl), WithLogger(log.NewLogger())) + em := NewGatewayEventMapper(&rateLimitPolicyType{}, WithClient(cl), WithLogger(log.NewLogger())) requests := em.Map(ctx, &gatewayapiv1.Gateway{}) assert.DeepEqual(subT, []reconcile.Request{}, requests) }) @@ -91,7 +93,7 @@ func TestNewGatewayEventMapper(t *testing.T) { }) objs := []runtime.Object{gw, route, pGw, pRoute} cl := clientBuilder(objs) - em := NewGatewayEventMapper(kuadrantv1beta3.NewRateLimitPolicyType(), WithClient(cl), WithLogger(log.NewLogger())) + em := NewGatewayEventMapper(&rateLimitPolicyType{}, WithClient(cl), WithLogger(log.NewLogger())) requests := em.Map(ctx, gw) assert.Equal(subT, len(requests), 2) assert.Assert(subT, utils.Index(requests, func(r reconcile.Request) bool { @@ -102,3 +104,43 @@ func TestNewGatewayEventMapper(t *testing.T) { }) >= 0) }) } + +const ( + RateLimitPolicyBackReferenceAnnotationName = "kuadrant.io/ratelimitpolicies" + RateLimitPolicyDirectReferenceAnnotationName = "kuadrant.io/ratelimitpolicy" +) + +type rateLimitPolicyType struct{} + +func (r rateLimitPolicyType) GetGVK() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: kuadrantv1beta3.GroupVersion.Group, + Version: kuadrantv1beta3.GroupVersion.Version, + Kind: "RateLimitPolicy", + } +} +func (r rateLimitPolicyType) GetInstance() client.Object { + return &kuadrantv1beta3.RateLimitPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantv1beta3.RateLimitPolicyGroupKind.Kind, + APIVersion: kuadrantv1beta3.GroupVersion.String(), + }, + } +} + +func (r rateLimitPolicyType) BackReferenceAnnotationName() string { + return RateLimitPolicyBackReferenceAnnotationName +} + +func (r rateLimitPolicyType) DirectReferenceAnnotationName() string { + return RateLimitPolicyDirectReferenceAnnotationName +} + +func (r rateLimitPolicyType) GetList(ctx context.Context, cl client.Client, listOpts ...client.ListOption) ([]kuadrantgatewayapi.Policy, error) { + rlpList := &kuadrantv1beta3.RateLimitPolicyList{} + err := cl.List(ctx, rlpList, listOpts...) + if err != nil { + return nil, err + } + return utils.Map(rlpList.Items, func(p kuadrantv1beta3.RateLimitPolicy) kuadrantgatewayapi.Policy { return &p }), nil +} diff --git a/pkg/library/mappers/utils_test.go b/pkg/library/mappers/utils_test.go index a7fbb0b11..50ed4b541 100644 --- a/pkg/library/mappers/utils_test.go +++ b/pkg/library/mappers/utils_test.go @@ -34,6 +34,6 @@ func policyFactory(ns, name string, targetRef gatewayapiv1alpha2.LocalPolicyTarg return &kuadrantv1beta3.RateLimitPolicy{ TypeMeta: metav1.TypeMeta{Kind: "RateLimitPolicy", APIVersion: kuadrantv1beta3.GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}, - Spec: kuadrantv1beta3.RateLimitPolicySpec{TargetRef: targetRef}, + Spec: kuadrantv1beta3.RateLimitPolicySpec{TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{LocalPolicyTargetReference: targetRef}}, } } diff --git a/pkg/library/reconcilers/base_reconciler.go b/pkg/library/reconcilers/base_reconciler.go index dbfc527ab..bf4d73f56 100644 --- a/pkg/library/reconcilers/base_reconciler.go +++ b/pkg/library/reconcilers/base_reconciler.go @@ -25,7 +25,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -72,21 +71,17 @@ type BaseReconciler struct { scheme *runtime.Scheme apiClientReader client.Reader logger logr.Logger - recorder record.EventRecorder } // blank assignment to verify that BaseReconciler implements reconcile.Reconciler var _ reconcile.Reconciler = &BaseReconciler{} -func NewBaseReconciler( - client client.Client, scheme *runtime.Scheme, apiClientReader client.Reader, - logger logr.Logger, recorder record.EventRecorder) *BaseReconciler { +func NewBaseReconciler(client client.Client, scheme *runtime.Scheme, apiClientReader client.Reader, logger logr.Logger) *BaseReconciler { return &BaseReconciler{ client: client, scheme: scheme, apiClientReader: apiClientReader, logger: logger, - recorder: recorder, } } @@ -114,10 +109,6 @@ func (b *BaseReconciler) Logger() logr.Logger { return b.logger } -func (b *BaseReconciler) EventRecorder() record.EventRecorder { - return b.recorder -} - // ReconcileResource attempts to mutate the existing state // in order to match the desired state. The object's desired state must be reconciled // with the existing state inside the passed in callback MutateFn. diff --git a/pkg/library/reconcilers/base_reconciler_test.go b/pkg/library/reconcilers/base_reconciler_test.go index f74404a57..90ad7d161 100644 --- a/pkg/library/reconcilers/base_reconciler_test.go +++ b/pkg/library/reconcilers/base_reconciler_test.go @@ -32,7 +32,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -77,9 +76,8 @@ func TestBaseReconcilerCreate(t *testing.T) { // Create a fake client to mock API calls. cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(10000) - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) + baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log) desiredConfigmap := &v1.ConfigMap{ TypeMeta: metav1.TypeMeta{ @@ -144,9 +142,8 @@ func TestBaseReconcilerUpdateNeeded(t *testing.T) { // Create a fake client to mock API calls. cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(10000) - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) + baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log) desiredConfigmap := &v1.ConfigMap{ TypeMeta: metav1.TypeMeta{ @@ -230,9 +227,8 @@ func TestBaseReconcilerDelete(t *testing.T) { // Create a fake client to mock API calls. cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(10000) - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) + baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log) desired := &v1.ConfigMap{ TypeMeta: metav1.TypeMeta{ diff --git a/pkg/library/utils/hostname.go b/pkg/library/utils/hostname.go index f1781b087..af6ff7a11 100644 --- a/pkg/library/utils/hostname.go +++ b/pkg/library/utils/hostname.go @@ -53,3 +53,25 @@ func HostnamesToStrings(hostnames []gatewayapiv1.Hostname) []string { return string(hostname) }) } + +// SortableHostnames is a slice of hostnames that can be sorted from the most specific to the least specific +type SortableHostnames []string + +func (h SortableHostnames) Len() int { return len(h) } +func (h SortableHostnames) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h SortableHostnames) Less(i, j int) bool { return CompareHostnamesSpecificity(h[i], h[j]) } + +// CompareHostnamesSpecificity returns true if hostname1 is more specific than hostname2 +func CompareHostnamesSpecificity(hostname1, hostname2 string) bool { + labels1 := len(strings.Split(hostname1, ".")) + labels2 := len(strings.Split(hostname2, ".")) + if labels1 != labels2 { + return labels1 > labels2 + } + hasWildcard1 := strings.HasPrefix(hostname1, "*") + hasWildcard2 := strings.HasPrefix(hostname2, "*") + if hasWildcard1 != hasWildcard2 { + return !hasWildcard1 + } + return hostname1 < hostname2 +} diff --git a/pkg/library/utils/hostname_test.go b/pkg/library/utils/hostname_test.go index 5c0939890..836b66ed2 100644 --- a/pkg/library/utils/hostname_test.go +++ b/pkg/library/utils/hostname_test.go @@ -4,6 +4,7 @@ package utils import ( "reflect" + "sort" "testing" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -128,3 +129,47 @@ func TestHostnamesToStrings(t *testing.T) { }) } } + +func TestSortableHostnames(t *testing.T) { + testCases := []struct { + name string + inputHostnames []string + expectedOutput []string + }{ + { + name: "when input is empty then return empty output", + inputHostnames: []string{}, + expectedOutput: []string{}, + }, + { + name: "when input has a single precise hostname then return the hostname", + inputHostnames: []string{"example.com"}, + expectedOutput: []string{"example.com"}, + }, + { + name: "when input has multiple precise hostnames then return the hostnames ordered lexicographically", + inputHostnames: []string{"example.com", "test.com", "localhost"}, + expectedOutput: []string{"example.com", "test.com", "localhost"}, + }, + { + name: "when input has precise and wildcard hostnames then return the hostnames ordered from most specific to least specific", + inputHostnames: []string{"*.com", "*.example.com", "*", "other.example.com"}, + expectedOutput: []string{"other.example.com", "*.example.com", "*.com", "*"}, + }, + { + name: "when input contains repeated hostnames then return the equal hostnames adjacent to each other", + inputHostnames: []string{"*.com", "other.example.com", "*.example.com", "*", "*.com", "*.example.com", "other.example.com"}, + expectedOutput: []string{"other.example.com", "other.example.com", "*.example.com", "*.example.com", "*.com", "*.com", "*"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + hostnames := SortableHostnames(tc.inputHostnames) + sort.Sort(hostnames) + if !reflect.DeepEqual(tc.expectedOutput, tc.expectedOutput) { + t.Errorf("Unexpected output. Expected %v but got %v", tc.expectedOutput, tc.expectedOutput) + } + }) + } +} diff --git a/pkg/rlptools/rate_limit_index.go b/pkg/ratelimit/index.go similarity index 66% rename from pkg/rlptools/rate_limit_index.go rename to pkg/ratelimit/index.go index c391981c2..fbfa38727 100644 --- a/pkg/rlptools/rate_limit_index.go +++ b/pkg/ratelimit/index.go @@ -1,54 +1,54 @@ -package rlptools +package ratelimit import ( "reflect" "sort" "strings" + "sync" "github.com/elliotchance/orderedmap/v2" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "k8s.io/apimachinery/pkg/types" "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) -type RateLimitIndexKey struct { - RateLimitPolicyKey types.NamespacedName - GatewayKey types.NamespacedName +// NewIndex builds an index to manage sets of rate limits, organized by key +func NewIndex() *Index { + return &Index{OrderedMap: *orderedmap.NewOrderedMap[string, LimitadorRateLimits]()} } -// NewRateLimitIndex builds an index to manage sets of rate limits, organized by key -func NewRateLimitIndex() *RateLimitIndex { - return &RateLimitIndex{*orderedmap.NewOrderedMap[RateLimitIndexKey, RateLimitList]()} +// Index stores LimitadorRateLimitss by key +type Index struct { + sync.RWMutex + orderedmap.OrderedMap[string, LimitadorRateLimits] } -// RateLimitIndex stores RateLimitLists by key -type RateLimitIndex struct { - orderedmap.OrderedMap[RateLimitIndexKey, RateLimitList] -} - -func (l *RateLimitIndex) Set(key RateLimitIndexKey, rateLimits RateLimitList) { +func (l *Index) Set(key string, rateLimits LimitadorRateLimits) { if len(rateLimits) == 0 { return } + l.Lock() + defer l.Unlock() l.OrderedMap.Set(key, rateLimits) } -func (l *RateLimitIndex) ToRateLimits() RateLimitList { - limitadorRateLimits := make(RateLimitList, 0) +func (l *Index) ToRateLimits() LimitadorRateLimits { + l.RLock() + defer l.RUnlock() + limitadorRateLimits := make(LimitadorRateLimits, 0) for rlSet := l.Front(); rlSet != nil; rlSet = rlSet.Next() { limitadorRateLimits = append(limitadorRateLimits, rlSet.Value...) } return limitadorRateLimits } -type RateLimitList []limitadorv1alpha1.RateLimit +type LimitadorRateLimits []limitadorv1alpha1.RateLimit -func (l RateLimitList) Len() int { +func (l LimitadorRateLimits) Len() int { return len(l) } -func (l RateLimitList) Less(i, j int) bool { +func (l LimitadorRateLimits) Less(i, j int) bool { if l[i].MaxValue != l[j].MaxValue { return l[i].MaxValue > l[j].MaxValue } @@ -93,20 +93,20 @@ func (l RateLimitList) Less(i, j int) bool { return true } -func (l RateLimitList) Swap(i, j int) { +func (l LimitadorRateLimits) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -func Equal(a, b RateLimitList) bool { - if len(a) != len(b) { +func (l LimitadorRateLimits) EqualTo(other LimitadorRateLimits) bool { + if len(l) != len(other) { return false } - aCopy := make(RateLimitList, len(a)) - bCopy := make(RateLimitList, len(b)) + aCopy := make(LimitadorRateLimits, len(l)) + bCopy := make(LimitadorRateLimits, len(other)) - copy(aCopy, a) - copy(bCopy, b) + copy(aCopy, l) + copy(bCopy, other) // two limits with reordered conditions/variables are effectively the same // For comparison purposes, nil equals the empty array for conditions and variables diff --git a/pkg/rlptools/rate_limit_index_test.go b/pkg/ratelimit/index_test.go similarity index 69% rename from pkg/rlptools/rate_limit_index_test.go rename to pkg/ratelimit/index_test.go index a5a8d0519..5ba886065 100644 --- a/pkg/rlptools/rate_limit_index_test.go +++ b/pkg/ratelimit/index_test.go @@ -1,21 +1,19 @@ //go:build unit -package rlptools +package ratelimit import ( "reflect" "testing" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" ) -func TestRateLimitIndexSet(t *testing.T) { +func TestIndexSet(t *testing.T) { t.Run("index rate limits to a key", func(subT *testing.T) { - index := NewRateLimitIndex() + index := NewIndex() - key := RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-1", Namespace: "ns"}} - index.Set(key, []limitadorv1alpha1.RateLimit{ + index.Set("foo", []limitadorv1alpha1.RateLimit{ {Namespace: "ns/rlp-1", MaxValue: 10, Seconds: 1}, {Namespace: "ns/rlp-1", MaxValue: 100, Seconds: 60}, {Namespace: "ns/rlp-1", MaxValue: 1000, Seconds: 1}, @@ -29,19 +27,19 @@ func TestRateLimitIndexSet(t *testing.T) { }) t.Run("index rate limits to different keys", func(subT *testing.T) { - index := NewRateLimitIndex() + index := NewIndex() - index.Set(RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-1", Namespace: "ns"}}, []limitadorv1alpha1.RateLimit{ + index.Set("foo", []limitadorv1alpha1.RateLimit{ {Namespace: "ns/rlp-1", MaxValue: 10, Seconds: 1}, {Namespace: "ns/rlp-1", MaxValue: 100, Seconds: 60}, {Namespace: "ns/rlp-1", MaxValue: 1000, Seconds: 1}, }) - index.Set(RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-2", Namespace: "ns"}}, []limitadorv1alpha1.RateLimit{ + index.Set("bar", []limitadorv1alpha1.RateLimit{ {Namespace: "ns/rlp-2", MaxValue: 50, Seconds: 1}, }) - key := RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-1", Namespace: "ns"}} + key := "foo" rateLimits, found := index.Get(key) if !found { subT.Fatal("expected rate limits to be indexed to key but none found:", key) @@ -51,7 +49,7 @@ func TestRateLimitIndexSet(t *testing.T) { subT.Fatal("expected:", expectedCount, "rate limits for key", key, ", returned:", len(rateLimits)) } - key = RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-2", Namespace: "ns"}} + key = "bar" rateLimits, found = index.Get(key) if !found { subT.Fatal("expected rate limits to be indexed to key but none found:", key) @@ -69,15 +67,15 @@ func TestRateLimitIndexSet(t *testing.T) { }) t.Run("reset rate limits for an existing key", func(subT *testing.T) { - index := NewRateLimitIndex() + index := NewIndex() - index.Set(RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-1", Namespace: "ns"}}, []limitadorv1alpha1.RateLimit{ + index.Set("foo", []limitadorv1alpha1.RateLimit{ {Namespace: "ns/rlp-1", MaxValue: 10, Seconds: 1}, {Namespace: "ns/rlp-1", MaxValue: 100, Seconds: 60}, {Namespace: "ns/rlp-1", MaxValue: 1000, Seconds: 1}, }) - index.Set(RateLimitIndexKey{RateLimitPolicyKey: client.ObjectKey{Name: "rlp-1", Namespace: "ns"}}, []limitadorv1alpha1.RateLimit{ + index.Set("foo", []limitadorv1alpha1.RateLimit{ {Namespace: "ns/rlp-1", MaxValue: 500, Seconds: 3600}, }) @@ -92,9 +90,9 @@ func TestRateLimitIndexSet(t *testing.T) { }) t.Run("add an empty list of limits if a noop", func(subT *testing.T) { - idx := NewRateLimitIndex() + idx := NewIndex() - idx.Set(RateLimitIndexKey{GatewayKey: client.ObjectKey{Name: "gwA", Namespace: "nsA"}}, []limitadorv1alpha1.RateLimit{}) + idx.Set("foo", []limitadorv1alpha1.RateLimit{}) aggregatedRateLimits := idx.ToRateLimits() if len(aggregatedRateLimits) != 0 { @@ -103,9 +101,9 @@ func TestRateLimitIndexSet(t *testing.T) { }) t.Run("add nil list of limits if a noop", func(subT *testing.T) { - idx := NewRateLimitIndex() + idx := NewIndex() - idx.Set(RateLimitIndexKey{GatewayKey: client.ObjectKey{Name: "gwA", Namespace: "nsA"}}, []limitadorv1alpha1.RateLimit{}) + idx.Set("foo", []limitadorv1alpha1.RateLimit{}) aggregatedRateLimits := idx.ToRateLimits() if len(aggregatedRateLimits) != 0 { @@ -114,9 +112,9 @@ func TestRateLimitIndexSet(t *testing.T) { }) } -func TestRateLimitIndexToRateLimits(t *testing.T) { +func TestIndexToRateLimits(t *testing.T) { t.Run("nil index return empty list", func(subT *testing.T) { - idx := NewRateLimitIndex() + idx := NewIndex() limits := idx.ToRateLimits() if limits == nil { @@ -128,7 +126,7 @@ func TestRateLimitIndexToRateLimits(t *testing.T) { }) t.Run("empty index return empty list", func(subT *testing.T) { - idx := NewRateLimitIndex() + idx := NewIndex() limits := idx.ToRateLimits() if limits == nil { diff --git a/pkg/rlptools/overrides.go b/pkg/rlptools/overrides.go deleted file mode 100644 index f361ef16f..000000000 --- a/pkg/rlptools/overrides.go +++ /dev/null @@ -1,62 +0,0 @@ -package rlptools - -import ( - "slices" - - "github.com/samber/lo" - "sigs.k8s.io/controller-runtime/pkg/client" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -// ApplyOverrides applies the overrides defined in the RateLimitPolicies attached to the gateway policies for a given -// gateway, and returns a new topology with all policies overridden as applicable. -func ApplyOverrides(topology *kuadrantgatewayapi.Topology, gateway *gatewayapiv1.Gateway) (*kuadrantgatewayapi.Topology, error) { - gatewayNode, ok := lo.Find(topology.Gateways(), func(g kuadrantgatewayapi.GatewayNode) bool { - return g.ObjectKey() == client.ObjectKeyFromObject(gateway) - }) - if !ok || len(gatewayNode.AttachedPolicies()) == 0 { - return topology, nil - } - - overridePolicies := lo.FilterMap(gatewayNode.AttachedPolicies(), func(pNode kuadrantgatewayapi.PolicyNode, _ int) (*kuadrantv1beta3.RateLimitPolicy, bool) { - policy := pNode.Policy - rlp, ok := policy.(*kuadrantv1beta3.RateLimitPolicy) - if !ok || rlp.Spec.Overrides == nil { - return nil, false - } - return rlp, true - }) - - if len(overridePolicies) == 0 { - return topology, nil - } - - overriddenPolicies := lo.Map(overridePolicies, func(p *kuadrantv1beta3.RateLimitPolicy, _ int) kuadrantgatewayapi.Policy { return p }) - - for _, route := range topology.Routes() { - if !slices.Contains(kuadrantgatewayapi.GetRouteAcceptedGatewayParentKeys(route.HTTPRoute), client.ObjectKeyFromObject(gateway)) { - routePolicies := utils.Map(route.AttachedPolicies(), func(pNode kuadrantgatewayapi.PolicyNode) kuadrantgatewayapi.Policy { return pNode.Policy }) - overriddenPolicies = append(overriddenPolicies, routePolicies...) - continue - } - - for _, policy := range route.AttachedPolicies() { - overriddenPolicy := policy.DeepCopyObject().(*kuadrantv1beta3.RateLimitPolicy) - overriddenPolicy.Spec.CommonSpec().Limits = overridePolicies[0].Spec.Overrides.Limits - overriddenPolicies = append(overriddenPolicies, overriddenPolicy) - } - } - - return kuadrantgatewayapi.NewTopology( - kuadrantgatewayapi.WithAcceptedRoutesLinkedOnly(), - kuadrantgatewayapi.WithProgrammedGatewaysOnly(), - kuadrantgatewayapi.WithGateways(lo.Map(topology.Gateways(), func(g kuadrantgatewayapi.GatewayNode, _ int) *gatewayapiv1.Gateway { return g.Gateway })), - kuadrantgatewayapi.WithRoutes(lo.Map(topology.Routes(), func(r kuadrantgatewayapi.RouteNode, _ int) *gatewayapiv1.HTTPRoute { return r.HTTPRoute })), - kuadrantgatewayapi.WithPolicies(overriddenPolicies), - kuadrantgatewayapi.WithLogger(topology.Logger), - ) -} diff --git a/pkg/rlptools/utils.go b/pkg/rlptools/utils.go deleted file mode 100644 index f5472e695..000000000 --- a/pkg/rlptools/utils.go +++ /dev/null @@ -1,68 +0,0 @@ -package rlptools - -import ( - "fmt" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" -) - -func LimitsNameFromRLP(rlp *kuadrantv1beta3.RateLimitPolicy) string { - return wasm.LimitsNamespaceFromRLP(rlp) -} - -var timeUnitMap = map[kuadrantv1beta3.TimeUnit]int{ - kuadrantv1beta3.TimeUnit("second"): 1, - kuadrantv1beta3.TimeUnit("minute"): 60, - kuadrantv1beta3.TimeUnit("hour"): 60 * 60, - kuadrantv1beta3.TimeUnit("day"): 60 * 60 * 24, -} - -// LimitadorRateLimitsFromRLP converts rate limits from a Kuadrant RateLimitPolicy into a list of Limitador rate limit -// objects -func LimitadorRateLimitsFromRLP(rlp *kuadrantv1beta3.RateLimitPolicy) []limitadorv1alpha1.RateLimit { - limitsNamespace := wasm.LimitsNamespaceFromRLP(rlp) - rlpKey := client.ObjectKeyFromObject(rlp) - - rateLimits := make([]limitadorv1alpha1.RateLimit, 0) - for limitKey, limit := range rlp.Spec.CommonSpec().Limits { - limitIdentifier := wasm.LimitNameToLimitadorIdentifier(rlpKey, limitKey) - for _, rate := range limit.Rates { - maxValue, seconds := rateToSeconds(rate) - rateLimits = append(rateLimits, limitadorv1alpha1.RateLimit{ - Namespace: limitsNamespace, - MaxValue: maxValue, - Seconds: seconds, - Conditions: []string{fmt.Sprintf("%s == \"1\"", limitIdentifier)}, - Variables: utils.GetEmptySliceIfNil(limit.CountersAsStringList()), - Name: LimitsNameFromRLP(rlp), - }) - } - } - return rateLimits -} - -// rateToSeconds converts from RLP Rate API (limit, duration and unit) -// to Limitador's Limit format (maxValue, Seconds) -func rateToSeconds(rate kuadrantv1beta3.Rate) (maxValue int, seconds int) { - maxValue = rate.Limit - seconds = 0 - - if tmpSecs, ok := timeUnitMap[rate.Unit]; ok && rate.Duration > 0 { - seconds = tmpSecs * rate.Duration - } - - if rate.Duration < 0 { - seconds = 0 - } - - if rate.Limit < 0 { - maxValue = 0 - } - - return -} diff --git a/pkg/rlptools/utils_test.go b/pkg/rlptools/utils_test.go deleted file mode 100644 index 8d414847a..000000000 --- a/pkg/rlptools/utils_test.go +++ /dev/null @@ -1,337 +0,0 @@ -//go:build unit - -package rlptools - -import ( - "reflect" - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -func testRLP_1Limit_1Rate(ns, name string) *kuadrantv1beta3.RateLimitPolicy { - return &kuadrantv1beta3.RateLimitPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "RateLimitPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Spec: kuadrantv1beta3.RateLimitPolicySpec{ - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 5, - Duration: 10, - Unit: "second", - }, - }, - }, - }, - }, - }, - } -} - -func testRLP_2Limits_1Rate(ns, name string) *kuadrantv1beta3.RateLimitPolicy { - return &kuadrantv1beta3.RateLimitPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "RateLimitPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Spec: kuadrantv1beta3.RateLimitPolicySpec{ - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 5, - Duration: 10, - Unit: "second", - }, - }, - }, - "l2": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 3, - Duration: 1, - Unit: "hour", - }, - }, - }, - }, - }, - }, - } -} - -func testRLP_1Limit_2Rates(ns, name string) *kuadrantv1beta3.RateLimitPolicy { - return &kuadrantv1beta3.RateLimitPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "RateLimitPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Spec: kuadrantv1beta3.RateLimitPolicySpec{ - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 5, - Duration: 10, - Unit: "second", - }, - { - Limit: 3, - Duration: 1, - Unit: "minute", - }, - }, - }, - }, - }, - }, - } -} - -func testRLP_1Limit_1Rate_1Counter(ns, name string) *kuadrantv1beta3.RateLimitPolicy { - return &kuadrantv1beta3.RateLimitPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "RateLimitPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Spec: kuadrantv1beta3.RateLimitPolicySpec{ - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Counters: []kuadrantv1beta3.ContextSelector{ - "request.path", - }, - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 5, - Duration: 10, - Unit: "second", - }, - }, - }, - }, - }, - }, - } -} - -func TestLimitadorRateLimitsFromRLP(t *testing.T) { - testCases := []struct { - name string - rlp *kuadrantv1beta3.RateLimitPolicy - expected []limitadorv1alpha1.RateLimit - }{ - { - name: "basic: 1 limit, 1 rate", - rlp: testRLP_1Limit_1Rate("testNS", "rlpA"), - expected: []limitadorv1alpha1.RateLimit{ - { - Namespace: "testNS/rlpA", - MaxValue: 5, - Seconds: 10, - Conditions: []string{`limit.l1__65f19ee8 == "1"`}, - Variables: []string{}, - Name: "testNS/rlpA", - }, - }, - }, - { - name: "multiple limits: 2 limits with 1 rate each", - rlp: testRLP_2Limits_1Rate("testNS", "rlpA"), - expected: []limitadorv1alpha1.RateLimit{ - { - Namespace: "testNS/rlpA", - MaxValue: 5, - Seconds: 10, - Conditions: []string{`limit.l1__65f19ee8 == "1"`}, - Variables: []string{}, - Name: "testNS/rlpA", - }, - { - Namespace: "testNS/rlpA", - MaxValue: 3, - Seconds: 3600, - Conditions: []string{`limit.l2__3e871d60 == "1"`}, - Variables: []string{}, - Name: "testNS/rlpA", - }, - }, - }, - { - name: "multiple rates: 1 limit with 2 rates", - rlp: testRLP_1Limit_2Rates("testNS", "rlpA"), - expected: []limitadorv1alpha1.RateLimit{ - { - Namespace: "testNS/rlpA", - MaxValue: 5, - Seconds: 10, - Conditions: []string{`limit.l1__65f19ee8 == "1"`}, - Variables: []string{}, - Name: "testNS/rlpA", - }, - { - Namespace: "testNS/rlpA", - MaxValue: 3, - Seconds: 60, - Conditions: []string{`limit.l1__65f19ee8 == "1"`}, - Variables: []string{}, - Name: "testNS/rlpA", - }, - }, - }, - { - name: "basic: 1 limit, 1 rate", - rlp: testRLP_1Limit_1Rate_1Counter("testNS", "rlpA"), - expected: []limitadorv1alpha1.RateLimit{ - { - Namespace: "testNS/rlpA", - MaxValue: 5, - Seconds: 10, - Conditions: []string{`limit.l1__65f19ee8 == "1"`}, - Variables: []string{"request.path"}, - Name: "testNS/rlpA", - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(subT *testing.T) { - rateLimits := LimitadorRateLimitsFromRLP(tc.rlp) - // Instead of sorting to compare, check len and then iterate - if len(rateLimits) != len(tc.expected) { - subT.Errorf("expected limits len (%d), got (%d)", len(tc.expected), len(rateLimits)) - } - // When both slices have equal length, items can be checked one by one. - for _, rl := range rateLimits { - if _, found := utils.Find(tc.expected, func(expectedRateLimit limitadorv1alpha1.RateLimit) bool { - return reflect.DeepEqual(rl, expectedRateLimit) - }); !found { - subT.Errorf("returned rate limit (%+v) not within expected ones, expected: %v", rl, tc.expected) - } - } - }) - } -} - -func TestConvertRateIntoSeconds(t *testing.T) { - testCases := []struct { - name string - rate kuadrantv1beta3.Rate - expectedMaxValue int - expectedSeconds int - }{ - { - name: "seconds", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("second"), - }, - expectedMaxValue: 5, - expectedSeconds: 2, - }, - { - name: "minutes", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("minute"), - }, - expectedMaxValue: 5, - expectedSeconds: 2 * 60, - }, - { - name: "hours", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("hour"), - }, - expectedMaxValue: 5, - expectedSeconds: 2 * 60 * 60, - }, - { - name: "day", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("day"), - }, - expectedMaxValue: 5, - expectedSeconds: 2 * 60 * 60 * 24, - }, - { - name: "negative limit", - rate: kuadrantv1beta3.Rate{ - Limit: -5, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("second"), - }, - expectedMaxValue: 0, - expectedSeconds: 2, - }, - { - name: "negative duration", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: -2, Unit: kuadrantv1beta3.TimeUnit("second"), - }, - expectedMaxValue: 5, - expectedSeconds: 0, - }, - { - name: "limit is 0", - rate: kuadrantv1beta3.Rate{ - Limit: 0, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("second"), - }, - expectedMaxValue: 0, - expectedSeconds: 2, - }, - { - name: "rate is 0", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: 0, Unit: kuadrantv1beta3.TimeUnit("second"), - }, - expectedMaxValue: 5, - expectedSeconds: 0, - }, - { - name: "unexpected time unit", - rate: kuadrantv1beta3.Rate{ - Limit: 5, Duration: 2, Unit: kuadrantv1beta3.TimeUnit("unknown"), - }, - expectedMaxValue: 5, - expectedSeconds: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(subT *testing.T) { - maxValue, seconds := rateToSeconds(tc.rate) - if maxValue != tc.expectedMaxValue { - subT.Errorf("maxValue does not match, expected(%d), got (%d)", tc.expectedMaxValue, maxValue) - } - if seconds != tc.expectedSeconds { - subT.Errorf("seconds does not match, expected(%d), got (%d)", tc.expectedSeconds, seconds) - } - }) - } -} diff --git a/pkg/rlptools/wasm/utils.go b/pkg/rlptools/wasm/utils.go deleted file mode 100644 index dc5ce74b3..000000000 --- a/pkg/rlptools/wasm/utils.go +++ /dev/null @@ -1,389 +0,0 @@ -package wasm - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "slices" - "sort" - "unicode" - - "github.com/go-logr/logr" - "github.com/samber/lo" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/env" - "sigs.k8s.io/controller-runtime/pkg/client" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -const ( - LimitadorRateLimitIdentifierPrefix = "limit." - RateLimitPolicyExtensionName = "limitador" -) - -var ( - WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") -) - -func LimitsNamespaceFromRLP(rlp *kuadrantv1beta3.RateLimitPolicy) string { - return fmt.Sprintf("%s/%s", rlp.GetNamespace(), rlp.GetName()) -} - -// Rules computes WASM rules from the policy and the targeted route. -// It returns an empty list of wasm rules if the policy specifies no limits or if all limits specified in the policy -// fail to match any route rule according to the limits route selectors. -func Rules(rlp *kuadrantv1beta3.RateLimitPolicy, route *gatewayapiv1.HTTPRoute) []Rule { - rules := make([]Rule, 0) - if rlp == nil { - return rules - } - - rlpKey := client.ObjectKeyFromObject(rlp) - limits := rlp.Spec.CommonSpec().Limits - - // Sort RLP limits for consistent comparison with existing wasmplugin objects - limitNames := lo.Keys(limits) - slices.Sort(limitNames) - - for _, limitName := range limitNames { - // 1 RLP limit <---> 1 WASM rule - limit := limits[limitName] - limitIdentifier := LimitNameToLimitadorIdentifier(rlpKey, limitName) - rule, err := ruleFromLimit(rlp, limitIdentifier, &limit, route) - if err == nil { - rules = append(rules, rule) - } - } - - return rules -} - -func LimitNameToLimitadorIdentifier(rlpKey types.NamespacedName, uniqueLimitName string) string { - identifier := LimitadorRateLimitIdentifierPrefix - - // sanitize chars that are not allowed in limitador identifiers - for _, c := range uniqueLimitName { - if unicode.IsLetter(c) || unicode.IsDigit(c) || c == '_' { - identifier += string(c) - } else { - identifier += "_" - } - } - - // to avoid breaking the uniqueness of the limit name after sanitization, we add a hash of the original name - hash := sha256.Sum256([]byte(fmt.Sprintf("%s/%s", rlpKey.String(), uniqueLimitName))) - identifier += "__" + hex.EncodeToString(hash[:4]) - - return identifier -} - -func ruleFromLimit(rlp *kuadrantv1beta3.RateLimitPolicy, limitIdentifier string, limit *kuadrantv1beta3.Limit, route *gatewayapiv1.HTTPRoute) (Rule, error) { - rule := Rule{} - - conditions, err := conditionsFromLimit(limit, route) - if err != nil { - return rule, err - } - - rule.Conditions = conditions - - if data := dataFromLimit(limitIdentifier, limit); data != nil { - rule.Actions = []Action{ - { - Scope: LimitsNamespaceFromRLP(rlp), - ExtensionName: RateLimitPolicyExtensionName, - Data: data, - }, - } - } - - return rule, nil -} - -func conditionsFromLimit(limit *kuadrantv1beta3.Limit, route *gatewayapiv1.HTTPRoute) ([]Condition, error) { - if limit == nil { - return nil, errors.New("limit should not be nil") - } - - routeConditions := make([]Condition, 0) - - // build conditions from all rules - for _, rule := range route.Spec.Rules { - routeConditions = append(routeConditions, conditionsFromRule(rule)...) - } - - if len(limit.When) == 0 { - if len(routeConditions) == 0 { - return nil, nil - } - return routeConditions, nil - } - - if len(routeConditions) > 0 { - // merge the 'when' conditions into each route level one - mergedConditions := make([]Condition, len(routeConditions)) - for _, when := range limit.When { - for idx := range routeConditions { - mergedCondition := routeConditions[idx] - mergedCondition.AllOf = append(mergedCondition.AllOf, patternExpresionFromWhen(when)) - mergedConditions[idx] = mergedCondition - } - } - return mergedConditions, nil - } - - // build conditions only from the 'when' field - whenConditions := make([]Condition, len(limit.When)) - for idx, when := range limit.When { - whenConditions[idx] = Condition{AllOf: []PatternExpression{patternExpresionFromWhen(when)}} - } - return whenConditions, nil -} - -// conditionsFromRule builds a list of conditions from a rule -// rules that specify no explicit match are assumed to match all request (i.e. implicit catch-all rule) -func conditionsFromRule(rule gatewayapiv1.HTTPRouteRule) []Condition { - return utils.Map(rule.Matches, func(match gatewayapiv1.HTTPRouteMatch) Condition { - return Condition{AllOf: patternExpresionsFromMatch(match)} - }) -} - -func patternExpresionsFromMatch(match gatewayapiv1.HTTPRouteMatch) []PatternExpression { - expressions := make([]PatternExpression, 0) - - // path - if match.Path != nil { - expressions = append(expressions, patternExpresionFromPathMatch(*match.Path)) - } - - // method - if match.Method != nil { - expressions = append(expressions, patternExpresionFromMethod(*match.Method)) - } - - // headers - for _, headerMatch := range match.Headers { - // Multiple match values are ANDed together - expressions = append(expressions, patternExpresionFromHeader(headerMatch)) - } - - // TODO(eguzki): query params. Investigate integration with wasm regarding Envoy params - // from https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes - // request.query -> string : The query portion of the URL in the format of “name1=value1&name2=value2”. - - return expressions -} - -func patternExpresionFromPathMatch(pathMatch gatewayapiv1.HTTPPathMatch) PatternExpression { - var ( - operator = PatternOperator(kuadrantv1beta3.StartsWithOperator) // default value - value = "/" // default value - ) - - if pathMatch.Value != nil { - value = *pathMatch.Value - } - - if pathMatch.Type != nil { - if val, ok := PathMatchTypeMap[*pathMatch.Type]; ok { - operator = val - } - } - - return PatternExpression{ - Selector: "request.url_path", - Operator: operator, - Value: value, - } -} - -func patternExpresionFromMethod(method gatewayapiv1.HTTPMethod) PatternExpression { - return PatternExpression{ - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: string(method), - } -} - -func patternExpresionFromHeader(headerMatch gatewayapiv1.HTTPHeaderMatch) PatternExpression { - // As for gateway api v1, the only operation type with core support is Exact match. - // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderMatch - - return PatternExpression{ - Selector: kuadrantv1beta3.ContextSelector(fmt.Sprintf("request.headers.%s", headerMatch.Name)), - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: headerMatch.Value, - } -} - -func patternExpresionFromWhen(when kuadrantv1beta3.WhenCondition) PatternExpression { - return PatternExpression{ - Selector: when.Selector, - Operator: PatternOperator(when.Operator), - Value: when.Value, - } -} - -func dataFromLimit(limitIdentifier string, limit *kuadrantv1beta3.Limit) (data []DataType) { - if limit == nil { - return - } - - // static key representing the limit - data = append(data, - DataType{ - Value: &Static{ - Static: StaticSpec{Key: limitIdentifier, Value: "1"}, - }, - }, - ) - - for _, counter := range limit.Counters { - data = append(data, - DataType{ - Value: &Selector{ - Selector: SelectorSpec{Selector: counter}, - }, - }, - ) - } - - return data -} - -func routeFromRLP(ctx context.Context, t *kuadrantgatewayapi.TopologyIndexes, rlp *kuadrantv1beta3.RateLimitPolicy, gw *gatewayapiv1.Gateway) (*gatewayapiv1.HTTPRoute, error) { - logger, err := logr.FromContext(ctx) - if err != nil { - return nil, err - } - - route := t.GetPolicyHTTPRoute(rlp) - - if route == nil { - // The policy is targeting a gateway - // This gateway policy will be enforced into all HTTPRoutes that do not have a policy attached to it - - // Build imaginary route with all the routes not having a RLP targeting it - untargetedRoutes := t.GetUntargetedRoutes(gw) - - if len(untargetedRoutes) == 0 { - // For policies targeting a gateway, when no httproutes is attached to the gateway, skip wasm config - // test wasm config when no http routes attached to the gateway - logger.V(1).Info("no untargeted httproutes attached to the targeted gateway, skipping wasm config for the gateway rlp", "ratelimitpolicy", client.ObjectKeyFromObject(rlp)) - return nil, nil - } - - untargetedRules := make([]gatewayapiv1.HTTPRouteRule, 0) - for idx := range untargetedRoutes { - untargetedRules = append(untargetedRules, untargetedRoutes[idx].Spec.Rules...) - } - gwHostnamesTmp := kuadrantgatewayapi.TargetHostnames(gw) - gwHostnames := utils.Map(gwHostnamesTmp, func(str string) gatewayapiv1.Hostname { return gatewayapiv1.Hostname(str) }) - route = &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: gwHostnames, - Rules: untargetedRules, - }, - } - } - - return route, nil -} - -func wasmRateLimitPolicy(ctx context.Context, t *kuadrantgatewayapi.TopologyIndexes, rlp *kuadrantv1beta3.RateLimitPolicy, gw *gatewayapiv1.Gateway) (*Policy, error) { - route, err := routeFromRLP(ctx, t, rlp, gw) - if err != nil { - return nil, err - } - if route == nil { - // no need to add the policy if there are no routes; - // a rlp can return no rules if all its limits fail to match any route rule - // or targeting a gateway with no "free" routes. "free" meaning no route with policies targeting it - return nil, 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 - gwHostnames := kuadrantgatewayapi.GatewayHostnames(gw) - if len(gwHostnames) == 0 { - gwHostnames = []gatewayapiv1.Hostname{"*"} - } - hostnames := kuadrantgatewayapi.FilterValidSubdomains(gwHostnames, route.Spec.Hostnames) - if len(hostnames) == 0 { // it should only happen when the route specifies no hostnames - hostnames = gwHostnames - } - - // - // The route selectors logic rely on the "hostnames" field of the route object. - // However, routes effective hostname can be inherited from parent gateway, - // hence it depends on the context as multiple gateways can be targeted by a route - // The route selectors logic needs to be refactored - // or just deleted as soon as the HTTPRoute has name in the route object - // - routeWithEffectiveHostnames := route.DeepCopy() - routeWithEffectiveHostnames.Spec.Hostnames = hostnames - - rules := Rules(rlp, routeWithEffectiveHostnames) - 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, nil - } - - return &Policy{ - Name: client.ObjectKeyFromObject(rlp).String(), - Hostnames: utils.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive - Rules: rules, - }, nil -} - -func ConfigForGateway( - ctx context.Context, gw *gatewayapiv1.Gateway, - topology *kuadrantgatewayapi.Topology) (*Config, error) { - logger, err := logr.FromContext(ctx) - if err != nil { - return nil, err - } - - topologyIndex := kuadrantgatewayapi.NewTopologyIndexes(topology) - - rateLimitPolicies := topologyIndex.PoliciesFromGateway(gw) - logger.V(1).Info("WasmConfig", "#RLPS", len(rateLimitPolicies)) - - // Sort RLPs for consistent comparison with existing objects - sort.Sort(kuadrantgatewayapi.PolicyByTargetRefKindAndCreationTimeStamp(rateLimitPolicies)) - - config := &Config{ - Extensions: map[string]Extension{ - RateLimitPolicyExtensionName: { - Endpoint: common.KuadrantRateLimitClusterName, - FailureMode: FailureModeAllow, - Type: RateLimitExtensionType, - }, - }, - Policies: make([]Policy, 0), - } - - for _, policy := range rateLimitPolicies { - rlp := policy.(*kuadrantv1beta3.RateLimitPolicy) - wasmRLP, err := wasmRateLimitPolicy(ctx, topologyIndex, rlp, gw) - if err != nil { - return nil, err - } - - if wasmRLP == nil { - // skip this RLP - continue - } - - config.Policies = append(config.Policies, *wasmRLP) - } - - return config, nil -} diff --git a/pkg/rlptools/wasm/utils_test.go b/pkg/rlptools/wasm/utils_test.go deleted file mode 100644 index 3ed026a9f..000000000 --- a/pkg/rlptools/wasm/utils_test.go +++ /dev/null @@ -1,528 +0,0 @@ -//go:build unit - -package wasm - -import ( - "regexp" - "testing" - - "github.com/google/go-cmp/cmp" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" -) - -// TODO(eastizle): missing WASMPluginMutator tests -// TODO(eastizle): missing TestWasmRules use cases tests. Only happy path -func TestRules(t *testing.T) { - httpRoute := &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: []gatewayapiv1.Hostname{ - "*.example.com", - "*.apps.example.internal", - }, - Rules: []gatewayapiv1.HTTPRouteRule{ - { - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: &[]gatewayapiv1.PathMatchType{gatewayapiv1.PathMatchPathPrefix}[0], - Value: &[]string{"/toy"}[0], - }, - Method: &[]gatewayapiv1.HTTPMethod{"GET"}[0], - }, - }, - }, - }, - }, - } - - catchAllHTTPRoute := &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: []gatewayapiv1.Hostname{"*"}, - }, - } - - rlp := func(name string, limits map[string]kuadrantv1beta3.Limit) *kuadrantv1beta3.RateLimitPolicy { - return &kuadrantv1beta3.RateLimitPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "my-app", - }, - Spec: kuadrantv1beta3.RateLimitPolicySpec{ - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: limits, - }, - }, - } - } - - // a simple 50rps counter, for convinience, to be used in tests - counter50rps := kuadrantv1beta3.Rate{ - Limit: 50, - Duration: 1, - Unit: kuadrantv1beta3.TimeUnit("second"), - } - - testCases := []struct { - name string - rlp *kuadrantv1beta3.RateLimitPolicy - route *gatewayapiv1.HTTPRoute - expectedRules []Rule - }{ - { - name: "minimal RLP", - rlp: rlp("minimal", map[string]kuadrantv1beta3.Limit{ - "50rps": { - Rates: []kuadrantv1beta3.Rate{counter50rps}, - }, - }), - route: httpRoute, - expectedRules: []Rule{ - { - Conditions: []Condition{ - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, - }, - Actions: []Action{ - { - Scope: "my-app/minimal", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "limit.50rps__36e9aa4c", - Value: "1", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "HTTPRouteRules without rule matches", - rlp: rlp("my-rlp", map[string]kuadrantv1beta3.Limit{ - "50rps": { - Rates: []kuadrantv1beta3.Rate{counter50rps}, - }, - }), - route: catchAllHTTPRoute, - expectedRules: []Rule{ - { - Conditions: nil, - Actions: []Action{ - { - Scope: "my-app/my-rlp", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "limit.50rps__783b9343", - Value: "1", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "RLP with counter qualifier", - rlp: rlp("my-rlp", map[string]kuadrantv1beta3.Limit{ - "50rps-per-username": { - Rates: []kuadrantv1beta3.Rate{counter50rps}, - Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, - }, - }), - route: catchAllHTTPRoute, - expectedRules: []Rule{ - { - Conditions: nil, - Actions: []Action{ - { - Scope: "my-app/my-rlp", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "limit.50rps_per_username__d681f6c3", - Value: "1", - }, - }, - }, - { - Value: &Selector{ - Selector: SelectorSpec{ - Selector: "auth.identity.username", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Route with header match", - rlp: rlp("my-rlp", map[string]kuadrantv1beta3.Limit{ - "50rps": { - Rates: []kuadrantv1beta3.Rate{counter50rps}, - }, - }), - route: &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: []gatewayapiv1.Hostname{"*.example.com"}, - Rules: []gatewayapiv1.HTTPRouteRule{ - { - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchPathPrefix), - Value: ptr.To("/v1"), - }, - Method: ptr.To(gatewayapiv1.HTTPMethodGet), - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Name: gatewayapiv1.HTTPHeaderName("X-kuadrant-a"), - Value: "1", - }, - { - Name: gatewayapiv1.HTTPHeaderName("X-kuadrant-b"), - Value: "1", - }, - }, - }, - }, - }, - }, - }, - }, - expectedRules: []Rule{ - { - Conditions: []Condition{ - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/v1", - }, - { - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - { - Selector: "request.headers.X-kuadrant-a", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "1", - }, - { - Selector: "request.headers.X-kuadrant-b", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "1", - }, - }, - }, - }, - Actions: []Action{ - { - Scope: "my-app/my-rlp", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "limit.50rps__783b9343", - Value: "1", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "RLP with when and counter attributes", - rlp: rlp("my-rlp", map[string]kuadrantv1beta3.Limit{ - "users": { - Rates: []kuadrantv1beta3.Rate{counter50rps}, - Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, - When: []kuadrantv1beta3.WhenCondition{ - { - Selector: kuadrantv1beta3.ContextSelector("auth.identity.group"), - Operator: kuadrantv1beta3.NotEqualOperator, - Value: "admin", - }, - }, - }, - "all": { - Rates: []kuadrantv1beta3.Rate{counter50rps}, - }, - }), - route: &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: []gatewayapiv1.Hostname{"api.toystore.com"}, - // 2 rules - Rules: []gatewayapiv1.HTTPRouteRule{ - { // Toys rule (think about routing to toystore backend) - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchPathPrefix), - Value: ptr.To("/toys"), - }, - Method: ptr.To(gatewayapiv1.HTTPMethodGet), - }, - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchPathPrefix), - Value: ptr.To("/toys"), - }, - Method: ptr.To(gatewayapiv1.HTTPMethodPost), - }, - }, - }, - { // Assets rule (think about routing to assets backend) - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchPathPrefix), - Value: ptr.To("/assets"), - }, - }, - }, - }, - }, - }, - }, - expectedRules: []Rule{ - { // rule associated to "all" limit - Conditions: []Condition{ - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", - }, - { - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", - }, - { - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "POST", - }, - }, - }, - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/assets", - }, - }, - }, - }, - Actions: []Action{ - { - Scope: "my-app/my-rlp", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "limit.all__1edae8a9", - Value: "1", - }, - }, - }, - }, - }, - }, - }, - { // rule associated to "users" limit - Conditions: []Condition{ - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", - }, - { - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - { - Selector: "auth.identity.group", - Operator: PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "admin", - }, - }, - }, - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", - }, - { - Selector: "request.method", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "POST", - }, - { - Selector: "auth.identity.group", - Operator: PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "admin", - }, - }, - }, - { - AllOf: []PatternExpression{ - { - Selector: "request.url_path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/assets", - }, - { - Selector: "auth.identity.group", - Operator: PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "admin", - }, - }, - }, - }, - Actions: []Action{ - { - Scope: "my-app/my-rlp", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "limit.users__6231d900", - Value: "1", - }, - }, - }, - { - Value: &Selector{ - Selector: SelectorSpec{ - Selector: "auth.identity.username", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - computedRules := Rules(tc.rlp, tc.route) - if diff := cmp.Diff(tc.expectedRules, computedRules); diff != "" { - t.Errorf("unexpected wasm rules (-want +got):\n%s", diff) - } - }) - } -} - -func TestLimitNameToLimitadorIdentifier(t *testing.T) { - testCases := []struct { - name string - rlpKey types.NamespacedName - uniqueLimitName string - expected *regexp.Regexp - }{ - { - name: "prepends the limitador limit identifier prefix", - rlpKey: types.NamespacedName{Namespace: "testNS", Name: "rlpA"}, - uniqueLimitName: "foo", - expected: regexp.MustCompile(`^limit\.foo.+`), - }, - { - name: "sanitizes invalid chars", - rlpKey: types.NamespacedName{Namespace: "testNS", Name: "rlpA"}, - uniqueLimitName: "my/limit-0", - expected: regexp.MustCompile(`^limit\.my_limit_0.+$`), - }, - { - name: "sanitizes the dot char (.) even though it is a valid char in limitador identifiers", - rlpKey: types.NamespacedName{Namespace: "testNS", Name: "rlpA"}, - uniqueLimitName: "my.limit", - expected: regexp.MustCompile(`^limit\.my_limit.+$`), - }, - { - name: "appends a hash of the original name to avoid breaking uniqueness", - rlpKey: types.NamespacedName{Namespace: "testNS", Name: "rlpA"}, - uniqueLimitName: "foo", - expected: regexp.MustCompile(`^.+__1da6e70a$`), - }, - { - name: "different rlp keys result in different identifiers", - rlpKey: types.NamespacedName{Namespace: "testNS", Name: "rlpB"}, - uniqueLimitName: "foo", - expected: regexp.MustCompile(`^.+__2c1520b6$`), - }, - { - name: "empty string", - rlpKey: types.NamespacedName{Namespace: "testNS", Name: "rlpA"}, - uniqueLimitName: "", - expected: regexp.MustCompile(`^limit.__6d5e49dc$`), - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(subT *testing.T) { - identifier := LimitNameToLimitadorIdentifier(tc.rlpKey, tc.uniqueLimitName) - if !tc.expected.MatchString(identifier) { - subT.Errorf("identifier does not match, expected(%s), got (%s)", tc.expected, identifier) - } - }) - } -} diff --git a/pkg/rlptools/wasm/types.go b/pkg/wasm/types.go similarity index 54% rename from pkg/rlptools/wasm/types.go rename to pkg/wasm/types.go index b3a1bcf87..32d1de1c2 100644 --- a/pkg/rlptools/wasm/types.go +++ b/pkg/wasm/types.go @@ -20,74 +20,127 @@ var ( } ) -type SelectorSpec struct { - // Selector of an attribute from the contextual properties provided by kuadrant - // during request and connection processing - Selector kuadrantv1beta3.ContextSelector `json:"selector"` +type Config struct { + Services map[string]Service `json:"services"` + ActionSets []ActionSet `json:"actionSets"` +} - // If not set it defaults to `selector` field value as the descriptor key. - // +optional - Key *string `json:"key,omitempty"` +func (c *Config) ToStruct() (*_struct.Struct, error) { + configJSON, err := json.Marshal(c) + if err != nil { + return nil, err + } - // An optional value to use if the selector is not found in the context. - // If not set and the selector is not found in the context, then no descriptor is generated. - // +optional - Default *string `json:"default,omitempty"` + configStruct := &_struct.Struct{} + if err := configStruct.UnmarshalJSON(configJSON); err != nil { + return nil, err + } + return configStruct, nil } -type StaticSpec struct { - Value string `json:"value"` - Key string `json:"key"` +func (c *Config) ToJSON() (*apiextensionsv1.JSON, error) { + configJSON, err := json.Marshal(c) + if err != nil { + return nil, err + } + + return &apiextensionsv1.JSON{Raw: configJSON}, nil } -type Static struct { - Static StaticSpec `json:"static"` +func (c *Config) EqualTo(other *Config) bool { + if len(c.Services) != len(other.Services) || len(c.ActionSets) != len(other.ActionSets) { + return false + } + + for key, service := range c.Services { + if otherService, ok := other.Services[key]; !ok || service != otherService { + return false + } + } + + for i := range c.ActionSets { + if !c.ActionSets[i].EqualTo(other.ActionSets[i]) { + return false + } + } + + return true } -type Selector struct { - Selector SelectorSpec `json:"selector"` +type Service struct { + Endpoint string `json:"endpoint"` + Type ServiceType `json:"type"` + FailureMode FailureModeType `json:"failureMode"` + Timeout *string `json:"timeout,omitempty"` } -type DataType struct { - Value interface{} +// +kubebuilder:validation:Enum:=ratelimit;auth +type ServiceType string + +const ( + RateLimitServiceType ServiceType = "ratelimit" + AuthServiceType ServiceType = "auth" +) + +// +kubebuilder:validation:Enum:=deny;allow +type FailureModeType string + +const ( + FailureModeDeny FailureModeType = "deny" + FailureModeAllow FailureModeType = "allow" +) + +type ActionSet struct { + Name string `json:"name"` + + // Conditions that activate the action set + RouteRuleConditions RouteRuleConditions `json:"routeRuleConditions,omitempty"` + + // Actions that will be invoked when the conditions are met + // +optional + Actions []Action `json:"actions,omitempty"` } -func (d *DataType) UnmarshalJSON(data []byte) error { - // Precisely one of "static", "selector" must be set. - types := []interface{}{ - &Static{}, - &Selector{}, +func (s *ActionSet) EqualTo(other ActionSet) bool { + if s.Name != other.Name || !s.RouteRuleConditions.EqualTo(other.RouteRuleConditions) || len(s.Actions) != len(other.Actions) { + return false } - var err error - - for idx := range types { - dec := json.NewDecoder(bytes.NewReader(data)) - dec.DisallowUnknownFields() // Force errors - err = dec.Decode(types[idx]) - if err == nil { - d.Value = types[idx] - return nil + for i := range s.Actions { + if !s.Actions[i].EqualTo(other.Actions[i]) { + return false } } - return err + return true } -func (d *DataType) MarshalJSON() ([]byte, error) { - switch val := d.Value.(type) { - case *Static: - return json.Marshal(val) - case *Selector: - return json.Marshal(val) - default: - return nil, errors.New("DataType.Value has unknown type") - } +type RouteRuleConditions struct { + Hostnames []string `json:"hostnames"` + Matches []Predicate `json:"matches,omitempty"` } -type PatternOperator kuadrantv1beta3.WhenConditionOperator +func (r *RouteRuleConditions) EqualTo(other RouteRuleConditions) bool { + if len(r.Hostnames) != len(other.Hostnames) || len(r.Matches) != len(other.Matches) { + return false + } + + for i := range r.Hostnames { + if r.Hostnames[i] != other.Hostnames[i] { + return false + } + } -type PatternExpression struct { + for i := range r.Matches { + if !r.Matches[i].EqualTo(other.Matches[i]) { + return false + } + } + + return true +} + +type Predicate struct { // Selector of an attribute from the contextual properties provided by kuadrant // during request and connection processing Selector kuadrantv1beta3.ContextSelector `json:"selector"` @@ -101,119 +154,119 @@ type PatternExpression struct { Value string `json:"value"` } -type Condition struct { - // All the expressions defined must match to match this rule - // +optional - AllOf []PatternExpression `json:"allOf,omitempty"` -} - -type Rule struct { - // Top level conditions for the rule. At least one of the conditions must be met. - // Empty conditions evaluate to true, so actions will be invoked. - // +optional - Conditions []Condition `json:"conditions,omitempty"` - - // Actions defines which extensions will be invoked when any of the top level conditions match. - Actions []Action `json:"actions"` +func (p *Predicate) EqualTo(other Predicate) bool { + return p.Selector == other.Selector && + p.Operator == other.Operator && + p.Value == other.Value } -type Policy struct { - Name string `json:"name"` - Hostnames []string `json:"hostnames"` - - // Rules includes top level conditions and actions to be invoked - // +optional - Rules []Rule `json:"rules,omitempty"` -} +type PatternOperator kuadrantv1beta3.WhenConditionOperator type Action struct { - Scope string `json:"scope"` - ExtensionName string `json:"extension"` + ServiceName string `json:"service"` + Scope string `json:"scope"` + + // Conditions that activate the action + Conditions []Predicate `json:"conditions,omitempty"` + // Data to be sent to the service // +optional Data []DataType `json:"data,omitempty"` } -// +kubebuilder:validation:Enum:=ratelimit;auth -type ExtensionType string - -const ( - RateLimitExtensionType ExtensionType = "ratelimit" - AuthExtensionType ExtensionType = "auth" -) - -// +kubebuilder:validation:Enum:=deny;allow -type FailureModeType string +func (a *Action) EqualTo(other Action) bool { + if a.Scope != other.Scope || a.ServiceName != other.ServiceName || len(a.Conditions) != len(other.Conditions) || len(a.Data) != len(other.Data) { + return false + } -const ( - FailureModeDeny FailureModeType = "deny" - FailureModeAllow FailureModeType = "allow" -) + for i := range a.Conditions { + if !a.Conditions[i].EqualTo(other.Conditions[i]) { + return false + } + } -type Extension struct { - Endpoint string `json:"endpoint"` - FailureMode FailureModeType `json:"failureMode"` - Type ExtensionType `json:"type"` -} + for i := range a.Data { + if !a.Data[i].EqualTo(other.Data[i]) { + return false + } + } -type LimitadorExtension struct { - Endpoint string `json:"endpoint"` + return true } -type Config struct { - Extensions map[string]Extension `json:"extensions"` - Policies []Policy `json:"policies"` +type DataType struct { + Value interface{} } -func (w *Config) ToStruct() (*_struct.Struct, error) { - configJSON, err := json.Marshal(w) - if err != nil { - return nil, err +func (d *DataType) UnmarshalJSON(data []byte) error { + // Precisely one of "static", "selector" must be set. + types := []interface{}{ + &Static{}, + &Selector{}, } - configStruct := &_struct.Struct{} - if err := configStruct.UnmarshalJSON(configJSON); err != nil { - return nil, err - } - return configStruct, nil -} + var err error -func (w *Config) ToJSON() (*apiextensionsv1.JSON, error) { - configJSON, err := json.Marshal(w) - if err != nil { - return nil, err + for idx := range types { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() // Force errors + err = dec.Decode(types[idx]) + if err == nil { + d.Value = types[idx] + return nil + } } - return &apiextensionsv1.JSON{Raw: configJSON}, nil + return err } -func ConfigFromStruct(structure *_struct.Struct) (*Config, error) { - if structure == nil { - return nil, errors.New("cannot desestructure config from nil") +func (d *DataType) MarshalJSON() ([]byte, error) { + switch val := d.Value.(type) { + case *Static: + return json.Marshal(val) + case *Selector: + return json.Marshal(val) + default: + return nil, errors.New("DataType.Value has unknown type") } - // Serialize struct into json - configJSON, err := structure.MarshalJSON() +} + +func (d *DataType) EqualTo(other DataType) bool { + dt, err := d.MarshalJSON() if err != nil { - return nil, err + return false } - // Deserialize protobuf struct into Config struct - config := &Config{} - if err := json.Unmarshal(configJSON, config); err != nil { - return nil, err + odt, err := other.MarshalJSON() + if err != nil { + return false } + return bytes.Equal(dt, odt) +} + +type Static struct { + Static StaticSpec `json:"static"` +} - return config, nil +type Selector struct { + Selector SelectorSpec `json:"selector"` } -func ConfigFromJSON(configJSON *apiextensionsv1.JSON) (*Config, error) { - if configJSON == nil { - return nil, errors.New("cannot desestructure config from nil") - } +type StaticSpec struct { + Value string `json:"value"` + Key string `json:"key"` +} - config := &Config{} - if err := json.Unmarshal(configJSON.Raw, config); err != nil { - return nil, err - } +type SelectorSpec struct { + // Selector of an attribute from the contextual properties provided by kuadrant + // during request and connection processing + Selector kuadrantv1beta3.ContextSelector `json:"selector"` + + // If not set it defaults to `selector` field value as the descriptor key. + // +optional + Key *string `json:"key,omitempty"` - return config, nil + // An optional value to use if the selector is not found in the context. + // If not set and the selector is not found in the context, then no descriptor is generated. + // +optional + Default *string `json:"default,omitempty"` } diff --git a/pkg/rlptools/wasm/types_test.go b/pkg/wasm/types_test.go similarity index 64% rename from pkg/rlptools/wasm/types_test.go rename to pkg/wasm/types_test.go index 0a589734c..5a796e876 100644 --- a/pkg/rlptools/wasm/types_test.go +++ b/pkg/wasm/types_test.go @@ -24,31 +24,34 @@ func TestConfig(t *testing.T) { name: "basic example", expectedConfig: testBasicConfigExample(), yaml: ` -extensions: - limitador: +services: + ratelimit-service: type: ratelimit endpoint: kuadrant-rate-limiting-service failureMode: allow -policies: +actionSets: - name: rlp-ns-A/rlp-name-A - hostnames: - - '*.toystore.com' - - example.com - rules: - - conditions: - - allOf: - - selector: request.host - operator: eq - value: cars.toystore.com - actions: - - extension: limitador - scope: rlp-ns-A/rlp-name-A - data: - - static: - key: rlp-ns-A/rlp-name-A - value: "1" - - selector: - selector: auth.metadata.username + routeRuleConditions: + hostnames: + - '*.toystore.com' + - example.com + matches: + - selector: request.path + operator: startswith + value: /cars + actions: + - service: ratelimit-service + scope: rlp-ns-A/rlp-name-A + conditions: + - selector: source.ip + operator: neq + value: 127.0.0.1 + data: + - static: + key: rlp-ns-A/rlp-name-A + value: "1" + - selector: + selector: auth.metadata.username `, }, } @@ -97,13 +100,13 @@ func TestValidActionConfig(t *testing.T) { { name: "valid empty data", expectedAction: &Action{ - Scope: "some-scope", - ExtensionName: "limitador", - Data: nil, + ServiceName: "ratelimit-service", + Scope: "some-scope", + Data: nil, }, yaml: ` +service: ratelimit-service scope: some-scope -extension: limitador `, }, } @@ -130,20 +133,20 @@ func TestInValidActionConfig(t *testing.T) { { name: "unknown data type", yaml: ` +service: ratelimit-service scope: some-scope -extension: limitador data: -- other: +- other: key: keyA `, }, { name: "both data types at the same time", yaml: ` +service: ratelimit-service scope: some-scope -extension: limitador data: -- static: +- static: key: keyA selector: selector: selectorA @@ -163,52 +166,53 @@ data: func testBasicConfigExample() *Config { return &Config{ - Extensions: map[string]Extension{ - RateLimitPolicyExtensionName: { + Services: map[string]Service{ + RateLimitServiceName: { + Type: RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, FailureMode: FailureModeAllow, - Type: RateLimitExtensionType, }, }, - Policies: []Policy{ + ActionSets: []ActionSet{ { Name: "rlp-ns-A/rlp-name-A", - Hostnames: []string{ - "*.toystore.com", - "example.com", + RouteRuleConditions: RouteRuleConditions{ + Hostnames: []string{ + "*.toystore.com", + "example.com", + }, + Matches: []Predicate{ + { + Selector: "request.path", + Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/cars", + }, + }, }, - Rules: []Rule{ + Actions: []Action{ { - Conditions: []Condition{ + ServiceName: RateLimitServiceName, + Scope: "rlp-ns-A/rlp-name-A", + Conditions: []Predicate{ { - AllOf: []PatternExpression{ - { - Selector: "request.host", - Operator: PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "cars.toystore.com", - }, - }, + Selector: "source.ip", + Operator: PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "127.0.0.1", }, }, - Actions: []Action{ + Data: []DataType{ { - Scope: "rlp-ns-A/rlp-name-A", - ExtensionName: RateLimitPolicyExtensionName, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "rlp-ns-A/rlp-name-A", - Value: "1", - }, - }, + Value: &Static{ + Static: StaticSpec{ + Key: "rlp-ns-A/rlp-name-A", + Value: "1", }, - { - Value: &Selector{ - Selector: SelectorSpec{ - Selector: "auth.metadata.username", - }, - }, + }, + }, + { + Value: &Selector{ + Selector: SelectorSpec{ + Selector: "auth.metadata.username", }, }, }, diff --git a/pkg/wasm/utils.go b/pkg/wasm/utils.go new file mode 100644 index 000000000..462f7bdd5 --- /dev/null +++ b/pkg/wasm/utils.go @@ -0,0 +1,202 @@ +package wasm + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + _struct "google.golang.org/protobuf/types/known/structpb" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" +) + +const ( + RateLimitServiceName = "ratelimit-service" + AuthServiceName = "auth-service" +) + +func ExtensionName(gatewayName string) string { + return fmt.Sprintf("kuadrant-%s", gatewayName) +} + +func BuildConfigForActionSet(actionSets []ActionSet) Config { + return Config{ + Services: map[string]Service{ + RateLimitServiceName: { + Type: RateLimitServiceType, + Endpoint: common.KuadrantRateLimitClusterName, + FailureMode: FailureModeAllow, + }, + // TODO: add auth extension + }, + ActionSets: actionSets, + } +} + +type ActionBuilderFunc func(uniquePolicyRuleKey string, policyRule kuadrantv1.MergeableRule) (Action, error) + +func BuildActionSetsForPath(pathID string, path []machinery.Targetable, policyRules map[string]kuadrantv1.MergeableRule, actionBuilder ActionBuilderFunc) ([]kuadrantgatewayapi.HTTPRouteMatchConfig, error) { + _, _, listener, httpRoute, httpRouteRule, err := common.ObjectsInRequestPath(path) + if err != nil { + return nil, err + } + + actions := lo.FilterMap(lo.Entries(policyRules), func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) (Action, bool) { + action, err := actionBuilder(r.Key, r.Value) + if err != nil { + errors.Join(err) + return Action{}, false + } + return action, true + }) + + return lo.FlatMap(kuadrantgatewayapi.HostnamesFromListenerAndHTTPRoute(listener.Listener, httpRoute.HTTPRoute), func(hostname gatewayapiv1.Hostname, _ int) []kuadrantgatewayapi.HTTPRouteMatchConfig { + return lo.Map(httpRouteRule.Matches, func(httpRouteMatch gatewayapiv1.HTTPRouteMatch, j int) kuadrantgatewayapi.HTTPRouteMatchConfig { + actionSet := ActionSet{ + Name: ActionSetNameForPath(pathID, j, string(hostname)), + Actions: actions, + } + routeRuleConditions := RouteRuleConditions{ + Hostnames: []string{string(hostname)}, + } + if predicates := PredicatesFromHTTPRouteMatch(httpRouteMatch); len(predicates) > 0 { + routeRuleConditions.Matches = predicates + } + actionSet.RouteRuleConditions = routeRuleConditions + return kuadrantgatewayapi.HTTPRouteMatchConfig{ + Hostname: string(hostname), + HTTPRouteMatch: httpRouteMatch, + CreationTimestamp: httpRoute.GetCreationTimestamp(), + Namespace: httpRoute.GetNamespace(), + Name: httpRoute.GetName(), + Config: actionSet, + } + }) + }), err +} + +func ActionSetNameForPath(pathID string, httpRouteMatchIndex int, hostname string) string { + source := fmt.Sprintf("%s|%d|%s", pathID, httpRouteMatchIndex+1, hostname) + hash := sha256.Sum256([]byte(source)) + return hex.EncodeToString(hash[:]) +} + +func ConfigFromStruct(structure *_struct.Struct) (*Config, error) { + if structure == nil { + return nil, errors.New("cannot desestructure config from nil") + } + // Serialize struct into json + configJSON, err := structure.MarshalJSON() + if err != nil { + return nil, err + } + // Deserialize protobuf struct into Config struct + config := &Config{} + if err := json.Unmarshal(configJSON, config); err != nil { + return nil, err + } + + return config, nil +} + +func ConfigFromJSON(configJSON *apiextensionsv1.JSON) (*Config, error) { + if configJSON == nil { + return nil, errors.New("cannot desestructure config from nil") + } + + config := &Config{} + if err := json.Unmarshal(configJSON.Raw, config); err != nil { + return nil, err + } + + return config, nil +} + +// PredicatesFromWhenConditions builds a list of predicates from a list of (selector, operator, value) when conditions +func PredicatesFromWhenConditions(when ...kuadrantv1beta3.WhenCondition) []Predicate { + return lo.Map(when, func(when kuadrantv1beta3.WhenCondition, _ int) Predicate { + return Predicate{ + Selector: when.Selector, + Operator: PatternOperator(when.Operator), + Value: when.Value, + } + }) +} + +// PredicatesFromHTTPRouteMatch builds a list of conditions from a rule match +func PredicatesFromHTTPRouteMatch(match gatewayapiv1.HTTPRouteMatch) []Predicate { + predicates := make([]Predicate, 0) + + // method + if match.Method != nil { + predicates = append(predicates, predicateFromMethod(*match.Method)) + } + + // path + if match.Path != nil { + predicates = append(predicates, predicateFromPathMatch(*match.Path)) + } + + // headers + for _, headerMatch := range match.Headers { + // Multiple match values are ANDed together + predicates = append(predicates, predicateFromHeader(headerMatch)) + } + + // TODO(eguzki): query params. Investigate integration with wasm regarding Envoy params + // from https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes + // request.query -> string : The query portion of the URL in the format of “name1=value1&name2=value2”. + + return predicates +} + +func predicateFromPathMatch(pathMatch gatewayapiv1.HTTPPathMatch) Predicate { + var ( + operator = PatternOperator(kuadrantv1beta3.StartsWithOperator) // default value + value = "/" // default value + ) + + if pathMatch.Value != nil { + value = *pathMatch.Value + } + + if pathMatch.Type != nil { + if val, ok := PathMatchTypeMap[*pathMatch.Type]; ok { + operator = val + } + } + + return Predicate{ + Selector: "request.url_path", + Operator: operator, + Value: value, + } +} + +func predicateFromMethod(method gatewayapiv1.HTTPMethod) Predicate { + return Predicate{ + Selector: "request.method", + Operator: PatternOperator(kuadrantv1beta3.EqualOperator), + Value: string(method), + } +} + +func predicateFromHeader(headerMatch gatewayapiv1.HTTPHeaderMatch) Predicate { + // As for gateway api v1, the only operation type with core support is Exact match. + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderMatch + + return Predicate{ + Selector: kuadrantv1beta3.ContextSelector(fmt.Sprintf("request.headers.%s", headerMatch.Name)), + Operator: PatternOperator(kuadrantv1beta3.EqualOperator), + Value: headerMatch.Value, + } +} diff --git a/tests/common/ratelimitpolicy/ratelimitpolicy_controller_test.go b/tests/common/ratelimitpolicy/ratelimitpolicy_controller_test.go index 4ffb86ce9..60cfe928a 100644 --- a/tests/common/ratelimitpolicy/ratelimitpolicy_controller_test.go +++ b/tests/common/ratelimitpolicy/ratelimitpolicy_controller_test.go @@ -4,7 +4,6 @@ package ratelimitpolicy import ( "context" - "encoding/json" "fmt" "strings" "time" @@ -12,6 +11,7 @@ import ( limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/samber/lo" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,10 +21,9 @@ import ( gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/controllers" "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" "github.com/kuadrant/kuadrant-operator/tests" ) @@ -51,17 +50,21 @@ var _ = Describe("RateLimitPolicy controller (Serial)", Serial, func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - Defaults: &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: "minute", + Defaults: &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: "minute", + }, }, }, }, @@ -127,7 +130,7 @@ var _ = Describe("RateLimitPolicy controller (Serial)", Serial, func() { Expect(k8sClient.Delete(ctx, &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: limitadorDeploymentName, Namespace: kuadrantInstallationNS}})).To(Succeed()) Eventually(assertAcceptedCondTrueAndEnforcedCond(ctx, policy, metav1.ConditionFalse, string(kuadrant.PolicyReasonUnknown), - "RateLimitPolicy has encountered some issues: limitador is not ready")).WithContext(ctx).Should(Succeed()) + "RateLimitPolicy waiting for the following components to sync: [Limitador]")).WithContext(ctx).Should(Succeed()) }, testTimeOut) It("Unknown Reason", func(ctx SpecContext) { @@ -138,7 +141,7 @@ var _ = Describe("RateLimitPolicy controller (Serial)", Serial, func() { policy := policyFactory() Expect(k8sClient.Create(ctx, policy)).To(Succeed()) Eventually(assertAcceptedCondTrueAndEnforcedCond(ctx, policy, metav1.ConditionFalse, string(kuadrant.PolicyReasonUnknown), - "RateLimitPolicy has encountered some issues: limitador is not ready")).WithContext(ctx).Should(Succeed()) + "RateLimitPolicy waiting for the following components to sync: [Limitador]")).WithContext(ctx).Should(Succeed()) // Enforced true once limitador is ready Eventually(assertAcceptedCondTrueAndEnforcedCond(ctx, policy, metav1.ConditionTrue, string(kuadrant.PolicyReasonEnforced), @@ -170,17 +173,21 @@ var _ = Describe("RateLimitPolicy controller", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - Defaults: &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: "minute", + Defaults: &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: "minute", + }, }, }, }, @@ -249,17 +256,6 @@ var _ = Describe("RateLimitPolicy controller", func() { rlpKey := client.ObjectKeyFromObject(rlp) Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - // Check HTTPRoute direct back reference - routeKey := client.ObjectKey{Name: routeName, Namespace: testNamespace} - existingRoute := &gatewayapiv1.HTTPRoute{} - Eventually(func(g Gomega) { - err = k8sClient.Get(ctx, routeKey, existingRoute) - // must exist - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingRoute.GetAnnotations()).To(HaveKeyWithValue( - rlp.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(rlp).String())) - }).WithContext(ctx).Should(Succeed()) - // check limits Eventually(func(g Gomega) { limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: kuadrantInstallationNS} @@ -270,26 +266,11 @@ var _ = Describe("RateLimitPolicy controller", func() { Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{ MaxValue: 1, Seconds: 3 * 60, - Namespace: wasm.LimitsNamespaceFromRLP(rlp), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"))}, + Namespace: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlp), })) }).WithContext(ctx).Should(Succeed()) - - // Check gateway back references - gwKey := client.ObjectKey{Name: TestGatewayName, Namespace: testNamespace} - existingGateway := &gatewayapiv1.Gateway{} - Eventually(func(g Gomega) { - err = k8sClient.Get(ctx, gwKey, existingGateway) - // must exist - Expect(err).ToNot(HaveOccurred()) - refs := []client.ObjectKey{rlpKey} - serialized, err := json.Marshal(refs) - Expect(err).ToNot(HaveOccurred()) - Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - rlp.BackReferenceAnnotationName(), string(serialized))) - }).WithContext(ctx).Should(Succeed()) }, testTimeOut) }) @@ -312,17 +293,21 @@ var _ = Describe("RateLimitPolicy controller", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.Group("gateway.networking.k8s.io"), - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.Group("gateway.networking.k8s.io"), + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - Defaults: &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: "minute", + Defaults: &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: "minute", + }, }, }, }, @@ -337,15 +322,11 @@ var _ = Describe("RateLimitPolicy controller", func() { rlpKey := client.ObjectKey{Name: rlpName, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - // Check Gateway direct back reference gwKey := client.ObjectKeyFromObject(gateway) existingGateway := &gatewayapiv1.Gateway{} Eventually(func(g Gomega) { err = k8sClient.Get(ctx, gwKey, existingGateway) - // must exist g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - rlp.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(rlp).String())) }).WithContext(ctx).Should(Succeed()) // check limits @@ -358,23 +339,11 @@ var _ = Describe("RateLimitPolicy controller", func() { Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{ MaxValue: 1, Seconds: 3 * 60, - Namespace: wasm.LimitsNamespaceFromRLP(rlp), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"))}, + Namespace: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlp), })) }).WithContext(ctx).Should(Succeed()) - - Eventually(func(g Gomega) { - // Check gateway back references - err = k8sClient.Get(ctx, gwKey, existingGateway) - // must exist - g.Expect(err).ToNot(HaveOccurred()) - refs := []client.ObjectKey{rlpKey} - serialized, err := json.Marshal(refs) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(rlp.BackReferenceAnnotationName(), string(serialized))) - }).WithContext(ctx).Should(Succeed()) }, testTimeOut) It("Creates all the resources for a basic Gateway and RateLimitPolicy when missing a HTTPRoute attached to the Gateway", func(ctx SpecContext) { @@ -389,16 +358,7 @@ var _ = Describe("RateLimitPolicy controller", func() { // Check RLP status is available rlpKey := client.ObjectKey{Name: rlpName, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) - - // Check Gateway direct back reference - gwKey := client.ObjectKeyFromObject(gateway) - existingGateway := &gatewayapiv1.Gateway{} - err = k8sClient.Get(ctx, gwKey, existingGateway) - // must exist - Expect(err).ToNot(HaveOccurred()) - Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - rlp.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(rlp).String())) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) // check limits Eventually(func(g Gomega) { @@ -407,25 +367,9 @@ var _ = Describe("RateLimitPolicy controller", func() { err = k8sClient.Get(ctx, limitadorKey, existingLimitador) // must exist Expect(err).ToNot(HaveOccurred()) - Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{ - MaxValue: 1, - Seconds: 3 * 60, - Namespace: wasm.LimitsNamespaceFromRLP(rlp), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"))}, - Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlp), - })) - }).WithContext(ctx).Should(Succeed()) - - Eventually(func(g Gomega) { - // Check gateway back references - err = k8sClient.Get(ctx, gwKey, existingGateway) - // must exist - g.Expect(err).ToNot(HaveOccurred()) - refs := []client.ObjectKey{rlpKey} - serialized, err := json.Marshal(refs) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(rlp.BackReferenceAnnotationName(), string(serialized))) + Expect(lo.Filter(existingLimitador.Spec.Limits, func(l limitadorv1alpha1.RateLimit, _ int) bool { // a hack to isolate test namespaces sharing the same limitador cr + return strings.HasPrefix(l.Namespace, fmt.Sprintf("%s/", testNamespace)) + })).To(BeEmpty()) }).WithContext(ctx).Should(Succeed()) }, testTimeOut) }) @@ -458,7 +402,7 @@ var _ = Describe("RateLimitPolicy controller", func() { // Create HTTPRoute RLP with new default limits routeRLP = policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "httproute-rlp" - policy.Spec.CommonSpec().Limits = map[string]kuadrantv1beta3.Limit{ + policy.Spec.Proper().Limits = map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ { @@ -473,33 +417,14 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(assertPolicyIsAcceptedAndEnforced(ctx, routeRLPKey)).WithContext(ctx).Should(BeTrue()) Eventually(tests.RLPIsEnforced(ctx, testClient(), gwRLPKey)).WithContext(ctx).Should(BeFalse()) - // Check Gateway direct back reference - gwKey := client.ObjectKeyFromObject(gateway) - existingGateway := &gatewayapiv1.Gateway{} - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - gwRLP.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(gwRLP).String())) - }).WithContext(ctx).Should(Succeed()) - // check limits Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 10, Seconds: 5, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "l1"))}, + Namespace: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(routeRLPKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) - - // Gateway should contain HTTPRoute RLP in backreference - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed()) - serialized, err := json.Marshal(routeRLPKey) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKey(routeRLP.BackReferenceAnnotationName())) - g.Expect(existingGateway.GetAnnotations()[routeRLP.BackReferenceAnnotationName()]).To(ContainSubstring(string(serialized))) - }).WithContext(ctx).Should(Succeed()) }) When("Free route is created", func() { @@ -527,31 +452,32 @@ var _ = Describe("RateLimitPolicy controller", func() { Expect(k8sClient.Create(ctx, gwRLP)).To(Succeed()) gwRLPKey := client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, gwRLPKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), gwRLPKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), gwRLPKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) }, testTimeOut) It("Implicit defaults - no underlying routes to enforce policy", func(ctx SpecContext) { gwRLP := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(TestGatewayName) - policy.Spec.RateLimitPolicyCommonSpec = *policy.Spec.Defaults.DeepCopy() + policy.Spec.RateLimitPolicySpecProper = *policy.Spec.Defaults.RateLimitPolicySpecProper.DeepCopy() policy.Spec.Defaults = nil }) Expect(k8sClient.Create(ctx, gwRLP)).To(Succeed()) gwRLPKey := client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, gwRLPKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), gwRLPKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), gwRLPKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) }, testTimeOut) }) Context("RLP Overrides", func() { + var httpRoute *gatewayapiv1.HTTPRoute var gwRLP *kuadrantv1beta3.RateLimitPolicy var routeRLP *kuadrantv1beta3.RateLimitPolicy BeforeEach(func(ctx SpecContext) { // create httproute - httpRoute := tests.BuildBasicHttpRoute(routeName, TestGatewayName, testNamespace, []string{"*.example.com"}) + httpRoute = tests.BuildBasicHttpRoute(routeName, TestGatewayName, testNamespace, []string{"*.example.com"}) Expect(k8sClient.Create(ctx, httpRoute)).To(Succeed()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) @@ -564,7 +490,7 @@ var _ = Describe("RateLimitPolicy controller", func() { routeRLP = policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "httproute-rlp" - policy.Spec.CommonSpec().Limits = map[string]kuadrantv1beta3.Limit{ + policy.Spec.Proper().Limits = map[string]kuadrantv1beta3.Limit{ "route": { Rates: []kuadrantv1beta3.Rate{ { @@ -588,34 +514,17 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, routeRLPKey)).WithContext(ctx).Should(BeTrue()) Expect(tests.RLPEnforcedCondition(ctx, testClient(), routeRLPKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("RateLimitPolicy is overridden by [%s]", gwRLPKey))) - // Check Gateway direct back reference - gwKey := client.ObjectKeyFromObject(gateway) - existingGateway := &gatewayapiv1.Gateway{} - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - gwRLP.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(gwRLP).String())) - }).WithContext(ctx).Should(Succeed()) + limitsNamespace := controllers.LimitsNamespaceFromRoute(httpRoute) // check limits - should contain override values Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 1, Seconds: 180, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "l1"))}, + Namespace: limitsNamespace, + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(gwRLPKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) - // Gateway should contain HTTPRoute RLP in backreference - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed()) - serialized, err := json.Marshal(gwRLPKey) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKey(routeRLP.BackReferenceAnnotationName())) - g.Expect(existingGateway.GetAnnotations()[routeRLP.BackReferenceAnnotationName()]).To(ContainSubstring(string(serialized))) - }).WithContext(ctx).Should(Succeed()) - // Delete GW RLP -> Route RLP should be enforced Expect(k8sClient.Delete(ctx, gwRLP)).To(Succeed()) Eventually(tests.RLPIsEnforced(ctx, testClient(), routeRLPKey)).WithContext(ctx).Should(BeTrue()) @@ -623,10 +532,9 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 10, Seconds: 5, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "route"))}, + Namespace: limitsNamespace, + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(routeRLPKey, "route"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) }, testTimeOut) @@ -645,33 +553,14 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(tests.RLPIsEnforced(ctx, testClient(), routeRLPKey)).WithContext(ctx).Should(BeFalse()) Expect(tests.RLPEnforcedCondition(ctx, testClient(), routeRLPKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("RateLimitPolicy is overridden by [%s]", gwRLPKey))) - // Check Gateway direct back reference - gwKey := client.ObjectKeyFromObject(gateway) - existingGateway := &gatewayapiv1.Gateway{} - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - gwRLP.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(gwRLP).String())) - }).WithContext(ctx).Should(Succeed()) - // Should contain override values Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 1, Seconds: 180, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "l1"))}, + Namespace: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(gwRLPKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) - - // Gateway should contain HTTPRoute RLP in backreference - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed()) - serialized, err := json.Marshal(routeRLPKey) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingGateway.GetAnnotations()).To(HaveKey(routeRLP.BackReferenceAnnotationName())) - g.Expect(existingGateway.GetAnnotations()[routeRLP.BackReferenceAnnotationName()]).To(ContainSubstring(string(serialized))) - }).WithContext(ctx).Should(Succeed()) }, testTimeOut) It("Gateway atomic override - gateway defaults turned into overrides later on", func(ctx SpecContext) { @@ -693,14 +582,15 @@ var _ = Describe("RateLimitPolicy controller", func() { // Route RLP should still be enforced Eventually(tests.RLPIsEnforced(ctx, testClient(), routeRLPKey)).WithContext(ctx).Should(BeTrue()) + limitsNamespace := controllers.LimitsNamespaceFromRoute(httpRoute) + // Should contain Route RLP values Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 10, Seconds: 5, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "route"))}, + Namespace: limitsNamespace, + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(routeRLPKey, "route"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) // Update GW RLP defaults to overrides @@ -720,10 +610,9 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 1, Seconds: 180, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "l1"))}, + Namespace: limitsNamespace, + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(gwRLPKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) }, testTimeOut) @@ -742,14 +631,15 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(tests.RLPIsEnforced(ctx, testClient(), routeRLPKey)).WithContext(ctx).Should(BeFalse()) Expect(tests.RLPEnforcedCondition(ctx, testClient(), routeRLPKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("RateLimitPolicy is overridden by [%s]", gwRLPKey))) + limitsNamespace := controllers.LimitsNamespaceFromRoute(httpRoute) + // Should contain override values Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 1, Seconds: 180, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "l1"))}, + Namespace: limitsNamespace, + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(gwRLPKey, "l1"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) // Update GW RLP overrides to defaults @@ -769,10 +659,9 @@ var _ = Describe("RateLimitPolicy controller", func() { Eventually(limitadorContainsLimit(ctx, limitadorv1alpha1.RateLimit{ MaxValue: 10, Seconds: 5, - Namespace: wasm.LimitsNamespaceFromRLP(routeRLP), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "route"))}, + Namespace: limitsNamespace, + Conditions: []string{fmt.Sprintf(`%s == "1"`, controllers.LimitNameToLimitadorIdentifier(routeRLPKey, "route"))}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(routeRLP), })).WithContext(ctx).Should(Succeed()) }, testTimeOut) @@ -784,7 +673,7 @@ var _ = Describe("RateLimitPolicy controller", func() { Expect(k8sClient.Create(ctx, gwRLP)).To(Succeed()) gwRLPKey := client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, gwRLPKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), gwRLPKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), gwRLPKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) }, testTimeOut) }) @@ -825,7 +714,7 @@ var _ = Describe("RateLimitPolicy controller", func() { ).WithContext(ctx).Should(Succeed()) }, testTimeOut) - It("Conflict reason", func(ctx SpecContext) { + It("Multiple policies can target a same resource", func(ctx SpecContext) { httpRoute := tests.BuildBasicHttpRoute(routeName, TestGatewayName, testNamespace, []string{"*.example.com"}) Expect(k8sClient.Create(ctx, httpRoute)).To(Succeed()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) @@ -841,426 +730,7 @@ var _ = Describe("RateLimitPolicy controller", func() { }) Expect(k8sClient.Create(ctx, rlp2)).To(Succeed()) - Eventually(assertAcceptedConditionFalse(ctx, rlp2, string(gatewayapiv1alpha2.PolicyReasonConflicted), - fmt.Sprintf("RateLimitPolicy is conflicted by %[1]v/toystore-rlp: the gateway.networking.k8s.io/v1, Kind=HTTPRoute target %[1]v/toystore-route is already referenced by policy %[1]v/toystore-rlp", testNamespace)), - ).WithContext(ctx).Should(Succeed()) - }, testTimeOut) - }) - - Context("When RLP switches target from one HTTPRoute to another HTTPRoute", func() { - var ( - routeAName = "route-a" - routeBName = "route-b" - ) - - It("direct references are updated", func(ctx SpecContext) { - // Initial state - // Route A - // RLP A -> Route A - - // Switch target to another route - // Route A - // Route B - // RLP A -> Route B - - // create httproute A - httpRouteA := tests.BuildBasicHttpRoute(routeAName, TestGatewayName, testNamespace, []string{"*.a.example.com"}) - err := k8sClient.Create(ctx, httpRouteA) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRouteA))).WithContext(ctx).Should(BeTrue()) - - // create ratelimitpolicy - rlp := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeAName) - }) - err = k8sClient.Create(ctx, rlp) - Expect(err).ToNot(HaveOccurred()) - - // Check RLP status is available - rlpKey := client.ObjectKeyFromObject(rlp) - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute A direct back reference - routeAKey := client.ObjectKey{Name: routeAName, Namespace: testNamespace} - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeAKey, rlp.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlp).String(), - )).WithContext(ctx).Should(BeTrue()) - - // Proceed with the update: - // From RLP A -> Route A - // To RLP A -> Route B - - // create httproute B - httpRouteB := tests.BuildBasicHttpRoute(routeBName, TestGatewayName, testNamespace, []string{"*.b.example.com"}) - err = k8sClient.Create(ctx, httpRouteB) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRouteB))).WithContext(ctx).Should(BeTrue()) - - Eventually(func(g Gomega) { - rlpUpdated := &kuadrantv1beta3.RateLimitPolicy{} - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(rlp), rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - rlpUpdated.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeBName) - err = k8sClient.Update(ctx, rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - }).WithContext(ctx).Should(Succeed()) - - // Check RLP status is available - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute A direct back reference is gone - Eventually( - tests.HTTPRouteWithoutDirectBackReference(testClient(), routeAKey, rlp.DirectReferenceAnnotationName())).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute B direct back reference - routeBKey := client.ObjectKey{Name: routeBName, Namespace: testNamespace} - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeBKey, rlp.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlp).String(), - )).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("When RLP switches target from one Gateway to another Gateway", func() { - var ( - gwAName = "gw-a" - gwBName = "gw-b" - ) - - It("direct references are updated", func(ctx SpecContext) { - // Initial state - // Gw A - // RLP A -> Gw A - - // Switch target to another gw - // Gw A - // Gw B - // RLP A -> Gw B - - // create Gw A - gatewayA := tests.BuildBasicGateway(gwAName, testNamespace) - err := k8sClient.Create(ctx, gatewayA) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.GatewayIsReady(ctx, testClient(), gatewayA)).WithContext(ctx).Should(BeTrue()) - - // create ratelimitpolicy - rlp := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gwAName) - }) - err = k8sClient.Create(ctx, rlp) - Expect(err).ToNot(HaveOccurred()) - - // Check RLP status is available - rlpKey := client.ObjectKeyFromObject(rlp) - Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) - - // Check Gateway direct back reference - gwAKey := client.ObjectKey{Name: gwAName, Namespace: testNamespace} - Eventually( - tests.GatewayHasDirectBackReference(testClient(), - gwAKey, rlp.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlp).String(), - )).WithContext(ctx).Should(BeTrue()) - - // Proceed with the update: - // From RLP A -> Gw A - // To RLP A -> Gw B - - // create Gw B - gatewayB := tests.BuildBasicGateway(gwBName, testNamespace) - err = k8sClient.Create(ctx, gatewayB) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.GatewayIsReady(ctx, testClient(), gatewayB)).WithContext(ctx).Should(BeTrue()) - - Eventually(func(g Gomega) { - rlpUpdated := &kuadrantv1beta3.RateLimitPolicy{} - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(rlp), rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - rlpUpdated.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gwBName) - err = k8sClient.Update(ctx, rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - }).WithContext(ctx).Should(Succeed()) - - // Check RLP status is available - Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) - - // Check Gw A direct back reference is gone - Eventually( - tests.GatewayWithoutDirectBackReference(testClient(), gwAKey, rlp.DirectReferenceAnnotationName())).WithContext(ctx).Should(BeTrue()) - - // Check Gateway B direct back reference - gwBKey := client.ObjectKey{Name: gwBName, Namespace: testNamespace} - Eventually( - tests.GatewayHasDirectBackReference(testClient(), - gwBKey, rlp.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlp).String(), - )).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("When RLP switches target from one HTTPRoute to another taken HTTPRoute", func() { - var ( - routeAName = "route-a" - routeBName = "route-b" - rlpAName = "rlp-a" - rlpBName = "rlp-b" - ) - - It("direct references are updated", func(ctx SpecContext) { - // Initial state - // Route A - // Route B - // RLP A -> Route A - // RLP B -> Route B - - // Switch target to another route - // Route A - // Route B - // RLP A -> Route B - // RLP B -> Route B - - // create httproute A - httpRouteA := tests.BuildBasicHttpRoute(routeAName, TestGatewayName, testNamespace, []string{"*.a.example.com"}) - err := k8sClient.Create(ctx, httpRouteA) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRouteA))).WithContext(ctx).Should(BeTrue()) - - // create httproute B - httpRouteB := tests.BuildBasicHttpRoute(routeBName, TestGatewayName, testNamespace, []string{"*.b.example.com"}) - err = k8sClient.Create(ctx, httpRouteB) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRouteB))).WithContext(ctx).Should(BeTrue()) - - // create rlpA - rlpA := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.ObjectMeta.Name = rlpAName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeAName) - }) - err = k8sClient.Create(ctx, rlpA) - Expect(err).ToNot(HaveOccurred()) - - rlpAKey := client.ObjectKeyFromObject(rlpA) - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpAKey)).WithContext(ctx).Should(BeTrue()) - - // create rlpB - rlpB := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.ObjectMeta.Name = rlpBName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeBName) - }) - err = k8sClient.Create(ctx, rlpB) - Expect(err).ToNot(HaveOccurred()) - - // Check RLP status is available - rlpBKey := client.ObjectKeyFromObject(rlpB) - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpBKey)).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute A direct back reference - routeAKey := client.ObjectKey{Name: routeAName, Namespace: testNamespace} - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeAKey, rlpA.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlpA).String(), - )).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute B direct back reference - routeBKey := client.ObjectKey{Name: routeBName, Namespace: testNamespace} - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeBKey, rlpB.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlpB).String(), - )).WithContext(ctx).Should(BeTrue()) - - // Proceed with the update: - // From RLP A -> Route A - // To RLP A -> Route B (already taken) - - Eventually(func(g Gomega) { - rlpUpdated := &kuadrantv1beta3.RateLimitPolicy{} - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(rlpA), rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - rlpUpdated.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeBName) - err = k8sClient.Update(ctx, rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - // Check RLP status is available - }).WithContext(ctx).Should(Succeed()) - Eventually(tests.RLPIsNotAccepted(ctx, testClient(), rlpAKey)).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute A direct back reference is gone - Eventually( - tests.HTTPRouteWithoutDirectBackReference(testClient(), routeAKey, rlpA.DirectReferenceAnnotationName())).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute B direct back reference - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeBKey, rlpB.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlpB).String(), - )).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("When target is deleted", func() { - var ( - routeName = "route-a" - ) - - It("policy status reports error", func(ctx SpecContext) { - // Initial state - // Route A - // RLP A -> Route A - - // Delete route - // RLP A - - // create httproute A - httpRoute := tests.BuildBasicHttpRoute(routeName, TestGatewayName, testNamespace, []string{"*.example.com"}) - err := k8sClient.Create(ctx, httpRoute) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) - - // create rlp - rlp := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeName) - }) - err = k8sClient.Create(ctx, rlp) - Expect(err).ToNot(HaveOccurred()) - - rlpKey := client.ObjectKeyFromObject(rlp) - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute A direct back reference - routeKey := client.ObjectKey{Name: routeName, Namespace: testNamespace} - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeKey, rlp.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlp).String(), - )).WithContext(ctx).Should(BeTrue()) - - // Proceed with the update: - // Delete Route A - err = k8sClient.Delete(ctx, httpRoute) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.ObjectDoesNotExist(testClient(), httpRoute)).WithContext(ctx).Should(BeTrue()) - - // Check RLP status is available - Eventually(tests.RLPIsNotAccepted(ctx, testClient(), rlpKey)).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("When RLP targets already taken HTTPRoute and the route is being released", func() { - var ( - routeAName = "route-a" - routeBName = "route-b" - rlpAName = "rlp-a" - rlpBName = "rlp-b" - ) - - It("direct references are updated and RLP status is ready", func(ctx SpecContext) { - // Initial state - // Route A - // RLP A -> Route A - - // New RLP targets already taken route - // Route A - // RLP A -> Route A - // RLP B -> Route A (already taken) - - // already taken route is being released by owner policy - // Route A - // Route B - // RLP A -> Route B - // RLP B -> Route A - - // create httproute A - httpRouteA := tests.BuildBasicHttpRoute(routeAName, TestGatewayName, testNamespace, []string{"*.a.example.com"}) - err := k8sClient.Create(ctx, httpRouteA) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRouteA))).WithContext(ctx).Should(BeTrue()) - - // create rlpA - rlpA := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.ObjectMeta.Name = rlpAName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeAName) - }) - err = k8sClient.Create(ctx, rlpA) - Expect(err).ToNot(HaveOccurred()) - - rlpAKey := client.ObjectKeyFromObject(rlpA) - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpAKey)).WithContext(ctx).Should(BeTrue()) - - // Proceed with the update: - // new RLP B -> Route A (already taken) - - // create rlpB - rlpB := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.ObjectMeta.Name = rlpBName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeAName) - }) - err = k8sClient.Create(ctx, rlpB) - Expect(err).ToNot(HaveOccurred()) - - // Check RLP status is not available - rlpBKey := client.ObjectKeyFromObject(rlpB) - Eventually(tests.RLPIsNotAccepted(ctx, testClient(), rlpBKey)).WithContext(ctx).Should(BeTrue()) - - // Check HTTPRoute A direct back reference to RLP A - routeAKey := client.ObjectKey{Name: routeAName, Namespace: testNamespace} - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeAKey, rlpA.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlpA).String(), - )).WithContext(ctx).Should(BeTrue()) - - // Proceed with the update: - // new Route B - // RLP A -> Route B - // RLP A was the older owner of route A, and wiil be the new owner of route B - - // create httproute B - httpRouteB := tests.BuildBasicHttpRoute(routeBName, TestGatewayName, testNamespace, []string{"*.b.example.com"}) - err = k8sClient.Create(ctx, httpRouteB) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRouteB))).WithContext(ctx).Should(BeTrue()) - - // RLP A -> Route B - Eventually(func(g Gomega) { - rlpUpdated := &kuadrantv1beta3.RateLimitPolicy{} - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(rlpA), rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - rlpUpdated.Spec.TargetRef.Name = gatewayapiv1.ObjectName(routeBName) - err = k8sClient.Update(ctx, rlpUpdated) - g.Expect(err).ToNot(HaveOccurred()) - }).WithContext(ctx).Should(Succeed()) - - // Check HTTPRoute A direct back reference to RLP B - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeAKey, rlpB.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlpB).String(), - )).WithContext(ctx).Should(BeTrue()) - - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpBKey)).WithContext(ctx).Should(BeTrue()) - - routeBKey := client.ObjectKey{Name: routeBName, Namespace: testNamespace} - // Check HTTPRoute B direct back reference to RLP A - Eventually( - tests.HTTPRouteHasDirectBackReference(testClient(), - routeBKey, rlpA.DirectReferenceAnnotationName(), - client.ObjectKeyFromObject(rlpA).String(), - )).WithContext(ctx).Should(BeTrue()) - - Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpAKey)).WithContext(ctx).Should(BeTrue()) + Eventually(assertAcceptedConditionTrue(rlp), time.Minute, 5*time.Second).Should(BeTrue()) }, testTimeOut) }) @@ -1316,12 +786,14 @@ var _ = Describe("RateLimitPolicy controller", func() { policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gatewayAName) policy.Spec.Defaults = nil - policy.Spec.Overrides = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "gw-a-1000rps": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1000, Duration: 1, Unit: "second", + policy.Spec.Overrides = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "gw-a-1000rps": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1000, Duration: 1, Unit: "second", + }, }, }, }, @@ -1336,12 +808,14 @@ var _ = Describe("RateLimitPolicy controller", func() { policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gatewayBName) policy.Spec.Defaults = nil - policy.Spec.Overrides = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "gw-b-100rps": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 100, Duration: 1, Unit: "second", + policy.Spec.Overrides = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "gw-b-100rps": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 100, Duration: 1, Unit: "second", + }, }, }, }, @@ -1355,7 +829,7 @@ var _ = Describe("RateLimitPolicy controller", func() { policy.ObjectMeta.Name = targetedRouteName policy.Spec.TargetRef.Kind = "HTTPRoute" policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(targetedRouteName) - policy.Spec.CommonSpec().Limits = map[string]kuadrantv1beta3.Limit{ + policy.Spec.Proper().Limits = map[string]kuadrantv1beta3.Limit{ "route-10rps": { Rates: []kuadrantv1beta3.Rate{ { @@ -1368,39 +842,38 @@ var _ = Describe("RateLimitPolicy controller", func() { err = k8sClient.Create(ctx, rlpTargetedRoute) Expect(err).ToNot(HaveOccurred()) + limitIdentifierGwA := controllers.LimitNameToLimitadorIdentifier(client.ObjectKeyFromObject(rlpGatewayA), "gw-a-1000rps") + limitIdentifierGwB := controllers.LimitNameToLimitadorIdentifier(client.ObjectKeyFromObject(rlpGatewayB), "gw-b-100rps") + Eventually(limitadorContainsLimit( ctx, limitadorv1alpha1.RateLimit{ MaxValue: 1000, Seconds: 1, - Namespace: wasm.LimitsNamespaceFromRLP(rlpTargetedRoute), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(client.ObjectKeyFromObject(rlpTargetedRoute), "gw-a-1000rps"))}, + Namespace: controllers.LimitsNamespaceFromRoute(targetedRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, limitIdentifierGwA)}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlpTargetedRoute), }, limitadorv1alpha1.RateLimit{ MaxValue: 100, Seconds: 1, - Namespace: wasm.LimitsNamespaceFromRLP(rlpTargetedRoute), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(client.ObjectKeyFromObject(rlpTargetedRoute), "gw-b-100rps"))}, + Namespace: controllers.LimitsNamespaceFromRoute(targetedRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, limitIdentifierGwB)}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlpTargetedRoute), }, limitadorv1alpha1.RateLimit{ // FIXME(@guicassolato): we need to create one limit definition per gateway × route combination, not one per gateway × policy combination MaxValue: 1000, Seconds: 1, - Namespace: wasm.LimitsNamespaceFromRLP(rlpGatewayA), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(client.ObjectKeyFromObject(rlpGatewayA), "gw-a-1000rps"))}, + Namespace: controllers.LimitsNamespaceFromRoute(untargetedRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, limitIdentifierGwA)}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlpGatewayA), }, limitadorv1alpha1.RateLimit{ MaxValue: 100, Seconds: 1, - Namespace: wasm.LimitsNamespaceFromRLP(rlpGatewayB), - Conditions: []string{fmt.Sprintf(`%s == "1"`, wasm.LimitNameToLimitadorIdentifier(client.ObjectKeyFromObject(rlpGatewayB), "gw-b-100rps"))}, + Namespace: controllers.LimitsNamespaceFromRoute(untargetedRoute), + Conditions: []string{fmt.Sprintf(`%s == "1"`, limitIdentifierGwB)}, Variables: []string{}, - Name: rlptools.LimitsNameFromRLP(rlpGatewayB), }, )).WithContext(ctx).Should(Succeed()) }, testTimeOut) @@ -1430,10 +903,12 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: "my-target", + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: "my-target", + }, }, }, } @@ -1492,10 +967,12 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { It("Valid - only explicit defaults defined", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "explicit": { - Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + policy.Spec.Defaults = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "explicit": { + Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + }, }, }, } @@ -1505,10 +982,12 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { It("Invalid - implicit and explicit defaults are mutually exclusive", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "explicit": { - Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + policy.Spec.Defaults = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "explicit": { + Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + }, }, }, } @@ -1525,17 +1004,21 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { It("Invalid - explicit default and override defined", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "implicit": { - Rates: []kuadrantv1beta3.Rate{{Limit: 2, Duration: 20, Unit: "second"}}, + policy.Spec.Defaults = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "implicit": { + Rates: []kuadrantv1beta3.Rate{{Limit: 2, Duration: 20, Unit: "second"}}, + }, }, }, } - policy.Spec.Overrides = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "explicit": { - Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + policy.Spec.Overrides = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "explicit": { + Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + }, }, }, } @@ -1552,10 +1035,12 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { Rates: []kuadrantv1beta3.Rate{{Limit: 2, Duration: 20, Unit: "second"}}, }, } - policy.Spec.Overrides = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "overrides": { - Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + policy.Spec.Overrides = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "overrides": { + Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + }, }, }, } @@ -1565,28 +1050,15 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { Expect(err.Error()).To(ContainSubstring("Overrides and implicit defaults are mutually exclusive")) }, testTimeOut) - It("Invalid - policy override targeting resource other than Gateway", func(ctx SpecContext) { - policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { - policy.Spec.Overrides = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "implicit": { - Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, - }, - }, - } - }) - err := k8sClient.Create(ctx, policy) - Expect(err).ToNot(BeNil()) - Expect(err.Error()).To(ContainSubstring("Overrides are only allowed for policies targeting a Gateway resource")) - }, testTimeOut) - It("Valid - policy override targeting Gateway", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.Overrides = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "override": { - Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + policy.Spec.Overrides = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "override": { + Rates: []kuadrantv1beta3.Rate{{Limit: 1, Duration: 10, Unit: "second"}}, + }, }, }, } diff --git a/tests/common/targetstatus/target_status_controller_test.go b/tests/common/targetstatus/target_status_controller_test.go index 7ce33de85..fd44390c9 100644 --- a/tests/common/targetstatus/target_status_controller_test.go +++ b/tests/common/targetstatus/target_status_controller_test.go @@ -340,17 +340,21 @@ var _ = Describe("Target status reconciler", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(TestHTTPRouteName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(TestHTTPRouteName), + }, }, - Defaults: &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: kuadrantv1beta3.TimeUnit("minute"), + Defaults: &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: kuadrantv1beta3.TimeUnit("minute"), + }, }, }, }, @@ -372,7 +376,7 @@ var _ = Describe("Target status reconciler", func() { if !tests.RLPIsAccepted(ctx, testClient(), policyKey)() { return false } - return targetsAffected(ctx, policyKey, policyAffectedCondition, policy.Spec.TargetRef, routeNames...) + return targetsAffected(ctx, policyKey, policyAffectedCondition, policy.Spec.TargetRef.LocalPolicyTargetReference, routeNames...) } } @@ -403,10 +407,12 @@ var _ = Describe("Target status reconciler", func() { It("adds PolicyAffected status condition to the targeted gateway and routes", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "gateway-rlp" - policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: TestGatewayName, + policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: TestGatewayName, + }, } }) Expect(k8sClient.Create(ctx, policy)).To(Succeed()) @@ -416,10 +422,12 @@ var _ = Describe("Target status reconciler", func() { It("removes PolicyAffected status condition from the targeted gateway and routes when the policy is deleted", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "gateway-rlp" - policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: TestGatewayName, + policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: TestGatewayName, + }, } }) Expect(k8sClient.Create(ctx, policy)).To(Succeed()) @@ -455,10 +463,12 @@ var _ = Describe("Target status reconciler", func() { gatewayPolicy := policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "gateway-rlp" - policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: TestGatewayName, + policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: TestGatewayName, + }, } }) Expect(k8sClient.Create(ctx, gatewayPolicy)).To(Succeed()) diff --git a/tests/envoygateway/wasm_controller_test.go b/tests/envoygateway/extension_reconciler_test.go similarity index 69% rename from tests/envoygateway/wasm_controller_test.go rename to tests/envoygateway/extension_reconciler_test.go index c786d71a3..04bfc1323 100644 --- a/tests/envoygateway/wasm_controller_test.go +++ b/tests/envoygateway/extension_reconciler_test.go @@ -8,19 +8,22 @@ import ( "time" egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/kuadrant/policy-machinery/machinery" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/controllers" "github.com/kuadrant/kuadrant-operator/pkg/common" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" "github.com/kuadrant/kuadrant-operator/tests" ) @@ -32,15 +35,19 @@ var _ = Describe("wasm controller", func() { var ( testNamespace string gwHost = fmt.Sprintf("*.toystore-%s.com", rand.String(4)) + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway ) BeforeEach(func(ctx SpecContext) { testNamespace = tests.CreateNamespace(ctx, testClient()) + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.NewGatewayBuilder(TestGatewayName, tests.GatewayClassName, testNamespace). WithHTTPListener("test-listener", gwHost). Gateway - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) @@ -77,8 +84,9 @@ var _ = Describe("wasm controller", func() { Context("RateLimitPolicy attached to the gateway", func() { var ( - gwPolicy *kuadrantv1beta3.RateLimitPolicy - gwRoute *gatewayapiv1.HTTPRoute + gwPolicy *kuadrantv1beta3.RateLimitPolicy + gwRoute *gatewayapiv1.HTTPRoute + actionSetName string ) BeforeEach(func(ctx SpecContext) { @@ -87,17 +95,30 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(gwRoute))).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: gwRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &gwRoute.Spec.Rules[0], Name: "rule-1"}, + }) + actionSetName = wasm.ActionSetNameForPath(pathID, 0, string(gwRoute.Spec.Hostnames[0])) + gwPolicy = policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "gw" policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.Defaults = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: "minute", + policy.Spec.Defaults = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: "minute", + }, }, }, }, @@ -119,7 +140,7 @@ var _ = Describe("wasm controller", func() { It("Creates envoyextensionpolicy", func(ctx SpecContext) { extKey := client.ObjectKey{ - Name: controllers.EnvoyExtensionPolicyName(TestGatewayName), + Name: wasm.ExtensionName(TestGatewayName), Namespace: testNamespace, } @@ -146,47 +167,41 @@ var _ = Describe("wasm controller", func() { existingWASMConfig, err := wasm.ConfigFromJSON(ext.Spec.Wasm[0].Config) Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { + Type: wasm.RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: gwPolicyKey.String(), - Hostnames: []string{gwHost}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: actionSetName, + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{string(gwRoute.Spec.Hostnames[0])}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(gwRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(gwPolicy), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(gwPolicyKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(gwPolicyKey, "l1"), + Value: "1", }, }, }, @@ -205,7 +220,7 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) extKey := client.ObjectKey{ - Name: controllers.EnvoyExtensionPolicyName(TestGatewayName), + Name: wasm.ExtensionName(TestGatewayName), Namespace: testNamespace, } @@ -222,7 +237,7 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) extKey := client.ObjectKey{ - Name: controllers.EnvoyExtensionPolicyName(TestGatewayName), + Name: wasm.ExtensionName(TestGatewayName), Namespace: testNamespace, } @@ -237,8 +252,9 @@ var _ = Describe("wasm controller", func() { Context("RateLimitPolicy attached to the route", func() { var ( - routePolicy *kuadrantv1beta3.RateLimitPolicy - gwRoute *gatewayapiv1.HTTPRoute + routePolicy *kuadrantv1beta3.RateLimitPolicy + gwRoute *gatewayapiv1.HTTPRoute + actionSetName string ) BeforeEach(func(ctx SpecContext) { @@ -247,17 +263,30 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(gwRoute))).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: gwRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &gwRoute.Spec.Rules[0], Name: "rule-1"}, + }) + actionSetName = wasm.ActionSetNameForPath(pathID, 0, string(gwRoute.Spec.Hostnames[0])) + routePolicy = policyFactory(func(policy *kuadrantv1beta3.RateLimitPolicy) { policy.Name = "route" policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "HTTPRoute" policy.Spec.TargetRef.Name = TestHTTPRouteName - policy.Spec.Defaults = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: "minute", + policy.Spec.Defaults = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: "minute", + }, }, }, }, @@ -277,7 +306,7 @@ var _ = Describe("wasm controller", func() { It("Creates envoyextensionpolicy", func(ctx SpecContext) { extKey := client.ObjectKey{ - Name: controllers.EnvoyExtensionPolicyName(TestGatewayName), + Name: wasm.ExtensionName(TestGatewayName), Namespace: testNamespace, } @@ -305,47 +334,41 @@ var _ = Describe("wasm controller", func() { existingWASMConfig, err := wasm.ConfigFromJSON(ext.Spec.Wasm[0].Config) Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { + Type: wasm.RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: routePolicyKey.String(), - Hostnames: []string{string(gwRoute.Spec.Hostnames[0])}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: actionSetName, + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{string(gwRoute.Spec.Hostnames[0])}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(gwRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(routePolicy), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(routePolicyKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(routePolicyKey, "l1"), + Value: "1", }, }, }, @@ -364,7 +387,7 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) extKey := client.ObjectKey{ - Name: controllers.EnvoyExtensionPolicyName(TestGatewayName), + Name: wasm.ExtensionName(TestGatewayName), Namespace: testNamespace, } @@ -382,7 +405,7 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) extKey := client.ObjectKey{ - Name: controllers.EnvoyExtensionPolicyName(TestGatewayName), + Name: wasm.ExtensionName(TestGatewayName), Namespace: testNamespace, } diff --git a/tests/envoygateway/envoygateway_limitador_cluster_controller_test.go b/tests/envoygateway/rate_limit_cluster_reconciler_test.go similarity index 92% rename from tests/envoygateway/envoygateway_limitador_cluster_controller_test.go rename to tests/envoygateway/rate_limit_cluster_reconciler_test.go index aad9dab0a..cb41fb03b 100644 --- a/tests/envoygateway/envoygateway_limitador_cluster_controller_test.go +++ b/tests/envoygateway/rate_limit_cluster_reconciler_test.go @@ -103,12 +103,14 @@ var _ = Describe("limitador cluster controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.Defaults = &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "l1": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: "minute", + policy.Spec.Defaults = &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "l1": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: "minute", + }, }, }, }, @@ -130,9 +132,7 @@ var _ = Describe("limitador cluster controller", func() { It("Creates envoypatchpolicy for limitador cluster", func(ctx SpecContext) { patchKey := client.ObjectKey{ - Name: controllers.LimitadorClusterEnvoyPatchPolicyName( - controllers.EnvoyExtensionPolicyName(TestGatewayName), - ), + Name: controllers.RateLimitClusterName(TestGatewayName), Namespace: testNamespace, } @@ -202,9 +202,7 @@ var _ = Describe("limitador cluster controller", func() { Expect(err).ToNot(HaveOccurred()) patchKey := client.ObjectKey{ - Name: controllers.LimitadorClusterEnvoyPatchPolicyName( - controllers.EnvoyExtensionPolicyName(TestGatewayName), - ), + Name: controllers.RateLimitClusterName(TestGatewayName), Namespace: testNamespace, } @@ -221,9 +219,7 @@ var _ = Describe("limitador cluster controller", func() { Expect(err).ToNot(HaveOccurred()) patchKey := client.ObjectKey{ - Name: controllers.LimitadorClusterEnvoyPatchPolicyName( - controllers.EnvoyExtensionPolicyName(TestGatewayName), - ), + Name: controllers.RateLimitClusterName(TestGatewayName), Namespace: testNamespace, } diff --git a/tests/istio/rate_limiting_istio_wasmplugin_controller_test.go b/tests/istio/extension_reconciler_test.go similarity index 55% rename from tests/istio/rate_limiting_istio_wasmplugin_controller_test.go rename to tests/istio/extension_reconciler_test.go index 4fbd6c8dd..46cbe8a8c 100644 --- a/tests/istio/rate_limiting_istio_wasmplugin_controller_test.go +++ b/tests/istio/extension_reconciler_test.go @@ -8,22 +8,25 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/kuadrant/policy-machinery/machinery" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/controllers" "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" "github.com/kuadrant/kuadrant-operator/tests" ) @@ -59,14 +62,18 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Context("Basic tests", func() { var ( - routeName = "toystore-route" - rlpName = "toystore-rlp" - gateway *gatewayapiv1.Gateway + routeName = "toystore-route" + rlpName = "toystore-rlp" + gatewayClass *gatewayapiv1.GatewayClass + gateway *gatewayapiv1.Gateway ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } @@ -80,6 +87,16 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + // create ratelimitpolicy rlp := &kuadrantv1beta3.RateLimitPolicy{ TypeMeta: metav1.TypeMeta{ @@ -87,12 +104,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -113,61 +132,55 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} Eventually(tests.WasmPluginIsAvailable(ctx, testClient(), wasmPluginKey)).WithContext(ctx).Should(BeTrue()) existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err = testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) // must exist Expect(err).ToNot(HaveOccurred()) // has the correct target ref - Expect(existingWasmPlugin.Spec.TargetRef).To(Not(BeNil())) - Expect(existingWasmPlugin.Spec.TargetRef.Group).To(Equal("gateway.networking.k8s.io")) - Expect(existingWasmPlugin.Spec.TargetRef.Kind).To(Equal("Gateway")) - Expect(existingWasmPlugin.Spec.TargetRef.Name).To(Equal(gateway.Name)) + Expect(existingWasmPlugin.Spec.TargetRefs).To(Not(BeEmpty())) + Expect(existingWasmPlugin.Spec.TargetRefs[0].Group).To(Equal("gateway.networking.k8s.io")) + Expect(existingWasmPlugin.Spec.TargetRefs[0].Kind).To(Equal("Gateway")) + Expect(existingWasmPlugin.Spec.TargetRefs[0].Name).To(Equal(gateway.Name)) existingWASMConfig, err := wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { + Type: wasm.RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -227,12 +240,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "users": { Rates: []kuadrantv1beta3.Rate{ @@ -265,7 +280,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} Eventually(tests.WasmPluginIsAvailable(ctx, testClient(), wasmPluginKey)).WithContext(ctx).Should(BeTrue()) existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err = testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -273,151 +288,391 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) existingWASMConfig, err := wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) Expect(err).ToNot(HaveOccurred()) - Expect(existingWASMConfig.Extensions).To(HaveKeyWithValue(wasm.RateLimitPolicyExtensionName, wasm.Extension{ + Expect(existingWASMConfig.Services).To(HaveKeyWithValue(wasm.RateLimitServiceName, wasm.Service{ Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, })) - Expect(existingWASMConfig.Policies).To(HaveLen(1)) - policy := existingWASMConfig.Policies[0] - Expect(policy.Name).To(Equal(rlpKey.String())) - Expect(policy.Hostnames).To(Equal([]string{"*.toystore.acme.com", "api.toystore.io"})) - Expect(policy.Rules).To(ContainElement(wasm.Rule{ // rule to activate the 'users' limit definition - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", + Expect(existingWASMConfig.ActionSets).To(HaveLen(6)) + + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + basePath := []machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + } + httpRouteRuleToys := &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"} + httpRouteRuleAssets := &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[1], Name: "rule-2"} + + // *.toystore.acme.com/assets* + actionSet := existingWASMConfig.ActionSets[0] + pathID := kuadrantv1.PathID(append(basePath, httpRouteRuleAssets)) + Expect(actionSet.Name).To(Equal(wasm.ActionSetNameForPath(pathID, 0, "*.toystore.acme.com"))) + Expect(actionSet.RouteRuleConditions.Hostnames).To(Equal([]string{"*.toystore.acme.com"})) + Expect(actionSet.RouteRuleConditions.Matches).To(ContainElements( + wasm.Predicate{ + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/assets", + }, + )) + Expect(actionSet.Actions).To(HaveLen(2)) + Expect(actionSet.Actions).To(ContainElements( + wasm.Action{ // action to activate the 'users' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "users"), + Value: "1", + }, }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), + }, }, - { - Selector: "auth.identity.group", - Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "admin", + }, + }, + }, + wasm.Action{ // action to activate the 'all' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "all"), + Value: "1", + }, }, }, }, - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", + }, + )) + + // GET *.toystore.acme.com/toys* + actionSet = existingWASMConfig.ActionSets[1] + pathID = kuadrantv1.PathID(append(basePath, httpRouteRuleToys)) + Expect(actionSet.Name).To(Equal(wasm.ActionSetNameForPath(pathID, 0, "*.toystore.acme.com"))) + Expect(actionSet.RouteRuleConditions.Hostnames).To(Equal([]string{"*.toystore.acme.com"})) + Expect(actionSet.RouteRuleConditions.Matches).To(ContainElements( + wasm.Predicate{ + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + wasm.Predicate{ + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toys", + }, + )) + Expect(actionSet.Actions).To(HaveLen(2)) + Expect(actionSet.Actions).To(ContainElements( + wasm.Action{ // action to activate the 'users' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "users"), + Value: "1", + }, }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "POST", + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), + }, }, - { - Selector: "auth.identity.group", - Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "admin", + }, + }, + }, + wasm.Action{ // action to activate the 'all' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "all"), + Value: "1", + }, }, }, }, - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/assets", + }, + )) + + // POST *.toystore.acme.com/toys* + actionSet = existingWASMConfig.ActionSets[2] + pathID = kuadrantv1.PathID(append(basePath, httpRouteRuleToys)) + Expect(actionSet.Name).To(Equal(wasm.ActionSetNameForPath(pathID, 1, "*.toystore.acme.com"))) + Expect(actionSet.RouteRuleConditions.Hostnames).To(Equal([]string{"*.toystore.acme.com"})) + Expect(actionSet.RouteRuleConditions.Matches).To(ContainElements( + wasm.Predicate{ + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "POST", + }, + wasm.Predicate{ + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toys", + }, + )) + Expect(actionSet.Actions).To(HaveLen(2)) + Expect(actionSet.Actions).To(ContainElements( + wasm.Action{ // action to activate the 'users' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "users"), + Value: "1", + }, }, - { - Selector: "auth.identity.group", - Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "admin", + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), + }, }, }, }, }, - Actions: []wasm.Action{ - { - Scope: wasm.LimitsNamespaceFromRLP(rlp), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "users"), - Value: "1", - }, + wasm.Action{ // action to activate the 'all' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "all"), + Value: "1", }, }, - { - Value: &wasm.Selector{ - Selector: wasm.SelectorSpec{ - Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), - }, + }, + }, + }, + )) + + // api.toystore.io/assets* + actionSet = existingWASMConfig.ActionSets[3] + pathID = kuadrantv1.PathID(append(basePath, httpRouteRuleAssets)) + Expect(actionSet.Name).To(Equal(wasm.ActionSetNameForPath(pathID, 0, "api.toystore.io"))) + Expect(actionSet.RouteRuleConditions.Hostnames).To(Equal([]string{"api.toystore.io"})) + Expect(actionSet.RouteRuleConditions.Matches).To(ContainElements( + wasm.Predicate{ + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/assets", + }, + )) + Expect(actionSet.Actions).To(HaveLen(2)) + Expect(actionSet.Actions).To(ContainElements( + wasm.Action{ // action to activate the 'users' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "users"), + Value: "1", + }, + }, + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), }, }, }, }, }, - })) - Expect(policy.Rules).To(ContainElement(wasm.Rule{ // rule to activate the 'all' limit definition - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", + wasm.Action{ // action to activate the 'all' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "all"), + Value: "1", + }, }, }, }, - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toys", + }, + )) + + // GET api.toystore.io/toys* + actionSet = existingWASMConfig.ActionSets[4] + pathID = kuadrantv1.PathID(append(basePath, httpRouteRuleToys)) + Expect(actionSet.Name).To(Equal(wasm.ActionSetNameForPath(pathID, 0, "api.toystore.io"))) + Expect(actionSet.RouteRuleConditions.Hostnames).To(Equal([]string{"api.toystore.io"})) + Expect(actionSet.RouteRuleConditions.Matches).To(ContainElements( + wasm.Predicate{ + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + wasm.Predicate{ + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toys", + }, + )) + Expect(actionSet.Actions).To(HaveLen(2)) + Expect(actionSet.Actions).To(ContainElements( + wasm.Action{ // action to activate the 'users' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "users"), + Value: "1", + }, }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "POST", + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), + }, }, }, }, - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/assets", + }, + wasm.Action{ // action to activate the 'all' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "all"), + Value: "1", + }, }, }, }, }, - Actions: []wasm.Action{ - { - Scope: wasm.LimitsNamespaceFromRLP(rlp), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "all"), - Value: "1", - }, + )) + + // POST api.toystore.io/toys* + actionSet = existingWASMConfig.ActionSets[5] + pathID = kuadrantv1.PathID(append(basePath, httpRouteRuleToys)) + Expect(actionSet.Name).To(Equal(wasm.ActionSetNameForPath(pathID, 1, "api.toystore.io"))) + Expect(actionSet.RouteRuleConditions.Hostnames).To(Equal([]string{"api.toystore.io"})) + Expect(actionSet.RouteRuleConditions.Matches).To(ContainElements( + wasm.Predicate{ + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "POST", + }, + wasm.Predicate{ + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toys", + }, + )) + Expect(actionSet.Actions).To(HaveLen(2)) + Expect(actionSet.Actions).To(ContainElements( + wasm.Action{ // action to activate the 'users' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Conditions: []wasm.Predicate{ + { + Selector: "auth.identity.group", + Operator: wasm.PatternOperator(kuadrantv1beta3.NotEqualOperator), + Value: "admin", + }, + }, + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "users"), + Value: "1", + }, + }, + }, + { + Value: &wasm.Selector{ + Selector: wasm.SelectorSpec{ + Selector: kuadrantv1beta3.ContextSelector("auth.identity.username"), }, }, }, }, }, - })) + wasm.Action{ // action to activate the 'all' limit definition + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ + { + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "all"), + Value: "1", + }, + }, + }, + }, + }, + )) }, testTimeOut) It("Simple RLP targeting Gateway parented by one HTTPRoute creates wasmplugin", func(ctx SpecContext) { @@ -427,6 +682,16 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + // create ratelimitpolicy rlp := &kuadrantv1beta3.RateLimitPolicy{ TypeMeta: metav1.TypeMeta{ @@ -434,12 +699,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -460,7 +727,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} Eventually(tests.WasmPluginIsAvailable(ctx, testClient(), wasmPluginKey)).WithContext(ctx).Should(BeTrue()) existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err = testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -469,47 +736,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { existingWASMConfig, err := wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -546,12 +807,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -570,10 +833,10 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // Check RLP status is available rlpKey := client.ObjectKey{Name: rlpName, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} // Wait a bit to catch cases where wasmplugin is created and takes a bit to be created Eventually(tests.WasmPluginIsAvailable(ctx, testClient(), wasmPluginKey), 20*time.Second, 5*time.Second).Should(BeFalse()) existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} @@ -588,13 +851,17 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { routeName = "route-a" rlpName = "rlp-a" TestGatewayName = "toystore-gw" + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway gwBName = "gw-b" ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } @@ -623,12 +890,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -646,7 +915,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // Check RLP status is available rlpKey := client.ObjectKey{Name: rlpName, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndNotEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) // create Route A -> Gw A httpRoute := tests.BuildBasicHttpRoute(routeName, TestGatewayName, testNamespace, []string{"*.example.com"}) @@ -661,12 +930,22 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gwB)).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + // Initial state set. // Check wasm plugin for gateway A has configuration from the route // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -681,47 +960,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlpA), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -745,7 +1018,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gwB), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gwB.GetName()), Namespace: testNamespace} existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) if err == nil { @@ -775,7 +1048,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // Check wasm plugin for gateway A no longer exists // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) if err == nil { @@ -794,7 +1067,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // There is not RLP targeting Gateway B or any route parented by Gateway B // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gwB), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gwB.GetName()), Namespace: testNamespace} existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) if err == nil { @@ -837,6 +1110,16 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + // create RLP A -> Route A rlpA := &kuadrantv1beta3.RateLimitPolicy{ TypeMeta: metav1.TypeMeta{ @@ -844,12 +1127,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -873,7 +1158,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -888,47 +1173,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlpA), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -952,7 +1231,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gwB), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gwB.GetName()), Namespace: testNamespace} existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) if err == nil { @@ -982,7 +1261,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // Check wasm plugin for gateway A no longer exists // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) if err == nil { @@ -997,11 +1276,20 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { return true }) + mGateway = &machinery.Gateway{Gateway: gwB} + pathID = kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + // Check wasm plugin for gateway B has configuration from the route // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gwB), Namespace: testNamespace, + Name: wasm.ExtensionName(gwB.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1016,47 +1304,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlpA), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -1081,12 +1363,16 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Context("RLP switches targetRef from one route A to another route B", func() { var ( TestGatewayName = "toystore-gw" + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } @@ -1165,12 +1451,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeAName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeAName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -1189,12 +1477,22 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { rlpKey := client.ObjectKey{Name: rlpName, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRouteA} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRouteA.Spec.Rules[0], Name: "rule-1"}, + }) + // Initial state set. // Check wasm plugin has configuration from the route A // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1209,47 +1507,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*.a.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeA", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.a.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.a.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeA", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteA), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlpR), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -1281,11 +1573,20 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { g.Expect(err).ToNot(HaveOccurred()) }).WithContext(ctx).Should(Succeed()) + mHTTPRoute = &machinery.HTTPRoute{HTTPRoute: httpRouteB} + pathID = kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRouteB.Spec.Rules[0], Name: "rule-1"}, + }) + // Check wasm plugin has configuration from the route B // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1300,47 +1601,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{"*.b.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeB", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.b.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.b.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeB", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteB), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlpR), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -1365,12 +1660,16 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Context("Free Route gets dedicated RLP", func() { var ( TestGatewayName = "toystore-gw" + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } @@ -1424,12 +1723,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlp1Name, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "gatewaylimit": { Rates: []kuadrantv1beta3.Rate{ @@ -1448,12 +1749,22 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { rlp1Key := client.ObjectKey{Name: rlp1Name, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlp1Key)).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRouteA} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRouteA.Spec.Rules[0], Name: "rule-1"}, + }) + // Initial state set. // Check wasm plugin for gateway A has configuration from the route 1 // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1468,47 +1779,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlp1Key.String(), - Hostnames: []string{"*"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeA", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.a.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.a.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeA", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteA), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp1), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlp1Key, "gatewaylimit"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlp1Key, "gatewaylimit"), + Value: "1", }, }, }, @@ -1540,12 +1845,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlp2Name, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeAName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeAName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "routelimit": { Rates: []kuadrantv1beta3.Rate{ @@ -1569,7 +1876,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1584,47 +1891,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlp2Key.String(), - Hostnames: []string{"*.a.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeA", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.a.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.a.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeA", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteA), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp2), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlp2Key, "routelimit"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlp2Key, "routelimit"), + Value: "1", }, }, }, @@ -1649,12 +1950,16 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Context("New free route on a Gateway with RLP", func() { var ( TestGatewayName = "toystore-gw" + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } @@ -1711,12 +2016,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlp1Name, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "gatewaylimit": { Rates: []kuadrantv1beta3.Rate{ @@ -1742,12 +2049,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlp2Name, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeAName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeAName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "routelimit": { Rates: []kuadrantv1beta3.Rate{ @@ -1766,12 +2075,22 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { rlp2Key := client.ObjectKey{Name: rlp2Name, Namespace: testNamespace} Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlp2Key)).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRouteA} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRouteA.Spec.Rules[0], Name: "rule-1"}, + }) + // Initial state set. // Check wasm plugin for gateway A has configuration from the route A only affected by RLP 2 // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1786,47 +2105,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { } expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlp2Key.String(), - Hostnames: []string{"*.a.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeA", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, "*.a.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.a.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeA", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteA), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp2), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlp2Key, "routelimit"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlp2Key, "routelimit"), + Value: "1", }, }, }, @@ -1877,7 +2190,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { // it may take some reconciliation loops to get to that, so checking it with eventually Eventually(func() bool { wasmPluginKey := client.ObjectKey{ - Name: controllers.WASMPluginName(gateway), Namespace: testNamespace, + Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace, } existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err := testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -1891,48 +2204,51 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { return false } + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRouteB} + pathID_B := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRouteB.Spec.Rules[0], Name: "rule-1"}, + }) + expectedPlugin := &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ - { // First RLP 1 as the controller will sort based on RLP name - Name: rlp1Key.String(), // Route B affected by RLP 1 -> Gateway - Hostnames: []string{"*"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeB", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + ActionSets: []wasm.ActionSet{ + { + Name: wasm.ActionSetNameForPath(pathID, 0, "*.a.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.a.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeA", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteA), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp1), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlp1Key, "gatewaylimit"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlp2Key, "routelimit"), + Value: "1", }, }, }, @@ -1941,38 +2257,32 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, }, { - Name: rlp2Key.String(), // Route A affected by RLP 1 -> Route A - Hostnames: []string{"*.a.example.com"}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/routeA", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID_B, 0, "*.b.example.com"), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{"*.b.example.com"}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", }, - Actions: []wasm.Action{ + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/routeB", + }, + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRouteB), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp2), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlp2Key, "routelimit"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlp1Key, "gatewaylimit"), + Value: "1", }, }, }, @@ -2000,14 +2310,18 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { TestGatewayName = "toystore-gw" routeName = "toystore-route" rlpName = "rlp-a" + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway gwHostname = "*.gw.example.com" ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) gateway.Spec.Listeners[0].Hostname = ptr.To(gatewayapiv1.Hostname(gwHostname)) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } @@ -2029,12 +2343,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: rlpName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -2054,8 +2370,18 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { rlpKey := client.ObjectKeyFromObject(rlp) Eventually(assertPolicyIsAcceptedAndEnforced(ctx, rlpKey)).WithContext(ctx).Should(BeTrue()) + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} Eventually(tests.WasmPluginIsAvailable(ctx, testClient(), wasmPluginKey)).WithContext(ctx).Should(BeTrue()) existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} err = testClient().Get(ctx, wasmPluginKey, existingWasmPlugin) @@ -2064,47 +2390,41 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { existingWASMConfig, err := wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{gwHostname}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, gwHostname), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{gwHostname}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: wasm.LimitNameToLimitadorIdentifier(rlpKey, "l1"), - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: controllers.LimitNameToLimitadorIdentifier(rlpKey, "l1"), + Value: "1", }, }, }, @@ -2123,59 +2443,67 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { gwRLPName = "gw-rlp" routeRLPName = "route-rlp" TestGatewayName = "toystore-gw" + gatewayClass *gatewayapiv1.GatewayClass gateway *gatewayapiv1.Gateway ) beforeEachCallback := func(ctx SpecContext) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := testClient().Create(ctx, gateway) + err = testClient().Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) } - expectedWasmPluginConfig := func(rlpKey client.ObjectKey, rlp *kuadrantv1beta3.RateLimitPolicy, key, hostname string) *wasm.Config { + expectedWasmPluginConfig := func(httpRoute *gatewayapiv1.HTTPRoute, key, hostname string) *wasm.Config { + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + pathID := kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[0], Name: "rule-1"}, + }) + return &wasm.Config{ - Extensions: map[string]wasm.Extension{ - wasm.RateLimitPolicyExtensionName: { + Services: map[string]wasm.Service{ + wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, - Type: wasm.RateLimitExtensionType, + Type: wasm.RateLimitServiceType, }, }, - Policies: []wasm.Policy{ + ActionSets: []wasm.ActionSet{ { - Name: rlpKey.String(), - Hostnames: []string{hostname}, - Rules: []wasm.Rule{ - { - Conditions: []wasm.Condition{ - { - AllOf: []wasm.PatternExpression{ - { - Selector: "request.url_path", - Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/toy", - }, - { - Selector: "request.method", - Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), - Value: "GET", - }, - }, - }, + Name: wasm.ActionSetNameForPath(pathID, 0, hostname), + RouteRuleConditions: wasm.RouteRuleConditions{ + Hostnames: []string{hostname}, + Matches: []wasm.Predicate{ + { + Selector: "request.method", + Operator: wasm.PatternOperator(kuadrantv1beta3.EqualOperator), + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: wasm.PatternOperator(kuadrantv1beta3.StartsWithOperator), + Value: "/toy", }, - Actions: []wasm.Action{ + }, + }, + Actions: []wasm.Action{ + { + ServiceName: wasm.RateLimitServiceName, + Scope: controllers.LimitsNamespaceFromRoute(httpRoute), + Data: []wasm.DataType{ { - Scope: wasm.LimitsNamespaceFromRLP(rlp), - ExtensionName: wasm.RateLimitPolicyExtensionName, - Data: []wasm.DataType{ - { - Value: &wasm.Static{ - Static: wasm.StaticSpec{ - Key: key, - Value: "1", - }, - }, + Value: &wasm.Static{ + Static: wasm.StaticSpec{ + Key: key, + Value: "1", }, }, }, @@ -2202,17 +2530,21 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: gwRLPName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - Defaults: &kuadrantv1beta3.RateLimitPolicyCommonSpec{ - Limits: map[string]kuadrantv1beta3.Limit{ - "gateway": { - Rates: []kuadrantv1beta3.Rate{ - { - Limit: 1, Duration: 3, Unit: kuadrantv1beta3.TimeUnit("minute"), + Defaults: &kuadrantv1beta3.MergeableRateLimitPolicySpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ + Limits: map[string]kuadrantv1beta3.Limit{ + "gateway": { + Rates: []kuadrantv1beta3.Rate{ + { + Limit: 1, Duration: 3, Unit: kuadrantv1beta3.TimeUnit("minute"), + }, }, }, }, @@ -2227,14 +2559,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Eventually(assertPolicyIsAcceptedAndEnforced(ctx, gwRLPKey)).WithContext(ctx).Should(BeTrue()) // Check wasm plugin - wasmPluginKey := client.ObjectKey{Name: controllers.WASMPluginName(gateway), Namespace: testNamespace} + wasmPluginKey := client.ObjectKey{Name: wasm.ExtensionName(gateway.GetName()), Namespace: testNamespace} Eventually(tests.WasmPluginIsAvailable(ctx, testClient(), wasmPluginKey)).WithContext(ctx).Should(BeTrue()) existingWasmPlugin := &istioclientgoextensionv1alpha1.WasmPlugin{} // must exist Expect(testClient().Get(ctx, wasmPluginKey, existingWasmPlugin)).To(Succeed()) existingWASMConfig, err := wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) Expect(err).ToNot(HaveOccurred()) - Expect(existingWASMConfig).To(Equal(expectedWasmPluginConfig(gwRLPKey, gwRLP, wasm.LimitNameToLimitadorIdentifier(gwRLPKey, "gateway"), "*"))) + Expect(existingWASMConfig).To(Equal(expectedWasmPluginConfig(httpRoute, controllers.LimitNameToLimitadorIdentifier(gwRLPKey, "gateway"), "*.example.com"))) // Create Route RLP routeRLP := &kuadrantv1beta3.RateLimitPolicy{ @@ -2243,12 +2575,14 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { }, ObjectMeta: metav1.ObjectMeta{Name: routeRLPName, Namespace: testNamespace}, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "route": { Rates: []kuadrantv1beta3.Rate{ @@ -2270,7 +2604,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { g.Expect(testClient().Get(ctx, wasmPluginKey, existingWasmPlugin)).To(Succeed()) existingWASMConfig, err = wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingWASMConfig).To(Equal(expectedWasmPluginConfig(routeRLPKey, routeRLP, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "route"), "*.example.com"))) + g.Expect(existingWASMConfig).To(Equal(expectedWasmPluginConfig(httpRoute, controllers.LimitNameToLimitadorIdentifier(routeRLPKey, "route"), "*.example.com"))) }).WithContext(ctx).Should(Succeed()) // Update GW RLP to overrides @@ -2287,7 +2621,7 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { g.Expect(testClient().Get(ctx, wasmPluginKey, existingWasmPlugin)).To(Succeed()) existingWASMConfig, err = wasm.ConfigFromStruct(existingWasmPlugin.Spec.PluginConfig) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(existingWASMConfig).To(Equal(expectedWasmPluginConfig(routeRLPKey, routeRLP, wasm.LimitNameToLimitadorIdentifier(routeRLPKey, "gateway"), "*.example.com"))) + g.Expect(existingWASMConfig).To(Equal(expectedWasmPluginConfig(httpRoute, controllers.LimitNameToLimitadorIdentifier(gwRLPKey, "gateway"), "*.example.com"))) }).WithContext(ctx).Should(Succeed()) }, testTimeOut) diff --git a/tests/istio/limitador_cluster_envoyfilter_controller_test.go b/tests/istio/rate_limit_cluster_reconciler_test.go similarity index 72% rename from tests/istio/limitador_cluster_envoyfilter_controller_test.go rename to tests/istio/rate_limit_cluster_reconciler_test.go index 6effe5fec..2f51713b9 100644 --- a/tests/istio/limitador_cluster_envoyfilter_controller_test.go +++ b/tests/istio/rate_limit_cluster_reconciler_test.go @@ -3,7 +3,6 @@ package istio_test import ( - "fmt" "time" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" @@ -19,6 +18,7 @@ import ( gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/controllers" "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/tests" @@ -32,7 +32,6 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { var ( testNamespace string rlpName = "toystore-rlp" - efName = fmt.Sprintf("kuadrant-ratelimiting-cluster-%s", TestGatewayName) ) beforeEachCallback := func(ctx SpecContext) { @@ -77,7 +76,7 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { }, afterEachTimeOut) Context("RLP targeting Gateway", func() { - It("EnvoyFilter created when RLP exists and deleted with RLP is deleted", func(ctx SpecContext) { + It("EnvoyFilter only created if RLP is in the path to a route", func(ctx SpecContext) { // create ratelimitpolicy rlp := &kuadrantv1beta3.RateLimitPolicy{ TypeMeta: metav1.TypeMeta{ @@ -89,12 +88,14 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.RateLimitPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: gatewayapiv1.ObjectName(TestGatewayName), + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(TestGatewayName), + }, }, - RateLimitPolicyCommonSpec: kuadrantv1beta3.RateLimitPolicyCommonSpec{ + RateLimitPolicySpecProper: kuadrantv1beta3.RateLimitPolicySpecProper{ Limits: map[string]kuadrantv1beta3.Limit{ "l1": { Rates: []kuadrantv1beta3.Rate{ @@ -113,12 +114,24 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { rlpKey := client.ObjectKey{Name: rlpName, Namespace: testNamespace} Eventually(tests.RLPIsAccepted(ctx, testClient(), rlpKey)).WithContext(ctx).Should(BeTrue()) Eventually(tests.RLPIsEnforced(ctx, testClient(), rlpKey)).WithContext(ctx).Should(BeFalse()) - Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy has encountered some issues: no free routes to enforce policy")) + Expect(tests.RLPEnforcedCondition(ctx, testClient(), rlpKey, kuadrant.PolicyReasonUnknown, "RateLimitPolicy is not in the path to any existing routes")) + + // Check envoy filter has not been created + Eventually(func() bool { + existingEF := &istioclientnetworkingv1alpha3.EnvoyFilter{} + efKey := client.ObjectKey{Name: controllers.RateLimitClusterName(TestGatewayName), Namespace: testNamespace} + err = testClient().Get(ctx, efKey, existingEF) + return apierrors.IsNotFound(err) + }).WithContext(ctx).Should(BeTrue()) + + route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{"*.toystore.com"}) + Expect(k8sClient.Create(ctx, route)).To(Succeed()) + Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - // Check envoy filter + // Check envoy filter has been created Eventually(func() bool { existingEF := &istioclientnetworkingv1alpha3.EnvoyFilter{} - efKey := client.ObjectKey{Name: efName, Namespace: testNamespace} + efKey := client.ObjectKey{Name: controllers.RateLimitClusterName(TestGatewayName), Namespace: testNamespace} err = testClient().Get(ctx, efKey, existingEF) if err != nil { return false @@ -132,7 +145,7 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { // Check envoy filter is gone Eventually(func() bool { existingEF := &istioclientnetworkingv1alpha3.EnvoyFilter{} - efKey := client.ObjectKey{Name: efName, Namespace: testNamespace} + efKey := client.ObjectKey{Name: controllers.RateLimitClusterName(TestGatewayName), Namespace: testNamespace} err = testClient().Get(ctx, efKey, existingEF) return apierrors.IsNotFound(err) }).WithContext(ctx).Should(BeTrue())