Skip to content

Commit bce6139

Browse files
committed
HTTPLocalRateLimitPolicy validator
Followup to #13231
1 parent aff1aab commit bce6139

File tree

2 files changed

+158
-3
lines changed

2 files changed

+158
-3
lines changed

policy-controller/src/admission.rs

+64-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use super::validation;
22
use crate::k8s::policy::{
33
httproute, server::Selector, AuthorizationPolicy, AuthorizationPolicySpec, EgressNetwork,
4-
EgressNetworkSpec, HttpRoute, HttpRouteSpec, LocalTargetRef, MeshTLSAuthentication,
5-
MeshTLSAuthenticationSpec, NamespacedTargetRef, Network, NetworkAuthentication,
6-
NetworkAuthenticationSpec, Server, ServerAuthorization, ServerAuthorizationSpec, ServerSpec,
4+
EgressNetworkSpec, HTTPLocalRateLimitPolicy, HttpRoute, HttpRouteSpec, LocalTargetRef,
5+
MeshTLSAuthentication, MeshTLSAuthenticationSpec, NamespacedTargetRef, Network,
6+
NetworkAuthentication, NetworkAuthenticationSpec, RateLimitPolicySpec, Server,
7+
ServerAuthorization, ServerAuthorizationSpec, ServerSpec,
78
};
89
use anyhow::{anyhow, bail, ensure, Result};
910
use futures::future;
@@ -148,6 +149,10 @@ impl Admission {
148149
return self.admit_spec::<k8s_gateway_api::TcpRouteSpec>(req).await;
149150
}
150151

152+
if is_kind::<HTTPLocalRateLimitPolicy>(&req) {
153+
return self.admit_spec::<RateLimitPolicySpec>(req).await;
154+
}
155+
151156
AdmissionResponse::invalid(format_args!(
152157
"unsupported resource type: {}.{}.{}",
153158
req.kind.group, req.kind.version, req.kind.kind
@@ -844,3 +849,59 @@ fn validate_parent_ref_port_requirements(parent: &k8s_gateway_api::ParentReferen
844849

845850
Ok(())
846851
}
852+
853+
#[async_trait::async_trait]
854+
impl Validate<RateLimitPolicySpec> for Admission {
855+
async fn validate(
856+
self,
857+
_ns: &str,
858+
_name: &str,
859+
_annotations: &BTreeMap<String, String>,
860+
spec: RateLimitPolicySpec,
861+
) -> Result<()> {
862+
if !spec.target_ref.targets_kind::<Server>() {
863+
bail!(
864+
"invalid targetRef kind: {}",
865+
spec.target_ref.canonical_kind()
866+
);
867+
}
868+
869+
if let Some(total) = spec.total {
870+
if total.requests_per_second == 0 {
871+
bail!("total.requestsPerSecond must be greater than 0");
872+
}
873+
874+
if let Some(ref identity) = spec.identity {
875+
if identity.requests_per_second > total.requests_per_second {
876+
bail!("identity.requestsPerSecond must be less than or equal to total.requestsPerSecond");
877+
}
878+
}
879+
880+
for ovr in spec.overrides.iter() {
881+
if ovr.requests_per_second > total.requests_per_second {
882+
bail!("override.requestsPerSecond must be less than or equal to total.requestsPerSecond");
883+
}
884+
}
885+
}
886+
887+
if let Some(identity) = spec.identity {
888+
if identity.requests_per_second == 0 {
889+
bail!("identity.requestsPerSecond must be greater than 0");
890+
}
891+
}
892+
893+
for ovr in spec.overrides.iter() {
894+
if ovr.requests_per_second == 0 {
895+
bail!("override.requestsPerSecond must be greater than 0");
896+
}
897+
898+
for target_ref in ovr.client_refs.iter() {
899+
if target_ref.kind != "ServiceAccount" {
900+
bail!("overrides.clientRefs must target a ServiceAccount");
901+
}
902+
}
903+
}
904+
905+
Ok(())
906+
}
907+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use linkerd_policy_controller_k8s_api::{
2+
self as api,
3+
policy::{
4+
HTTPLocalRateLimitPolicy, Limit, LocalTargetRef, NamespacedTargetRef, Override,
5+
RateLimitPolicySpec,
6+
},
7+
};
8+
use linkerd_policy_test::admission;
9+
10+
#[tokio::test(flavor = "current_thread")]
11+
async fn accepts_valid() {
12+
admission::accepts(|ns| {
13+
mk_ratelimiter(ns, default_target_ref(), 1000, 100, default_overrides())
14+
})
15+
.await;
16+
}
17+
18+
#[tokio::test(flavor = "current_thread")]
19+
async fn rejects_target_ref_deployment() {
20+
let target_ref = LocalTargetRef {
21+
group: Some("apps".to_string()),
22+
kind: "Deployment".to_string(),
23+
name: "api".to_string(),
24+
};
25+
admission::rejects(|ns| mk_ratelimiter(ns, target_ref, 1000, 100, default_overrides())).await;
26+
}
27+
28+
#[tokio::test(flavor = "current_thread")]
29+
async fn rejects_identity_rps_higher_than_total() {
30+
admission::rejects(|ns| {
31+
mk_ratelimiter(ns, default_target_ref(), 1000, 2000, default_overrides())
32+
})
33+
.await;
34+
}
35+
36+
#[tokio::test(flavor = "current_thread")]
37+
async fn rejects_overrides_rps_higher_than_total() {
38+
let overrides = vec![Override {
39+
requests_per_second: 2000,
40+
client_refs: vec![NamespacedTargetRef {
41+
group: Some("".to_string()),
42+
kind: "ServiceAccount".to_string(),
43+
name: "sa-1".to_string(),
44+
namespace: Some("linkerd".to_string()),
45+
}],
46+
}];
47+
admission::rejects(|ns| mk_ratelimiter(ns, default_target_ref(), 1000, 2000, overrides)).await;
48+
}
49+
50+
fn default_target_ref() -> LocalTargetRef {
51+
LocalTargetRef {
52+
group: Some("policy.linkerd.io".to_string()),
53+
kind: "Server".to_string(),
54+
name: "api".to_string(),
55+
}
56+
}
57+
58+
fn default_overrides() -> Vec<Override> {
59+
vec![Override {
60+
requests_per_second: 200,
61+
client_refs: vec![NamespacedTargetRef {
62+
group: Some("".to_string()),
63+
kind: "ServiceAccount".to_string(),
64+
name: "sa-1".to_string(),
65+
namespace: Some("linkerd".to_string()),
66+
}],
67+
}]
68+
}
69+
70+
fn mk_ratelimiter(
71+
namespace: String,
72+
target_ref: LocalTargetRef,
73+
total_rps: u32,
74+
identity_rps: u32,
75+
overrides: Vec<Override>,
76+
) -> HTTPLocalRateLimitPolicy {
77+
HTTPLocalRateLimitPolicy {
78+
metadata: api::ObjectMeta {
79+
namespace: Some(namespace),
80+
name: Some("test".to_string()),
81+
..Default::default()
82+
},
83+
spec: RateLimitPolicySpec {
84+
target_ref,
85+
total: Some(Limit {
86+
requests_per_second: total_rps,
87+
}),
88+
identity: Some(Limit {
89+
requests_per_second: identity_rps,
90+
}),
91+
overrides,
92+
},
93+
}
94+
}

0 commit comments

Comments
 (0)