forge_orchestration/controlplane/
admission.rs

1//! Admission controllers for validating and mutating resources
2//!
3//! Implements Kubernetes-style admission control with:
4//! - Validating webhooks
5//! - Mutating webhooks
6//! - Built-in admission controllers
7
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10
11/// Admission result
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AdmissionResult {
14    /// Whether the request is allowed
15    pub allowed: bool,
16    /// Reason for denial (if not allowed)
17    pub reason: Option<String>,
18    /// Warnings to return to the user
19    pub warnings: Vec<String>,
20    /// Patch to apply (for mutating admission)
21    pub patch: Option<serde_json::Value>,
22    /// Patch type (e.g., "JSONPatch")
23    pub patch_type: Option<String>,
24}
25
26impl AdmissionResult {
27    /// Create an allowed result
28    pub fn allowed() -> Self {
29        Self {
30            allowed: true,
31            reason: None,
32            warnings: Vec::new(),
33            patch: None,
34            patch_type: None,
35        }
36    }
37
38    /// Create a denied result
39    pub fn denied(reason: impl Into<String>) -> Self {
40        Self {
41            allowed: false,
42            reason: Some(reason.into()),
43            warnings: Vec::new(),
44            patch: None,
45            patch_type: None,
46        }
47    }
48
49    /// Add a warning
50    pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
51        self.warnings.push(warning.into());
52        self
53    }
54
55    /// Add a JSON patch
56    pub fn with_patch(mut self, patch: serde_json::Value) -> Self {
57        self.patch = Some(patch);
58        self.patch_type = Some("JSONPatch".to_string());
59        self
60    }
61}
62
63/// Admission request
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct AdmissionRequest {
66    /// Unique request ID
67    pub uid: String,
68    /// Operation type
69    pub operation: Operation,
70    /// Resource kind
71    pub kind: String,
72    /// Resource namespace
73    pub namespace: Option<String>,
74    /// Resource name
75    pub name: Option<String>,
76    /// The object being admitted
77    pub object: Option<serde_json::Value>,
78    /// The old object (for UPDATE/DELETE)
79    pub old_object: Option<serde_json::Value>,
80    /// User info
81    pub user_info: UserInfo,
82}
83
84/// Operation type
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub enum Operation {
87    /// Create operation
88    Create,
89    /// Update operation
90    Update,
91    /// Delete operation
92    Delete,
93    /// Connect operation
94    Connect,
95}
96
97/// User information
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct UserInfo {
100    /// Username
101    pub username: String,
102    /// User UID
103    pub uid: Option<String>,
104    /// Groups
105    pub groups: Vec<String>,
106}
107
108impl Default for UserInfo {
109    fn default() -> Self {
110        Self {
111            username: "system:anonymous".to_string(),
112            uid: None,
113            groups: vec!["system:unauthenticated".to_string()],
114        }
115    }
116}
117
118/// Admission controller trait
119pub trait AdmissionController: Send + Sync {
120    /// Controller name
121    fn name(&self) -> &str;
122
123    /// Whether this controller handles the given resource
124    fn handles(&self, kind: &str, operation: Operation) -> bool;
125
126    /// Validate the request
127    fn validate(&self, request: &AdmissionRequest) -> AdmissionResult;
128
129    /// Mutate the request (optional)
130    fn mutate(&self, _request: &AdmissionRequest) -> Option<serde_json::Value> {
131        None
132    }
133}
134
135/// Validation webhook configuration
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ValidationWebhook {
138    /// Webhook name
139    pub name: String,
140    /// Webhook URL
141    pub url: String,
142    /// Failure policy
143    pub failure_policy: FailurePolicy,
144    /// Resources to match
145    pub rules: Vec<WebhookRule>,
146    /// Timeout in seconds
147    pub timeout_seconds: u32,
148}
149
150/// Mutating webhook configuration
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct MutatingWebhook {
153    /// Webhook name
154    pub name: String,
155    /// Webhook URL
156    pub url: String,
157    /// Failure policy
158    pub failure_policy: FailurePolicy,
159    /// Resources to match
160    pub rules: Vec<WebhookRule>,
161    /// Timeout in seconds
162    pub timeout_seconds: u32,
163    /// Reinvocation policy
164    pub reinvocation_policy: ReinvocationPolicy,
165}
166
167/// Failure policy for webhooks
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169pub enum FailurePolicy {
170    /// Fail the request if webhook fails
171    Fail,
172    /// Ignore webhook failures
173    Ignore,
174}
175
176/// Reinvocation policy for mutating webhooks
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178pub enum ReinvocationPolicy {
179    /// Never reinvoke
180    Never,
181    /// Reinvoke if object was modified
182    IfNeeded,
183}
184
185/// Webhook rule
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct WebhookRule {
188    /// API groups to match
189    pub api_groups: Vec<String>,
190    /// API versions to match
191    pub api_versions: Vec<String>,
192    /// Resources to match
193    pub resources: Vec<String>,
194    /// Operations to match
195    pub operations: Vec<Operation>,
196}
197
198/// Built-in resource quota admission controller
199pub struct ResourceQuotaAdmission {
200    /// Namespace quotas
201    quotas: std::collections::HashMap<String, ResourceQuota>,
202}
203
204/// Resource quota
205#[derive(Debug, Clone)]
206pub struct ResourceQuota {
207    /// CPU limit (millicores)
208    pub cpu_limit: u64,
209    /// Memory limit (MB)
210    pub memory_limit: u64,
211    /// GPU limit
212    pub gpu_limit: u32,
213    /// Workload count limit
214    pub workload_limit: u32,
215}
216
217impl ResourceQuotaAdmission {
218    /// Create new resource quota admission controller
219    pub fn new() -> Self {
220        Self {
221            quotas: std::collections::HashMap::new(),
222        }
223    }
224
225    /// Set quota for namespace
226    pub fn set_quota(&mut self, namespace: impl Into<String>, quota: ResourceQuota) {
227        self.quotas.insert(namespace.into(), quota);
228    }
229}
230
231impl Default for ResourceQuotaAdmission {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237impl AdmissionController for ResourceQuotaAdmission {
238    fn name(&self) -> &str {
239        "ResourceQuota"
240    }
241
242    fn handles(&self, kind: &str, operation: Operation) -> bool {
243        kind == "Workload" && operation == Operation::Create
244    }
245
246    fn validate(&self, request: &AdmissionRequest) -> AdmissionResult {
247        let namespace = match &request.namespace {
248            Some(ns) => ns,
249            None => return AdmissionResult::allowed(),
250        };
251
252        let quota = match self.quotas.get(namespace) {
253            Some(q) => q,
254            None => return AdmissionResult::allowed(),
255        };
256
257        // Check workload count (simplified - real impl would track current usage)
258        if quota.workload_limit == 0 {
259            return AdmissionResult::denied(format!(
260                "Namespace {} has reached workload limit",
261                namespace
262            ));
263        }
264
265        AdmissionResult::allowed()
266    }
267}
268
269/// Built-in default values admission controller
270pub struct DefaultsAdmission;
271
272impl DefaultsAdmission {
273    /// Create new defaults admission controller
274    pub fn new() -> Self {
275        Self
276    }
277}
278
279impl Default for DefaultsAdmission {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285impl AdmissionController for DefaultsAdmission {
286    fn name(&self) -> &str {
287        "Defaults"
288    }
289
290    fn handles(&self, kind: &str, operation: Operation) -> bool {
291        kind == "Workload" && operation == Operation::Create
292    }
293
294    fn validate(&self, _request: &AdmissionRequest) -> AdmissionResult {
295        AdmissionResult::allowed()
296    }
297
298    fn mutate(&self, request: &AdmissionRequest) -> Option<serde_json::Value> {
299        let obj = request.object.as_ref()?;
300        
301        let mut patches = Vec::new();
302
303        // Add default namespace if not set
304        if obj.get("metadata").and_then(|m| m.get("namespace")).is_none() {
305            patches.push(serde_json::json!({
306                "op": "add",
307                "path": "/metadata/namespace",
308                "value": "default"
309            }));
310        }
311
312        // Add default priority if not set
313        if obj.get("spec").and_then(|s| s.get("priority")).is_none() {
314            patches.push(serde_json::json!({
315                "op": "add",
316                "path": "/spec/priority",
317                "value": 0
318            }));
319        }
320
321        if patches.is_empty() {
322            None
323        } else {
324            Some(serde_json::Value::Array(patches))
325        }
326    }
327}
328
329/// Admission controller chain
330pub struct AdmissionChain {
331    /// Validating controllers
332    validators: Vec<Arc<dyn AdmissionController>>,
333    /// Mutating controllers
334    mutators: Vec<Arc<dyn AdmissionController>>,
335}
336
337impl AdmissionChain {
338    /// Create new admission chain
339    pub fn new() -> Self {
340        Self {
341            validators: Vec::new(),
342            mutators: Vec::new(),
343        }
344    }
345
346    /// Add validating controller
347    pub fn add_validator<T: AdmissionController + 'static>(mut self, controller: T) -> Self {
348        self.validators.push(Arc::new(controller));
349        self
350    }
351
352    /// Add mutating controller
353    pub fn add_mutator<T: AdmissionController + 'static>(mut self, controller: T) -> Self {
354        self.mutators.push(Arc::new(controller));
355        self
356    }
357
358    /// Run admission for a request
359    pub fn admit(&self, mut request: AdmissionRequest) -> AdmissionResult {
360        // Run mutating admission first
361        let mut all_patches = Vec::new();
362        
363        for mutator in &self.mutators {
364            if mutator.handles(&request.kind, request.operation) {
365                if let Some(patch) = mutator.mutate(&request) {
366                    if let Some(patches) = patch.as_array() {
367                        all_patches.extend(patches.clone());
368                    }
369                }
370            }
371        }
372
373        // Apply patches to request object
374        if !all_patches.is_empty() {
375            // In a real implementation, we'd apply JSON patches here
376            // For now, just track that patches exist
377        }
378
379        // Run validating admission
380        let mut warnings = Vec::new();
381        
382        for validator in &self.validators {
383            if validator.handles(&request.kind, request.operation) {
384                let result = validator.validate(&request);
385                
386                if !result.allowed {
387                    return result;
388                }
389                
390                warnings.extend(result.warnings);
391            }
392        }
393
394        let mut result = AdmissionResult::allowed();
395        result.warnings = warnings;
396        
397        if !all_patches.is_empty() {
398            result = result.with_patch(serde_json::Value::Array(all_patches));
399        }
400
401        result
402    }
403}
404
405impl Default for AdmissionChain {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_admission_result() {
417        let allowed = AdmissionResult::allowed();
418        assert!(allowed.allowed);
419
420        let denied = AdmissionResult::denied("test reason");
421        assert!(!denied.allowed);
422        assert_eq!(denied.reason, Some("test reason".to_string()));
423    }
424
425    #[test]
426    fn test_defaults_admission() {
427        let controller = DefaultsAdmission::new();
428        
429        let request = AdmissionRequest {
430            uid: "test".to_string(),
431            operation: Operation::Create,
432            kind: "Workload".to_string(),
433            namespace: None,
434            name: Some("test".to_string()),
435            object: Some(serde_json::json!({
436                "metadata": {"name": "test"},
437                "spec": {}
438            })),
439            old_object: None,
440            user_info: UserInfo::default(),
441        };
442
443        let patch = controller.mutate(&request);
444        assert!(patch.is_some());
445    }
446
447    #[test]
448    fn test_admission_chain() {
449        let chain = AdmissionChain::new()
450            .add_mutator(DefaultsAdmission::new())
451            .add_validator(ResourceQuotaAdmission::new());
452
453        let request = AdmissionRequest {
454            uid: "test".to_string(),
455            operation: Operation::Create,
456            kind: "Workload".to_string(),
457            namespace: Some("default".to_string()),
458            name: Some("test".to_string()),
459            object: Some(serde_json::json!({})),
460            old_object: None,
461            user_info: UserInfo::default(),
462        };
463
464        let result = chain.admit(request);
465        assert!(result.allowed);
466    }
467}