Skip to content

Commit 53b621d

Browse files
Aias00claude
authored andcommitted
feat: add optional name field to RateLimitRule for improved observability
This enhancement allows users to provide meaningful names for rate limit rules, making rate limit keys more identifiable in metrics and dashboards. API Changes: - Add optional 'name' field to RateLimitRule with validation - Pattern: [a-zA-Z0-9._-]+ (alphanumeric, dots, underscores, hyphens) - Max length: 64 characters - Cannot be purely numeric (CEL validation at field level) - Must be unique within a policy (CEL validation at Rules array level) IR Translation: - When name is set: <policy-ns>/<policy-name>/rule/<rule-name> - When name is not set: <policy-ns>/<policy-name>/rule/<rule-index> (existing behavior) Testing: - Add test case for named rules (including shared rules) - Add CEL validation tests for valid names, duplicates, and numeric names - All existing tests continue to pass Benefits: - Improved observability: meaningful keys in metrics/dashboards - Stability: keys unchanged when rule ordering changes - Backward compatible: optional field preserves existing behavior Resolves: #9151 Signed-off-by: liuhy <liuhongyu@apache.org> Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2874485 commit 53b621d

18 files changed

Lines changed: 1063 additions & 5 deletions

api/v1alpha1/ratelimit_types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type GlobalRateLimit struct {
5555
// to rate limit the request.
5656
//
5757
// +kubebuilder:validation:MaxItems=256
58+
// +kubebuilder:validation:XValidation:rule="self.all(r, !has(r.name) || self.filter(r2, has(r2.name) && r2.name == r.name).size() == 1)", message="rate limit rule names must be unique within the global rules slice"
5859
Rules []RateLimitRule `json:"rules"`
5960
}
6061

@@ -69,6 +70,7 @@ type LocalRateLimit struct {
6970
// +kubebuilder:validation:MaxItems=16
7071
// +kubebuilder:validation:XValidation:rule="self.all(r, !has(r.cost) || !has(r.cost.response))", message="response cost is not supported for Local Rate Limits"
7172
// +kubebuilder:validation:XValidation:rule="self.all(r, !has(r.limit.fromMetadata))", message="limit fromMetadata is not supported for Local Rate Limits"
73+
// +kubebuilder:validation:XValidation:rule="self.all(r, !has(r.name) || self.filter(r2, has(r2.name) && r2.name == r.name).size() == 1)", message="rate limit rule names must be unique within the local rules slice"
7274
Rules []RateLimitRule `json:"rules"`
7375
}
7476

@@ -92,6 +94,22 @@ const (
9294
// RateLimitRule defines the semantics for matching attributes
9395
// from the incoming requests, and setting limits for them.
9496
type RateLimitRule struct {
97+
// Name is a user-facing name for this rule that can be used for debugging
98+
// and observability. When set, the rate limit key will include this name,
99+
// making it easier to identify in metrics and dashboards.
100+
// The name must be unique within a policy and should be a stable identifier
101+
// that won't change when the rule order changes.
102+
//
103+
// When name is set, the rate limit key format becomes:
104+
// <policy-namespace>/<policy-name>/rule/<rule-name>
105+
// When name is not set, the format remains:
106+
// <policy-namespace>/<policy-name>/rule/<rule-index>
107+
//
108+
// +optional
109+
// +kubebuilder:validation:MaxLength=64
110+
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9._-]+$`
111+
// +kubebuilder:validation:XValidation:rule="!self.matches('^[0-9]+$')", message="rate limit rule name must not be purely numeric to avoid collision with auto-generated index-based rule names"
112+
Name *string `json:"name,omitempty"`
95113
// ClientSelectors holds the list of select conditions to select
96114
// specific clients using attributes from the traffic flow.
97115
// All individual select conditions must hold True for this rule

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1847,6 +1847,26 @@ spec:
18471847
- requests
18481848
- unit
18491849
type: object
1850+
name:
1851+
description: |-
1852+
Name is a user-facing name for this rule that can be used for debugging
1853+
and observability. When set, the rate limit key will include this name,
1854+
making it easier to identify in metrics and dashboards.
1855+
The name must be unique within a policy and should be a stable identifier
1856+
that won't change when the rule order changes.
1857+
1858+
When name is set, the rate limit key format becomes:
1859+
<policy-namespace>/<policy-name>/rule/<rule-name>
1860+
When name is not set, the format remains:
1861+
<policy-namespace>/<policy-name>/rule/<rule-index>
1862+
maxLength: 64
1863+
pattern: ^[a-zA-Z0-9._-]+$
1864+
type: string
1865+
x-kubernetes-validations:
1866+
- message: rate limit rule name must not be purely numeric
1867+
to avoid collision with auto-generated index-based
1868+
rule names
1869+
rule: '!self.matches(''^[0-9]+$'')'
18501870
shadowMode:
18511871
description: |-
18521872
ShadowMode indicates whether this rate-limit rule runs in shadow mode.
@@ -1876,6 +1896,11 @@ spec:
18761896
type: object
18771897
maxItems: 256
18781898
type: array
1899+
x-kubernetes-validations:
1900+
- message: rate limit rule names must be unique within the
1901+
global rules slice
1902+
rule: self.all(r, !has(r.name) || self.filter(r2, has(r2.name)
1903+
&& r2.name == r.name).size() == 1)
18791904
required:
18801905
- rules
18811906
type: object
@@ -2258,6 +2283,26 @@ spec:
22582283
- requests
22592284
- unit
22602285
type: object
2286+
name:
2287+
description: |-
2288+
Name is a user-facing name for this rule that can be used for debugging
2289+
and observability. When set, the rate limit key will include this name,
2290+
making it easier to identify in metrics and dashboards.
2291+
The name must be unique within a policy and should be a stable identifier
2292+
that won't change when the rule order changes.
2293+
2294+
When name is set, the rate limit key format becomes:
2295+
<policy-namespace>/<policy-name>/rule/<rule-name>
2296+
When name is not set, the format remains:
2297+
<policy-namespace>/<policy-name>/rule/<rule-index>
2298+
maxLength: 64
2299+
pattern: ^[a-zA-Z0-9._-]+$
2300+
type: string
2301+
x-kubernetes-validations:
2302+
- message: rate limit rule name must not be purely numeric
2303+
to avoid collision with auto-generated index-based
2304+
rule names
2305+
rule: '!self.matches(''^[0-9]+$'')'
22612306
shadowMode:
22622307
description: |-
22632308
ShadowMode indicates whether this rate-limit rule runs in shadow mode.
@@ -2293,6 +2338,10 @@ spec:
22932338
- message: limit fromMetadata is not supported for Local Rate
22942339
Limits
22952340
rule: self.all(r, !has(r.limit.fromMetadata))
2341+
- message: rate limit rule names must be unique within the
2342+
local rules slice
2343+
rule: self.all(r, !has(r.name) || self.filter(r2, has(r2.name)
2344+
&& r2.name == r.name).size() == 1)
22962345
type: object
22972346
type:
22982347
description: |-

charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,26 @@ spec:
18461846
- requests
18471847
- unit
18481848
type: object
1849+
name:
1850+
description: |-
1851+
Name is a user-facing name for this rule that can be used for debugging
1852+
and observability. When set, the rate limit key will include this name,
1853+
making it easier to identify in metrics and dashboards.
1854+
The name must be unique within a policy and should be a stable identifier
1855+
that won't change when the rule order changes.
1856+
1857+
When name is set, the rate limit key format becomes:
1858+
<policy-namespace>/<policy-name>/rule/<rule-name>
1859+
When name is not set, the format remains:
1860+
<policy-namespace>/<policy-name>/rule/<rule-index>
1861+
maxLength: 64
1862+
pattern: ^[a-zA-Z0-9._-]+$
1863+
type: string
1864+
x-kubernetes-validations:
1865+
- message: rate limit rule name must not be purely numeric
1866+
to avoid collision with auto-generated index-based
1867+
rule names
1868+
rule: '!self.matches(''^[0-9]+$'')'
18491869
shadowMode:
18501870
description: |-
18511871
ShadowMode indicates whether this rate-limit rule runs in shadow mode.
@@ -1875,6 +1895,11 @@ spec:
18751895
type: object
18761896
maxItems: 256
18771897
type: array
1898+
x-kubernetes-validations:
1899+
- message: rate limit rule names must be unique within the
1900+
global rules slice
1901+
rule: self.all(r, !has(r.name) || self.filter(r2, has(r2.name)
1902+
&& r2.name == r.name).size() == 1)
18781903
required:
18791904
- rules
18801905
type: object
@@ -2257,6 +2282,26 @@ spec:
22572282
- requests
22582283
- unit
22592284
type: object
2285+
name:
2286+
description: |-
2287+
Name is a user-facing name for this rule that can be used for debugging
2288+
and observability. When set, the rate limit key will include this name,
2289+
making it easier to identify in metrics and dashboards.
2290+
The name must be unique within a policy and should be a stable identifier
2291+
that won't change when the rule order changes.
2292+
2293+
When name is set, the rate limit key format becomes:
2294+
<policy-namespace>/<policy-name>/rule/<rule-name>
2295+
When name is not set, the format remains:
2296+
<policy-namespace>/<policy-name>/rule/<rule-index>
2297+
maxLength: 64
2298+
pattern: ^[a-zA-Z0-9._-]+$
2299+
type: string
2300+
x-kubernetes-validations:
2301+
- message: rate limit rule name must not be purely numeric
2302+
to avoid collision with auto-generated index-based
2303+
rule names
2304+
rule: '!self.matches(''^[0-9]+$'')'
22602305
shadowMode:
22612306
description: |-
22622307
ShadowMode indicates whether this rate-limit rule runs in shadow mode.
@@ -2292,6 +2337,10 @@ spec:
22922337
- message: limit fromMetadata is not supported for Local Rate
22932338
Limits
22942339
rule: self.all(r, !has(r.limit.fromMetadata))
2340+
- message: rate limit rule names must be unique within the
2341+
local rules slice
2342+
rule: self.all(r, !has(r.name) || self.filter(r2, has(r2.name)
2343+
&& r2.name == r.name).size() == 1)
22952344
type: object
22962345
type:
22972346
description: |-

internal/gatewayapi/backendtrafficpolicy.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,8 +1422,8 @@ func (t *Translator) buildLocalRateLimit(policy *egv1a1.BackendTrafficPolicy) (*
14221422
if err != nil {
14231423
return nil, err
14241424
}
1425-
// Set the Name field as <policy-ns>/<policy-name>/rule/<rule-index>
1426-
irRule.Name = irRuleName(policy.Namespace, policy.Name, i)
1425+
// Set the Name field as <policy-ns>/<policy-name>/rule/<rule-name-or-index>
1426+
irRule.Name = irRuleName(policy.Namespace, policy.Name, i, rule.Name)
14271427
irRules = append(irRules, irRule)
14281428
}
14291429

@@ -1460,8 +1460,8 @@ func (t *Translator) buildGlobalRateLimit(policy *egv1a1.BackendTrafficPolicy) (
14601460
if err != nil {
14611461
return nil, err
14621462
}
1463-
// Set the Name field as <policy-ns>/<policy-name>/rule/<rule-index>
1464-
irRules[i].Name = irRuleName(policy.Namespace, policy.Name, i)
1463+
// Set the Name field as <policy-ns>/<policy-name>/rule/<rule-name-or-index>
1464+
irRules[i].Name = irRuleName(policy.Namespace, policy.Name, i, rule.Name)
14651465
}
14661466

14671467
return rateLimit, nil

internal/gatewayapi/helpers.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,10 @@ func irDestinationSettingName(destName string, backendIdx int) string {
517517
return fmt.Sprintf("%s/backend/%d", destName, backendIdx)
518518
}
519519

520-
func irRuleName(policyNamespace, policyName string, ruleIndex int) string {
520+
func irRuleName(policyNamespace, policyName string, ruleIndex int, ruleName *string) string {
521+
if ruleName != nil && *ruleName != "" {
522+
return fmt.Sprintf("%s/%s/rule/%s", policyNamespace, policyName, *ruleName)
523+
}
521524
return fmt.Sprintf("%s/%s/rule/%d", policyNamespace, policyName, ruleIndex)
522525
}
523526

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
gateways:
2+
- apiVersion: gateway.networking.k8s.io/v1
3+
kind: Gateway
4+
metadata:
5+
namespace: envoy-gateway
6+
name: gateway-1
7+
spec:
8+
gatewayClassName: envoy-gateway-class
9+
listeners:
10+
- name: http
11+
protocol: HTTP
12+
port: 80
13+
allowedRoutes:
14+
namespaces:
15+
from: All
16+
httpRoutes:
17+
- apiVersion: gateway.networking.k8s.io/v1
18+
kind: HTTPRoute
19+
metadata:
20+
namespace: default
21+
name: httproute-1
22+
spec:
23+
hostnames:
24+
- gateway.envoyproxy.io
25+
parentRefs:
26+
- namespace: envoy-gateway
27+
name: gateway-1
28+
sectionName: http
29+
rules:
30+
- matches:
31+
- path:
32+
value: "/"
33+
backendRefs:
34+
- name: service-1
35+
port: 8080
36+
backendTrafficPolicies:
37+
- apiVersion: gateway.envoyproxy.io/v1alpha1
38+
kind: BackendTrafficPolicy
39+
metadata:
40+
namespace: default
41+
name: policy-for-route
42+
spec:
43+
targetRef:
44+
group: gateway.networking.k8s.io
45+
kind: HTTPRoute
46+
name: httproute-1
47+
rateLimit:
48+
type: Global
49+
global:
50+
rules:
51+
# Named shared rule -> key uses the name: default/policy-for-route/rule/by-ip
52+
- name: by-ip
53+
clientSelectors:
54+
- sourceCIDR:
55+
type: "Distinct"
56+
value: "0.0.0.0/0"
57+
limit:
58+
requests: 20
59+
unit: Hour
60+
shared: true
61+
# Named non-shared rule -> key uses the name: default/policy-for-route/rule/by-org
62+
- name: by-org
63+
clientSelectors:
64+
- headers:
65+
- name: x-org-id
66+
type: Distinct
67+
limit:
68+
requests: 30
69+
unit: Hour
70+
# Unnamed rule -> key falls back to the index: default/policy-for-route/rule/2
71+
- clientSelectors:
72+
- headers:
73+
- name: x-team
74+
value: blue
75+
limit:
76+
requests: 40
77+
unit: Hour

0 commit comments

Comments
 (0)