@@ -17,7 +17,10 @@ limitations under the License.
17
17
package v1beta3
18
18
19
19
import (
20
+ "time"
21
+
20
22
"github.com/kuadrant/policy-machinery/machinery"
23
+ "github.com/samber/lo"
21
24
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
25
"k8s.io/apimachinery/pkg/runtime/schema"
23
26
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -30,14 +33,6 @@ import (
30
33
)
31
34
32
35
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
-
41
36
// TODO: remove after fixing the integration tests that still depend on these
42
37
RateLimitPolicyBackReferenceAnnotationName = "kuadrant.io/ratelimitpolicies"
43
38
RateLimitPolicyDirectReferenceAnnotationName = "kuadrant.io/ratelimitpolicy"
@@ -46,6 +41,9 @@ const (
46
41
var (
47
42
RateLimitPolicyGroupKind = schema.GroupKind {Group : SchemeGroupVersion .Group , Kind : "RateLimitPolicy" }
48
43
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_###"
49
47
)
50
48
51
49
// +kubebuilder:object:root=true
@@ -123,6 +121,13 @@ func (p *RateLimitPolicy) Rules() map[string]kuadrantv1.MergeableRule {
123
121
rules := make (map [string ]kuadrantv1.MergeableRule )
124
122
policyLocator := p .GetLocator ()
125
123
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
+
126
131
for ruleID := range p .Spec .Proper ().Limits {
127
132
limit := p .Spec .Proper ().Limits [ruleID ]
128
133
rules [ruleID ] = kuadrantv1 .NewMergeableRule (& limit , policyLocator )
@@ -134,13 +139,18 @@ func (p *RateLimitPolicy) Rules() map[string]kuadrantv1.MergeableRule {
134
139
func (p * RateLimitPolicy ) SetRules (rules map [string ]kuadrantv1.MergeableRule ) {
135
140
// clear all rules of the policy before setting new ones
136
141
p .Spec .Proper ().Limits = nil
142
+ p .Spec .Proper ().When = nil
137
143
138
144
if len (rules ) > 0 {
139
145
p .Spec .Proper ().Limits = make (map [string ]Limit )
140
146
}
141
147
142
148
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
+ }
144
154
}
145
155
}
146
156
@@ -226,22 +236,85 @@ type MergeableRateLimitPolicySpec struct {
226
236
227
237
// RateLimitPolicySpecProper contains common shared fields for defaults and overrides
228
238
type RateLimitPolicySpecProper struct {
239
+ // When holds a list of "top-level" `Predicate`s
240
+ // +optional
241
+ When WhenPredicates `json:"when,omitempty"`
242
+
229
243
// Limits holds the struct of limits indexed by a unique name
230
244
// +optional
231
245
Limits map [string ]Limit `json:"limits,omitempty"`
232
246
}
233
247
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
+
234
307
// Limit represents a complete rate limit configuration
235
308
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
237
310
// Called also "soft" conditions as route selectors must also match
238
311
// +optional
239
- When [] WhenCondition `json:"when,omitempty"`
312
+ When WhenPredicates `json:"when,omitempty"`
240
313
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
242
315
// TODO Document properly "Well-known selector" https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
243
316
// +optional
244
- Counters []ContextSelector `json:"counters,omitempty"`
317
+ Counters []Counter `json:"counters,omitempty"`
245
318
246
319
// Rates holds the list of limit rates
247
320
// +optional
@@ -255,7 +328,7 @@ func (l Limit) CountersAsStringList() []string {
255
328
if len (l .Counters ) == 0 {
256
329
return nil
257
330
}
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 ) })
259
332
}
260
333
261
334
var _ kuadrantv1.MergeableRule = & Limit {}
@@ -273,41 +346,34 @@ func (l *Limit) WithSource(source string) kuadrantv1.MergeableRule {
273
346
return l
274
347
}
275
348
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
+ }
278
360
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 ())
284
362
}
285
363
286
364
// Rate defines the actual rate limit that will be used when there is a match
287
365
type Rate struct {
288
366
// Limit defines the max value allowed for a given period of time
289
367
Limit int `json:"limit"`
290
368
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"`
297
371
}
298
372
299
373
// ToSeconds converts the rate to to Limitador's Limit format (maxValue, seconds)
300
374
func (r Rate ) ToSeconds () (maxValue , seconds int ) {
301
375
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 ()
311
377
312
378
if r .Limit < 0 {
313
379
maxValue = 0
@@ -316,32 +382,14 @@ func (r Rate) ToSeconds() (maxValue, seconds int) {
316
382
return
317
383
}
318
384
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
335
387
// Attributes: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes
336
388
// Well-known selectors: https://github.com/Kuadrant/architecture/blob/main/rfcs/0001-rlp-v2.md#well-known-selectors
337
389
// They are named by a dot-separated path (e.g. request.path)
338
390
// Example: "request.path" -> The path portion of the URL
339
391
// +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
345
393
346
394
type RateLimitPolicyStatus struct {
347
395
// ObservedGeneration reflects the generation of the most recently observed spec.
0 commit comments