diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs index f64bcea6ea6bd..10902aad82f86 100644 --- a/policy-controller/src/admission.rs +++ b/policy-controller/src/admission.rs @@ -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; @@ -148,6 +149,10 @@ impl Admission { return self.admit_spec::(req).await; } + if is_kind::(&req) { + return self.admit_spec::(req).await; + } + AdmissionResponse::invalid(format_args!( "unsupported resource type: {}.{}.{}", req.kind.group, req.kind.version, req.kind.kind @@ -844,3 +849,59 @@ fn validate_parent_ref_port_requirements(parent: &k8s_gateway_api::ParentReferen Ok(()) } + +#[async_trait::async_trait] +impl Validate for Admission { + async fn validate( + self, + _ns: &str, + _name: &str, + _annotations: &BTreeMap, + spec: RateLimitPolicySpec, + ) -> Result<()> { + if !spec.target_ref.targets_kind::() { + 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::() { + bail!("overrides.clientRefs must target a ServiceAccount"); + } + } + } + + Ok(()) + } +} diff --git a/policy-test/tests/admit_http_local_ratelimit_policy.rs b/policy-test/tests/admit_http_local_ratelimit_policy.rs new file mode 100644 index 0000000000000..5e4de499420be --- /dev/null +++ b/policy-test/tests/admit_http_local_ratelimit_policy.rs @@ -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 { + 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, +) -> 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), + }, + } +}