Skip to content

Commit

Permalink
HTTPLocalRateLimitPolicy validator (#13251)
Browse files Browse the repository at this point in the history
* HTTPLocalRateLimitPolicy validator

Followup to #13231
  • Loading branch information
alpeb authored Nov 12, 2024
1 parent 2760ebb commit 41b3b32
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 3 deletions.
67 changes: 64 additions & 3 deletions policy-controller/src/admission.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use super::validation;
use crate::k8s::policy::{
httproute, server::Selector, AuthorizationPolicy, AuthorizationPolicySpec, EgressNetwork,
EgressNetworkSpec, HttpRoute, HttpRouteSpec, LocalTargetRef, MeshTLSAuthentication,
MeshTLSAuthenticationSpec, NamespacedTargetRef, Network, NetworkAuthentication,
NetworkAuthenticationSpec, Server, ServerAuthorization, ServerAuthorizationSpec, ServerSpec,
EgressNetworkSpec, HTTPLocalRateLimitPolicy, HttpRoute, HttpRouteSpec, LocalTargetRef,
MeshTLSAuthentication, MeshTLSAuthenticationSpec, NamespacedTargetRef, Network,
NetworkAuthentication, NetworkAuthenticationSpec, RateLimitPolicySpec, Server,
ServerAuthorization, ServerAuthorizationSpec, ServerSpec,
};
use anyhow::{anyhow, bail, ensure, Result};
use futures::future;
Expand Down Expand Up @@ -148,6 +149,10 @@ impl Admission {
return self.admit_spec::<k8s_gateway_api::TcpRouteSpec>(req).await;
}

if is_kind::<HTTPLocalRateLimitPolicy>(&req) {
return self.admit_spec::<RateLimitPolicySpec>(req).await;
}

AdmissionResponse::invalid(format_args!(
"unsupported resource type: {}.{}.{}",
req.kind.group, req.kind.version, req.kind.kind
Expand Down Expand Up @@ -844,3 +849,59 @@ fn validate_parent_ref_port_requirements(parent: &k8s_gateway_api::ParentReferen

Ok(())
}

#[async_trait::async_trait]
impl Validate<RateLimitPolicySpec> for Admission {
async fn validate(
self,
_ns: &str,
_name: &str,
_annotations: &BTreeMap<String, String>,
spec: RateLimitPolicySpec,
) -> Result<()> {
if !spec.target_ref.targets_kind::<Server>() {
bail!(
"invalid targetRef kind: {}",
spec.target_ref.canonical_kind()
);
}

if let Some(total) = spec.total {
if total.requests_per_second == 0 {
bail!("total.requestsPerSecond must be greater than 0");
}

if let Some(ref identity) = spec.identity {
if identity.requests_per_second > total.requests_per_second {
bail!("identity.requestsPerSecond must be less than or equal to total.requestsPerSecond");
}
}

for ovr in spec.overrides.clone().unwrap_or_default().iter() {
if ovr.requests_per_second > total.requests_per_second {
bail!("override.requestsPerSecond must be less than or equal to total.requestsPerSecond");
}
}
}

if let Some(identity) = spec.identity {
if identity.requests_per_second == 0 {
bail!("identity.requestsPerSecond must be greater than 0");
}
}

for ovr in spec.overrides.unwrap_or_default().iter() {
if ovr.requests_per_second == 0 {
bail!("override.requestsPerSecond must be greater than 0");
}

for target_ref in ovr.client_refs.iter() {
if !target_ref.targets_kind::<ServiceAccount>() {
bail!("overrides.clientRefs must target a ServiceAccount");
}
}
}

Ok(())
}
}
94 changes: 94 additions & 0 deletions policy-test/tests/admit_http_local_ratelimit_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use linkerd_policy_controller_k8s_api::{
self as api,
policy::{
HTTPLocalRateLimitPolicy, Limit, LocalTargetRef, NamespacedTargetRef, Override,
RateLimitPolicySpec,
},
};
use linkerd_policy_test::admission;

#[tokio::test(flavor = "current_thread")]
async fn accepts_valid() {
admission::accepts(|ns| {
mk_ratelimiter(ns, default_target_ref(), 1000, 100, default_overrides())
})
.await;
}

#[tokio::test(flavor = "current_thread")]
async fn rejects_target_ref_deployment() {
let target_ref = LocalTargetRef {
group: Some("apps".to_string()),
kind: "Deployment".to_string(),
name: "api".to_string(),
};
admission::rejects(|ns| mk_ratelimiter(ns, target_ref, 1000, 100, default_overrides())).await;
}

#[tokio::test(flavor = "current_thread")]
async fn rejects_identity_rps_higher_than_total() {
admission::rejects(|ns| {
mk_ratelimiter(ns, default_target_ref(), 1000, 2000, default_overrides())
})
.await;
}

#[tokio::test(flavor = "current_thread")]
async fn rejects_overrides_rps_higher_than_total() {
let overrides = vec![Override {
requests_per_second: 2000,
client_refs: vec![NamespacedTargetRef {
group: Some("".to_string()),
kind: "ServiceAccount".to_string(),
name: "sa-1".to_string(),
namespace: Some("linkerd".to_string()),
}],
}];
admission::rejects(|ns| mk_ratelimiter(ns, default_target_ref(), 1000, 2000, overrides)).await;
}

fn default_target_ref() -> LocalTargetRef {
LocalTargetRef {
group: Some("policy.linkerd.io".to_string()),
kind: "Server".to_string(),
name: "api".to_string(),
}
}

fn default_overrides() -> Vec<Override> {
vec![Override {
requests_per_second: 200,
client_refs: vec![NamespacedTargetRef {
group: Some("".to_string()),
kind: "ServiceAccount".to_string(),
name: "sa-1".to_string(),
namespace: Some("linkerd".to_string()),
}],
}]
}

fn mk_ratelimiter(
namespace: String,
target_ref: LocalTargetRef,
total_rps: u32,
identity_rps: u32,
overrides: Vec<Override>,
) -> HTTPLocalRateLimitPolicy {
HTTPLocalRateLimitPolicy {
metadata: api::ObjectMeta {
namespace: Some(namespace),
name: Some("test".to_string()),
..Default::default()
},
spec: RateLimitPolicySpec {
target_ref,
total: Some(Limit {
requests_per_second: total_rps,
}),
identity: Some(Limit {
requests_per_second: identity_rps,
}),
overrides: Some(overrides),
},
}
}

0 comments on commit 41b3b32

Please sign in to comment.