Skip to content

Commit

Permalink
feat: apply RLP gateway overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
KevFan committed Apr 10, 2024
1 parent 241e50f commit 3ad3f9c
Show file tree
Hide file tree
Showing 7 changed files with 1,071 additions and 9 deletions.
5 changes: 5 additions & 0 deletions api/v1beta2/ratelimitpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ type RateLimitPolicySpec struct {
// +optional
Defaults *RateLimitPolicyCommonSpec `json:"defaults,omitempty"`

// Overrides define override values for this policy and for policies inheriting this policy.
// Defaults are mutually exclusive with implicit 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:""`
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

394 changes: 394 additions & 0 deletions bundle/manifests/kuadrant.io_ratelimitpolicies.yaml

Large diffs are not rendered by default.

394 changes: 394 additions & 0 deletions config/crd/bases/kuadrant.io_ratelimitpolicies.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions controllers/ratelimitpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (r *RateLimitPolicyReconciler) reconcileResources(ctx context.Context, rlp
return err
}

if err := r.reconcileLimits(ctx, rlp); err != nil {
if err := r.reconcileLimits(ctx, rlp, targetNetworkObject); err != nil {
return err
}

Expand All @@ -195,7 +195,7 @@ func (r *RateLimitPolicyReconciler) deleteResources(ctx context.Context, rlp *ku
return err
}

if err := r.deleteLimits(ctx, rlp); err != nil && !apierrors.IsNotFound(err) {
if err := r.deleteLimits(ctx, rlp, targetNetworkObject); err != nil && !apierrors.IsNotFound(err) {
return err
}

Expand Down
232 changes: 232 additions & 0 deletions controllers/ratelimitpolicy_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,238 @@ var _ = Describe("RateLimitPolicy controller", func() {
}, SpecTimeout(time.Minute))
})

Context("RLP Overrides", func() {
It("Gateway atomic override - gateway overrides exist and then route policy created", func(ctx SpecContext) {
// create httproute
httpRoute := testBuildBasicHttpRoute(routeName, gwName, testNamespace, []string{"*.example.com"})
Expect(k8sClient.Create(ctx, httpRoute)).To(Succeed())
Eventually(testRouteIsAccepted(client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue())

// create GW RLP
gwRLP := policyFactory(func(policy *kuadrantv1beta2.RateLimitPolicy) {
policy.Spec.TargetRef.Kind = "Gateway"
policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gwName)
policy.Spec.Defaults = nil
policy.Spec.Overrides = &kuadrantv1beta2.RateLimitPolicyCommonSpec{
Limits: map[string]kuadrantv1beta2.Limit{
"l1": {
Rates: []kuadrantv1beta2.Rate{
{
Limit: 1, Duration: 3, Unit: kuadrantv1beta2.TimeUnit("minute"),
},
},
},
},
}
})
Expect(k8sClient.Create(ctx, gwRLP)).To(Succeed())
rlpKey := client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace}
Eventually(testRLPIsAccepted(rlpKey)).WithContext(ctx).Should(BeTrue())

// Create HTTPRoute RLP with new default limits
routeRLP := policyFactory(func(policy *kuadrantv1beta2.RateLimitPolicy) {
policy.Name = "httproute-rlp"
policy.Spec.CommonSpec().Limits = map[string]kuadrantv1beta2.Limit{
"l1": {
Rates: []kuadrantv1beta2.Rate{
{
Limit: 10, Duration: 5, Unit: kuadrantv1beta2.TimeUnit("second"),
},
},
},
}
})
Expect(k8sClient.Create(ctx, routeRLP)).To(Succeed())
rlpKey = client.ObjectKey{Name: routeRLP.Name, Namespace: testNamespace}
Eventually(testRLPIsAccepted(rlpKey)).WithContext(ctx).Should(BeTrue())

// Check Gateway direct back reference
gwKey := client.ObjectKeyFromObject(gateway)
existingGateway := &gatewayapiv1.Gateway{}
Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed())
Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(
gwRLP.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(gwRLP).String()))

// check limits - should contain override values
Eventually(func(g Gomega) {
limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace}
existingLimitador := &limitadorv1alpha1.Limitador{}
g.Expect(k8sClient.Get(ctx, limitadorKey, existingLimitador)).To(Succeed())
g.Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{
MaxValue: 1,
Seconds: 180,
Namespace: rlptools.LimitsNamespaceFromRLP(routeRLP),
Conditions: []string{`limit.l1__2804bad6 == "1"`},
Variables: []string{},
Name: rlptools.LimitsNameFromRLP(routeRLP),
}))
}).WithContext(ctx).Should(Succeed())

// Gateway should contain HTTPRoute RLP in backreference
Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed())
serialized, err := json.Marshal(rlpKey)
Expect(err).ToNot(HaveOccurred())
Expect(existingGateway.GetAnnotations()).To(HaveKey(routeRLP.BackReferenceAnnotationName()))
Expect(existingGateway.GetAnnotations()[routeRLP.BackReferenceAnnotationName()]).To(ContainSubstring(string(serialized)))

}, SpecTimeout(time.Minute))

It("Gateway atomic override - route policy exits and then gateway policy created", func(ctx SpecContext) {
// create httproute
httpRoute := testBuildBasicHttpRoute(routeName, gwName, testNamespace, []string{"*.example.com"})
Expect(k8sClient.Create(ctx, httpRoute)).To(Succeed())
Eventually(testRouteIsAccepted(client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue())

// Create HTTPRoute RLP
routeRLP := policyFactory(func(policy *kuadrantv1beta2.RateLimitPolicy) {
policy.Name = "httproute-rlp"
policy.Spec.CommonSpec().Limits = map[string]kuadrantv1beta2.Limit{
"l1": {
Rates: []kuadrantv1beta2.Rate{
{
Limit: 10, Duration: 5, Unit: kuadrantv1beta2.TimeUnit("second"),
},
},
},
}
})
Expect(k8sClient.Create(ctx, routeRLP)).To(Succeed())
rlpKey := client.ObjectKey{Name: routeRLP.Name, Namespace: testNamespace}
Eventually(testRLPIsAccepted(rlpKey)).WithContext(ctx).Should(BeTrue())

// create GW RLP
gwRLP := policyFactory(func(policy *kuadrantv1beta2.RateLimitPolicy) {
policy.Spec.TargetRef.Kind = "Gateway"
policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gwName)
policy.Spec.Defaults = nil
policy.Spec.Overrides = &kuadrantv1beta2.RateLimitPolicyCommonSpec{
Limits: map[string]kuadrantv1beta2.Limit{
"l1": {
Rates: []kuadrantv1beta2.Rate{
{
Limit: 1, Duration: 3, Unit: kuadrantv1beta2.TimeUnit("minute"),
},
},
},
},
}
})
Expect(k8sClient.Create(ctx, gwRLP)).To(Succeed())
rlpKey = client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace}
Eventually(testRLPIsAccepted(rlpKey)).WithContext(ctx).Should(BeTrue())

// Check Gateway direct back reference
gwKey := client.ObjectKeyFromObject(gateway)
existingGateway := &gatewayapiv1.Gateway{}
Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed())
Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(
gwRLP.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(gwRLP).String()))

Eventually(func(g Gomega) {
// check limits - should contain override values
limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace}
existingLimitador := &limitadorv1alpha1.Limitador{}
Expect(k8sClient.Get(ctx, limitadorKey, existingLimitador)).To(Succeed())
Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{
MaxValue: 1,
Seconds: 180,
Namespace: rlptools.LimitsNamespaceFromRLP(routeRLP),
Conditions: []string{`limit.l1__2804bad6 == "1"`},
Variables: []string{},
Name: rlptools.LimitsNameFromRLP(routeRLP),
}))
})

// Gateway should contain HTTPRoute RLP in backreference
Expect(k8sClient.Get(ctx, gwKey, existingGateway)).To(Succeed())
serialized, err := json.Marshal(rlpKey)
Expect(err).ToNot(HaveOccurred())
Expect(existingGateway.GetAnnotations()).To(HaveKey(routeRLP.BackReferenceAnnotationName()))
Expect(existingGateway.GetAnnotations()[routeRLP.BackReferenceAnnotationName()]).To(ContainSubstring(string(serialized)))

}, SpecTimeout(time.Minute))

It("Gateway atomic override - gateway override added later on", func(ctx SpecContext) {
// create httproute
httpRoute := testBuildBasicHttpRoute(routeName, gwName, testNamespace, []string{"*.example.com"})
Expect(k8sClient.Create(ctx, httpRoute)).To(Succeed())
Eventually(testRouteIsAccepted(client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue())

// Create HTTPRoute RLP
routeRLP := policyFactory(func(policy *kuadrantv1beta2.RateLimitPolicy) {
policy.Name = "httproute-rlp"
policy.Spec.CommonSpec().Limits = map[string]kuadrantv1beta2.Limit{
"l1": {
Rates: []kuadrantv1beta2.Rate{
{
Limit: 10, Duration: 5, Unit: kuadrantv1beta2.TimeUnit("second"),
},
},
},
}
})
Expect(k8sClient.Create(ctx, routeRLP)).To(Succeed())
rlpKey := client.ObjectKey{Name: routeRLP.Name, Namespace: testNamespace}
Eventually(testRLPIsAccepted(rlpKey)).WithContext(ctx).Should(BeTrue())

// create GW RLP
gwRLP := policyFactory(func(policy *kuadrantv1beta2.RateLimitPolicy) {
policy.Spec.TargetRef.Kind = "Gateway"
policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gwName)
policy.Spec.Defaults = &kuadrantv1beta2.RateLimitPolicyCommonSpec{
Limits: map[string]kuadrantv1beta2.Limit{
"l1": {
Rates: []kuadrantv1beta2.Rate{
{
Limit: 1, Duration: 3, Unit: kuadrantv1beta2.TimeUnit("minute"),
},
},
},
},
}
})
Expect(k8sClient.Create(ctx, gwRLP)).To(Succeed())
rlpKey = client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace}
Eventually(testRLPIsAccepted(rlpKey)).WithContext(ctx).Should(BeTrue())

// check limits - should contain override values
limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace}
existingLimitador := &limitadorv1alpha1.Limitador{}
Expect(k8sClient.Get(ctx, limitadorKey, existingLimitador)).To(Succeed())
Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{
MaxValue: 10,
Seconds: 5,
Namespace: rlptools.LimitsNamespaceFromRLP(routeRLP),
Conditions: []string{`limit.l1__2804bad6 == "1"`},
Variables: []string{},
Name: rlptools.LimitsNameFromRLP(routeRLP),
}))

updatedGRLP := &kuadrantv1beta2.RateLimitPolicy{}
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: gwRLP.Name, Namespace: testNamespace}, updatedGRLP)).To(Succeed())
Eventually(func(g Gomega) {
updatedGRLP.Spec.Overrides = updatedGRLP.Spec.Defaults.DeepCopy()
updatedGRLP.Spec.Defaults = nil
g.Expect(k8sClient.Update(ctx, updatedGRLP)).To(Succeed())
}).WithContext(ctx).Should(Succeed())

Eventually(func(g Gomega) {
// check limits - should contain override values
limitadorKey = client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace}
existingLimitador = &limitadorv1alpha1.Limitador{}
g.Expect(k8sClient.Get(ctx, limitadorKey, existingLimitador)).To(Succeed())
g.Expect(existingLimitador.Spec.Limits).To(ContainElements(limitadorv1alpha1.RateLimit{
MaxValue: 1,
Seconds: 180,
Namespace: rlptools.LimitsNamespaceFromRLP(routeRLP),
Conditions: []string{`limit.l1__2804bad6 == "1"`},
Variables: []string{},
Name: rlptools.LimitsNameFromRLP(routeRLP),
}))
})
}, SpecTimeout(time.Minute))
})

Context("RLP accepted condition reasons", func() {
assertAcceptedConditionFalse := func(rlp *kuadrantv1beta2.RateLimitPolicy, reason, message string) func() bool {
return func() bool {
Expand Down
46 changes: 39 additions & 7 deletions controllers/ratelimitpolicy_limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@ import (
"github.com/go-logr/logr"
limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"

kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2"
"github.com/kuadrant/kuadrant-operator/pkg/common"
"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 *kuadrantv1beta2.RateLimitPolicy) error {
func (r *RateLimitPolicyReconciler) reconcileLimits(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, targetNetworkObject client.Object) error {
rlpRefs, err := r.TargetRefReconciler.GetAllGatewayPolicyRefs(ctx, rlp)
if err != nil {
return err
}
return r.reconcileLimitador(ctx, rlp, append(rlpRefs, client.ObjectKeyFromObject(rlp)))
return r.reconcileLimitador(ctx, rlp, append(rlpRefs, client.ObjectKeyFromObject(rlp)), targetNetworkObject)
}

func (r *RateLimitPolicyReconciler) deleteLimits(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy) error {
func (r *RateLimitPolicyReconciler) deleteLimits(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, targetNetworkObject client.Object) error {
rlpRefs, err := r.TargetRefReconciler.GetAllGatewayPolicyRefs(ctx, rlp)
if err != nil {
return err
Expand All @@ -32,14 +34,14 @@ func (r *RateLimitPolicyReconciler) deleteLimits(ctx context.Context, rlp *kuadr
return rlpRef.Name != rlp.Name || rlpRef.Namespace != rlp.Namespace
})

return r.reconcileLimitador(ctx, rlp, rlpRefsWithoutRLP)
return r.reconcileLimitador(ctx, rlp, rlpRefsWithoutRLP, targetNetworkObject)
}

func (r *RateLimitPolicyReconciler) reconcileLimitador(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, rlpRefs []client.ObjectKey) error {
func (r *RateLimitPolicyReconciler) reconcileLimitador(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, rlpRefs []client.ObjectKey, targetNetworkObject client.Object) error {
logger, _ := logr.FromContext(ctx)
logger = logger.WithName("reconcileLimitador").WithValues("rlp refs", utils.Map(rlpRefs, func(ref client.ObjectKey) string { return ref.String() }))

rateLimitIndex, err := r.buildRateLimitIndex(ctx, rlpRefs)
rateLimitIndex, err := r.buildRateLimitIndex(ctx, rlpRefs, targetNetworkObject)
if err != nil {
return err
}
Expand Down Expand Up @@ -87,7 +89,7 @@ func (r *RateLimitPolicyReconciler) reconcileLimitador(ctx context.Context, rlp
return nil
}

func (r *RateLimitPolicyReconciler) buildRateLimitIndex(ctx context.Context, rlpRefs []client.ObjectKey) (*rlptools.RateLimitIndex, error) {
func (r *RateLimitPolicyReconciler) buildRateLimitIndex(ctx context.Context, rlpRefs []client.ObjectKey, targetNetworkObject client.Object) (*rlptools.RateLimitIndex, error) {
logger, _ := logr.FromContext(ctx)
logger = logger.WithName("buildRateLimitIndex").WithValues("ratelimitpolicies", rlpRefs)

Expand All @@ -105,8 +107,38 @@ func (r *RateLimitPolicyReconciler) buildRateLimitIndex(ctx context.Context, rlp
return nil, err
}

if err := r.applyOverrides(ctx, rlp, targetNetworkObject); err != nil {
return nil, err
}

rateLimitIndex.Set(rlpKey, rlptools.LimitadorRateLimitsFromRLP(rlp))
}

return rateLimitIndex, nil
}

// applyOverrides checks for any overrides set for the RateLimitPolicy.
// It iterates through the RateLimitPolicyList to find overrides for the provided target HTTPRoute.
// If an override is found, it updates the limits in the RateLimitPolicySpec in accordingly.
func (r *RateLimitPolicyReconciler) applyOverrides(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, targetNetworkObject client.Object) error {
if route, ok := targetNetworkObject.(*gatewayapiv1.HTTPRoute); ok {
rlpList := &kuadrantv1beta2.RateLimitPolicyList{}
if err := r.Client().List(ctx, rlpList); err != nil {
return err
}

for _, p := range rlpList.Items {
clientKeys := gatewayapi.GetRouteAcceptedGatewayParentKeys(route)
for _, clientKey := range clientKeys {
if gatewayapi.IsTargetRefGateway(p.GetTargetRef()) &&
clientKey.Name == string(p.Spec.TargetRef.Name) && clientKey.Namespace == p.Namespace {
if p.Spec.Overrides != nil {
rlp.Spec.CommonSpec().Limits = p.Spec.Overrides.Limits
}
}
}
}
}

return nil
}

0 comments on commit 3ad3f9c

Please sign in to comment.