Skip to content

Commit a925273

Browse files
authored
951 finalize ratelimitpolicy v1beta3 (#976)
* Finalize ratelimitpolicy v1beta3 Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * ratelimit_workflow_test.go: fix and add new unittests Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * update helm charts· Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * ratelimitpolicy v1beta3 counter expression Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * fix predicate from HTTPRouteMatch Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * fix e2e tests Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * RLP Duration as GwAPI Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * predicates as full object and window name for limit duration Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * fix rebase issues Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * ratelimitpolicy_types: fix typo in a comment Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * move WhenCondition type to authpolicy type Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * update bundle and fix e2e tests Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * new rate limit policy doc Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * fix e2e tests Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * bundle update Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * change tests to increase coverage Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * limits reconciler needs to filter out top level rules Signed-off-by: Eguzki Astiz Lezaun <[email protected]> * no upperlimit for predicates and expressions strings Signed-off-by: Eguzki Astiz Lezaun <[email protected]> --------- Signed-off-by: Eguzki Astiz Lezaun <[email protected]>
1 parent 3b8e313 commit a925273

32 files changed

+1012
-1071
lines changed

api/v1beta3/authpolicy_types.go

+37
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,43 @@ const (
4242
AuthPolicyDirectReferenceAnnotationName = "kuadrant.io/authpolicy"
4343
)
4444

45+
const (
46+
EqualOperator WhenConditionOperator = "eq"
47+
NotEqualOperator WhenConditionOperator = "neq"
48+
StartsWithOperator WhenConditionOperator = "startsWith"
49+
EndsWithOperator WhenConditionOperator = "endsWith"
50+
IncludeOperator WhenConditionOperator = "incl"
51+
ExcludeOperator WhenConditionOperator = "excl"
52+
MatchesOperator WhenConditionOperator = "matches"
53+
)
54+
55+
// +kubebuilder:validation:Enum:=eq;neq;startswith;endswith;incl;excl;matches
56+
type WhenConditionOperator string
57+
58+
// ContextSelector defines one item from the well known attributes
59+
// Attributes: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes
60+
// Well-known selectors: https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
61+
// They are named by a dot-separated path (e.g. request.path)
62+
// Example: "request.path" -> The path portion of the URL
63+
// +kubebuilder:validation:MinLength=1
64+
// +kubebuilder:validation:MaxLength=253
65+
type ContextSelector string
66+
67+
// WhenCondition defines semantics for matching an HTTP request based on conditions
68+
// https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteSpec
69+
type WhenCondition struct {
70+
// Selector defines one item from the well known selectors
71+
// TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
72+
Selector ContextSelector `json:"selector"`
73+
74+
// The binary operator to be applied to the content fetched from the selector
75+
// Possible values are: "eq" (equal to), "neq" (not equal to)
76+
Operator WhenConditionOperator `json:"operator"`
77+
78+
// The value of reference for the comparison.
79+
Value string `json:"value"`
80+
}
81+
4582
var (
4683
AuthPolicyGroupKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "AuthPolicy"}
4784
AuthPoliciesResource = SchemeGroupVersion.WithResource("authpolicies")

api/v1beta3/ratelimitpolicy_types.go

+105-57
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ limitations under the License.
1717
package v1beta3
1818

1919
import (
20+
"time"
21+
2022
"github.com/kuadrant/policy-machinery/machinery"
23+
"github.com/samber/lo"
2124
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2225
"k8s.io/apimachinery/pkg/runtime/schema"
2326
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -30,14 +33,6 @@ import (
3033
)
3134

3235
const (
33-
EqualOperator WhenConditionOperator = "eq"
34-
NotEqualOperator WhenConditionOperator = "neq"
35-
StartsWithOperator WhenConditionOperator = "startsWith"
36-
EndsWithOperator WhenConditionOperator = "endsWith"
37-
IncludeOperator WhenConditionOperator = "incl"
38-
ExcludeOperator WhenConditionOperator = "excl"
39-
MatchesOperator WhenConditionOperator = "matches"
40-
4136
// TODO: remove after fixing the integration tests that still depend on these
4237
RateLimitPolicyBackReferenceAnnotationName = "kuadrant.io/ratelimitpolicies"
4338
RateLimitPolicyDirectReferenceAnnotationName = "kuadrant.io/ratelimitpolicy"
@@ -46,6 +41,9 @@ const (
4641
var (
4742
RateLimitPolicyGroupKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "RateLimitPolicy"}
4843
RateLimitPoliciesResource = SchemeGroupVersion.WithResource("ratelimitpolicies")
44+
// Top level predicate rules key starting with # to prevent conflict with limit names
45+
// TODO(eastizle): this coupling between limit names and rule IDs is a bad smell. Merging implementation should be enhanced.
46+
RulesKeyTopLevelPredicates = "###_TOP_LEVEL_PREDICATES_###"
4947
)
5048

5149
// +kubebuilder:object:root=true
@@ -123,6 +121,13 @@ func (p *RateLimitPolicy) Rules() map[string]kuadrantv1.MergeableRule {
123121
rules := make(map[string]kuadrantv1.MergeableRule)
124122
policyLocator := p.GetLocator()
125123

124+
if len(p.Spec.Proper().When) > 0 {
125+
rules[RulesKeyTopLevelPredicates] = kuadrantv1.NewMergeableRule(
126+
&WhenPredicatesMergeableRule{When: p.Spec.Proper().When, Source: policyLocator},
127+
policyLocator,
128+
)
129+
}
130+
126131
for ruleID := range p.Spec.Proper().Limits {
127132
limit := p.Spec.Proper().Limits[ruleID]
128133
rules[ruleID] = kuadrantv1.NewMergeableRule(&limit, policyLocator)
@@ -134,13 +139,18 @@ func (p *RateLimitPolicy) Rules() map[string]kuadrantv1.MergeableRule {
134139
func (p *RateLimitPolicy) SetRules(rules map[string]kuadrantv1.MergeableRule) {
135140
// clear all rules of the policy before setting new ones
136141
p.Spec.Proper().Limits = nil
142+
p.Spec.Proper().When = nil
137143

138144
if len(rules) > 0 {
139145
p.Spec.Proper().Limits = make(map[string]Limit)
140146
}
141147

142148
for ruleID := range rules {
143-
p.Spec.Proper().Limits[ruleID] = *rules[ruleID].(*Limit)
149+
if ruleID == RulesKeyTopLevelPredicates {
150+
p.Spec.Proper().When = rules[ruleID].(*WhenPredicatesMergeableRule).When
151+
} else {
152+
p.Spec.Proper().Limits[ruleID] = *rules[ruleID].(*Limit)
153+
}
144154
}
145155
}
146156

@@ -226,22 +236,85 @@ type MergeableRateLimitPolicySpec struct {
226236

227237
// RateLimitPolicySpecProper contains common shared fields for defaults and overrides
228238
type RateLimitPolicySpecProper struct {
239+
// When holds a list of "top-level" `Predicate`s
240+
// +optional
241+
When WhenPredicates `json:"when,omitempty"`
242+
229243
// Limits holds the struct of limits indexed by a unique name
230244
// +optional
231245
Limits map[string]Limit `json:"limits,omitempty"`
232246
}
233247

248+
// Predicate defines one CEL expression that must be evaluated to bool
249+
type Predicate struct {
250+
// +kubebuilder:validation:MinLength=1
251+
Predicate string `json:"predicate"`
252+
}
253+
254+
func NewPredicate(predicate string) Predicate {
255+
return Predicate{Predicate: predicate}
256+
}
257+
258+
type WhenPredicates []Predicate
259+
260+
func NewWhenPredicates(predicates ...string) WhenPredicates {
261+
whenPredicates := make(WhenPredicates, 0)
262+
for _, predicate := range predicates {
263+
whenPredicates = append(whenPredicates, NewPredicate(predicate))
264+
}
265+
266+
return whenPredicates
267+
}
268+
269+
func (w WhenPredicates) Extend(other WhenPredicates) WhenPredicates {
270+
return append(w, other...)
271+
}
272+
273+
func (w WhenPredicates) Into() []string {
274+
if w == nil {
275+
return nil
276+
}
277+
278+
return lo.Map(w, func(p Predicate, _ int) string { return p.Predicate })
279+
}
280+
281+
type WhenPredicatesMergeableRule struct {
282+
When WhenPredicates
283+
284+
// Source stores the locator of the policy where the limit is orignaly defined (internal use)
285+
Source string
286+
}
287+
288+
var _ kuadrantv1.MergeableRule = &WhenPredicatesMergeableRule{}
289+
290+
func (w *WhenPredicatesMergeableRule) GetSpec() any {
291+
return w.When
292+
}
293+
294+
func (w *WhenPredicatesMergeableRule) GetSource() string {
295+
return w.Source
296+
}
297+
298+
func (w *WhenPredicatesMergeableRule) WithSource(source string) kuadrantv1.MergeableRule {
299+
w.Source = source
300+
return w
301+
}
302+
303+
type Counter struct {
304+
Expression Expression `json:"expression"`
305+
}
306+
234307
// Limit represents a complete rate limit configuration
235308
type Limit struct {
236-
// When holds the list of conditions for the policy to be enforced.
309+
// When holds a list of "limit-level" `Predicate`s
237310
// Called also "soft" conditions as route selectors must also match
238311
// +optional
239-
When []WhenCondition `json:"when,omitempty"`
312+
When WhenPredicates `json:"when,omitempty"`
240313

241-
// Counters defines additional rate limit counters based on context qualifiers and well known selectors
314+
// Counters defines additional rate limit counters based on CEL expressions which can reference well known selectors
242315
// TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
243316
// +optional
244-
Counters []ContextSelector `json:"counters,omitempty"`
317+
Counters []Counter `json:"counters,omitempty"`
245318

246319
// Rates holds the list of limit rates
247320
// +optional
@@ -255,7 +328,7 @@ func (l Limit) CountersAsStringList() []string {
255328
if len(l.Counters) == 0 {
256329
return nil
257330
}
258-
return utils.Map(l.Counters, func(counter ContextSelector) string { return string(counter) })
331+
return utils.Map(l.Counters, func(counter Counter) string { return string(counter.Expression) })
259332
}
260333

261334
var _ kuadrantv1.MergeableRule = &Limit{}
@@ -273,41 +346,34 @@ func (l *Limit) WithSource(source string) kuadrantv1.MergeableRule {
273346
return l
274347
}
275348

276-
// +kubebuilder:validation:Enum:=second;minute;hour;day
277-
type TimeUnit string
349+
// Duration follows Gateway API Duration format: https://gateway-api.sigs.k8s.io/geps/gep-2257/?h=duration#gateway-api-duration-format
350+
// MUST match the regular expression ^([0-9]{1,5}(h|m|s|ms)){1,4}$
351+
// MUST be interpreted as specified by Golang's time.ParseDuration
352+
// +kubebuilder:validation:Pattern=`^([0-9]{1,5}(h|m|s|ms)){1,4}$`
353+
type Duration string
354+
355+
func (d Duration) Seconds() int {
356+
duration, err := time.ParseDuration(string(d))
357+
if err != nil {
358+
return 0
359+
}
278360

279-
var timeUnitMap = map[TimeUnit]int{
280-
TimeUnit("second"): 1,
281-
TimeUnit("minute"): 60,
282-
TimeUnit("hour"): 60 * 60,
283-
TimeUnit("day"): 60 * 60 * 24,
361+
return int(duration.Seconds())
284362
}
285363

286364
// Rate defines the actual rate limit that will be used when there is a match
287365
type Rate struct {
288366
// Limit defines the max value allowed for a given period of time
289367
Limit int `json:"limit"`
290368

291-
// Duration defines the time period for which the Limit specified above applies.
292-
Duration int `json:"duration"`
293-
294-
// Duration defines the time uni
295-
// Possible values are: "second", "minute", "hour", "day"
296-
Unit TimeUnit `json:"unit"`
369+
// Window defines the time period for which the Limit specified above applies.
370+
Window Duration `json:"window"`
297371
}
298372

299373
// ToSeconds converts the rate to to Limitador's Limit format (maxValue, seconds)
300374
func (r Rate) ToSeconds() (maxValue, seconds int) {
301375
maxValue = r.Limit
302-
seconds = 0
303-
304-
if tmpSecs, ok := timeUnitMap[r.Unit]; ok && r.Duration > 0 {
305-
seconds = tmpSecs * r.Duration
306-
}
307-
308-
if r.Duration < 0 {
309-
seconds = 0
310-
}
376+
seconds = r.Window.Seconds()
311377

312378
if r.Limit < 0 {
313379
maxValue = 0
@@ -316,32 +382,14 @@ func (r Rate) ToSeconds() (maxValue, seconds int) {
316382
return
317383
}
318384

319-
// WhenCondition defines semantics for matching an HTTP request based on conditions
320-
// https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteSpec
321-
type WhenCondition struct {
322-
// Selector defines one item from the well known selectors
323-
// TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
324-
Selector ContextSelector `json:"selector"`
325-
326-
// The binary operator to be applied to the content fetched from the selector
327-
// Possible values are: "eq" (equal to), "neq" (not equal to)
328-
Operator WhenConditionOperator `json:"operator"`
329-
330-
// The value of reference for the comparison.
331-
Value string `json:"value"`
332-
}
333-
334-
// ContextSelector defines one item from the well known attributes
385+
// Expression defines one CEL expression
386+
// Expression can use well known attributes
335387
// Attributes: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes
336388
// Well-known selectors: https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
337389
// They are named by a dot-separated path (e.g. request.path)
338390
// Example: "request.path" -> The path portion of the URL
339391
// +kubebuilder:validation:MinLength=1
340-
// +kubebuilder:validation:MaxLength=253
341-
type ContextSelector string
342-
343-
// +kubebuilder:validation:Enum:=eq;neq;startswith;endswith;incl;excl;matches
344-
type WhenConditionOperator string
392+
type Expression string
345393

346394
type RateLimitPolicyStatus struct {
347395
// ObservedGeneration reflects the generation of the most recently observed spec.

api/v1beta3/ratelimitpolicy_types_test.go

+18-42
Original file line numberDiff line numberDiff line change
@@ -14,74 +14,50 @@ func TestConvertRateIntoSeconds(t *testing.T) {
1414
expectedSeconds int
1515
}{
1616
{
17-
name: "seconds",
18-
rate: Rate{
19-
Limit: 5, Duration: 2, Unit: TimeUnit("second"),
20-
},
17+
name: "seconds",
18+
rate: Rate{Limit: 5, Window: Duration("2s")},
2119
expectedMaxValue: 5,
2220
expectedSeconds: 2,
2321
},
2422
{
25-
name: "minutes",
26-
rate: Rate{
27-
Limit: 5, Duration: 2, Unit: TimeUnit("minute"),
28-
},
23+
name: "minutes",
24+
rate: Rate{Limit: 5, Window: Duration("2m")},
2925
expectedMaxValue: 5,
3026
expectedSeconds: 2 * 60,
3127
},
3228
{
33-
name: "hours",
34-
rate: Rate{
35-
Limit: 5, Duration: 2, Unit: TimeUnit("hour"),
36-
},
29+
name: "hours",
30+
rate: Rate{Limit: 5, Window: Duration("2h")},
3731
expectedMaxValue: 5,
3832
expectedSeconds: 2 * 60 * 60,
3933
},
4034
{
41-
name: "day",
42-
rate: Rate{
43-
Limit: 5, Duration: 2, Unit: TimeUnit("day"),
44-
},
45-
expectedMaxValue: 5,
46-
expectedSeconds: 2 * 60 * 60 * 24,
35+
name: "negative limit",
36+
rate: Rate{Limit: -5, Window: Duration("2s")},
37+
expectedMaxValue: 0,
38+
expectedSeconds: 2,
4739
},
4840
{
49-
name: "negative limit",
50-
rate: Rate{
51-
Limit: -5, Duration: 2, Unit: TimeUnit("second"),
52-
},
41+
name: "limit is 0",
42+
rate: Rate{Limit: 0, Window: Duration("2s")},
5343
expectedMaxValue: 0,
5444
expectedSeconds: 2,
5545
},
5646
{
57-
name: "negative duration",
58-
rate: Rate{
59-
Limit: 5, Duration: -2, Unit: TimeUnit("second"),
60-
},
47+
name: "rate is 0",
48+
rate: Rate{Limit: 5, Window: Duration("0s")},
6149
expectedMaxValue: 5,
6250
expectedSeconds: 0,
6351
},
6452
{
65-
name: "limit is 0",
66-
rate: Rate{
67-
Limit: 0, Duration: 2, Unit: TimeUnit("second"),
68-
},
69-
expectedMaxValue: 0,
70-
expectedSeconds: 2,
71-
},
72-
{
73-
name: "rate is 0",
74-
rate: Rate{
75-
Limit: 5, Duration: 0, Unit: TimeUnit("second"),
76-
},
53+
name: "invalid duration 01",
54+
rate: Rate{Limit: 5, Window: Duration("unknown")},
7755
expectedMaxValue: 5,
7856
expectedSeconds: 0,
7957
},
8058
{
81-
name: "unexpected time unit",
82-
rate: Rate{
83-
Limit: 5, Duration: 2, Unit: TimeUnit("unknown"),
84-
},
59+
name: "invalid duration 02",
60+
rate: Rate{Limit: 5, Window: Duration("5d")},
8561
expectedMaxValue: 5,
8662
expectedSeconds: 0,
8763
},

0 commit comments

Comments
 (0)