use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdmissionResult {
pub allowed: bool,
pub reason: Option<String>,
pub warnings: Vec<String>,
pub patch: Option<serde_json::Value>,
pub patch_type: Option<String>,
}
impl AdmissionResult {
pub fn allowed() -> Self {
Self {
allowed: true,
reason: None,
warnings: Vec::new(),
patch: None,
patch_type: None,
}
}
pub fn denied(reason: impl Into<String>) -> Self {
Self {
allowed: false,
reason: Some(reason.into()),
warnings: Vec::new(),
patch: None,
patch_type: None,
}
}
pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
self.warnings.push(warning.into());
self
}
pub fn with_patch(mut self, patch: serde_json::Value) -> Self {
self.patch = Some(patch);
self.patch_type = Some("JSONPatch".to_string());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdmissionRequest {
pub uid: String,
pub operation: Operation,
pub kind: String,
pub namespace: Option<String>,
pub name: Option<String>,
pub object: Option<serde_json::Value>,
pub old_object: Option<serde_json::Value>,
pub user_info: UserInfo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Operation {
Create,
Update,
Delete,
Connect,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
pub username: String,
pub uid: Option<String>,
pub groups: Vec<String>,
}
impl Default for UserInfo {
fn default() -> Self {
Self {
username: "system:anonymous".to_string(),
uid: None,
groups: vec!["system:unauthenticated".to_string()],
}
}
}
pub trait AdmissionController: Send + Sync {
fn name(&self) -> &str;
fn handles(&self, kind: &str, operation: Operation) -> bool;
fn validate(&self, request: &AdmissionRequest) -> AdmissionResult;
fn mutate(&self, _request: &AdmissionRequest) -> Option<serde_json::Value> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWebhook {
pub name: String,
pub url: String,
pub failure_policy: FailurePolicy,
pub rules: Vec<WebhookRule>,
pub timeout_seconds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutatingWebhook {
pub name: String,
pub url: String,
pub failure_policy: FailurePolicy,
pub rules: Vec<WebhookRule>,
pub timeout_seconds: u32,
pub reinvocation_policy: ReinvocationPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FailurePolicy {
Fail,
Ignore,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReinvocationPolicy {
Never,
IfNeeded,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookRule {
pub api_groups: Vec<String>,
pub api_versions: Vec<String>,
pub resources: Vec<String>,
pub operations: Vec<Operation>,
}
pub struct ResourceQuotaAdmission {
quotas: std::collections::HashMap<String, ResourceQuota>,
}
#[derive(Debug, Clone)]
pub struct ResourceQuota {
pub cpu_limit: u64,
pub memory_limit: u64,
pub gpu_limit: u32,
pub workload_limit: u32,
}
impl ResourceQuotaAdmission {
pub fn new() -> Self {
Self {
quotas: std::collections::HashMap::new(),
}
}
pub fn set_quota(&mut self, namespace: impl Into<String>, quota: ResourceQuota) {
self.quotas.insert(namespace.into(), quota);
}
}
impl Default for ResourceQuotaAdmission {
fn default() -> Self {
Self::new()
}
}
impl AdmissionController for ResourceQuotaAdmission {
fn name(&self) -> &str {
"ResourceQuota"
}
fn handles(&self, kind: &str, operation: Operation) -> bool {
kind == "Workload" && operation == Operation::Create
}
fn validate(&self, request: &AdmissionRequest) -> AdmissionResult {
let namespace = match &request.namespace {
Some(ns) => ns,
None => return AdmissionResult::allowed(),
};
let quota = match self.quotas.get(namespace) {
Some(q) => q,
None => return AdmissionResult::allowed(),
};
if quota.workload_limit == 0 {
return AdmissionResult::denied(format!(
"Namespace {} has reached workload limit",
namespace
));
}
AdmissionResult::allowed()
}
}
pub struct DefaultsAdmission;
impl DefaultsAdmission {
pub fn new() -> Self {
Self
}
}
impl Default for DefaultsAdmission {
fn default() -> Self {
Self::new()
}
}
impl AdmissionController for DefaultsAdmission {
fn name(&self) -> &str {
"Defaults"
}
fn handles(&self, kind: &str, operation: Operation) -> bool {
kind == "Workload" && operation == Operation::Create
}
fn validate(&self, _request: &AdmissionRequest) -> AdmissionResult {
AdmissionResult::allowed()
}
fn mutate(&self, request: &AdmissionRequest) -> Option<serde_json::Value> {
let obj = request.object.as_ref()?;
let mut patches = Vec::new();
if obj.get("metadata").and_then(|m| m.get("namespace")).is_none() {
patches.push(serde_json::json!({
"op": "add",
"path": "/metadata/namespace",
"value": "default"
}));
}
if obj.get("spec").and_then(|s| s.get("priority")).is_none() {
patches.push(serde_json::json!({
"op": "add",
"path": "/spec/priority",
"value": 0
}));
}
if patches.is_empty() {
None
} else {
Some(serde_json::Value::Array(patches))
}
}
}
pub struct AdmissionChain {
validators: Vec<Arc<dyn AdmissionController>>,
mutators: Vec<Arc<dyn AdmissionController>>,
}
impl AdmissionChain {
pub fn new() -> Self {
Self {
validators: Vec::new(),
mutators: Vec::new(),
}
}
pub fn add_validator<T: AdmissionController + 'static>(mut self, controller: T) -> Self {
self.validators.push(Arc::new(controller));
self
}
pub fn add_mutator<T: AdmissionController + 'static>(mut self, controller: T) -> Self {
self.mutators.push(Arc::new(controller));
self
}
pub fn admit(&self, mut request: AdmissionRequest) -> AdmissionResult {
let mut all_patches = Vec::new();
for mutator in &self.mutators {
if mutator.handles(&request.kind, request.operation) {
if let Some(patch) = mutator.mutate(&request) {
if let Some(patches) = patch.as_array() {
all_patches.extend(patches.clone());
}
}
}
}
if !all_patches.is_empty() {
}
let mut warnings = Vec::new();
for validator in &self.validators {
if validator.handles(&request.kind, request.operation) {
let result = validator.validate(&request);
if !result.allowed {
return result;
}
warnings.extend(result.warnings);
}
}
let mut result = AdmissionResult::allowed();
result.warnings = warnings;
if !all_patches.is_empty() {
result = result.with_patch(serde_json::Value::Array(all_patches));
}
result
}
}
impl Default for AdmissionChain {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_admission_result() {
let allowed = AdmissionResult::allowed();
assert!(allowed.allowed);
let denied = AdmissionResult::denied("test reason");
assert!(!denied.allowed);
assert_eq!(denied.reason, Some("test reason".to_string()));
}
#[test]
fn test_defaults_admission() {
let controller = DefaultsAdmission::new();
let request = AdmissionRequest {
uid: "test".to_string(),
operation: Operation::Create,
kind: "Workload".to_string(),
namespace: None,
name: Some("test".to_string()),
object: Some(serde_json::json!({
"metadata": {"name": "test"},
"spec": {}
})),
old_object: None,
user_info: UserInfo::default(),
};
let patch = controller.mutate(&request);
assert!(patch.is_some());
}
#[test]
fn test_admission_chain() {
let chain = AdmissionChain::new()
.add_mutator(DefaultsAdmission::new())
.add_validator(ResourceQuotaAdmission::new());
let request = AdmissionRequest {
uid: "test".to_string(),
operation: Operation::Create,
kind: "Workload".to_string(),
namespace: Some("default".to_string()),
name: Some("test".to_string()),
object: Some(serde_json::json!({})),
old_object: None,
user_info: UserInfo::default(),
};
let result = chain.admit(request);
assert!(result.allowed);
}
}