forge_orchestration/controlplane/
admission.rs1use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AdmissionResult {
14 pub allowed: bool,
16 pub reason: Option<String>,
18 pub warnings: Vec<String>,
20 pub patch: Option<serde_json::Value>,
22 pub patch_type: Option<String>,
24}
25
26impl AdmissionResult {
27 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 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 pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
51 self.warnings.push(warning.into());
52 self
53 }
54
55 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#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct AdmissionRequest {
66 pub uid: String,
68 pub operation: Operation,
70 pub kind: String,
72 pub namespace: Option<String>,
74 pub name: Option<String>,
76 pub object: Option<serde_json::Value>,
78 pub old_object: Option<serde_json::Value>,
80 pub user_info: UserInfo,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub enum Operation {
87 Create,
89 Update,
91 Delete,
93 Connect,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct UserInfo {
100 pub username: String,
102 pub uid: Option<String>,
104 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
118pub trait AdmissionController: Send + Sync {
120 fn name(&self) -> &str;
122
123 fn handles(&self, kind: &str, operation: Operation) -> bool;
125
126 fn validate(&self, request: &AdmissionRequest) -> AdmissionResult;
128
129 fn mutate(&self, _request: &AdmissionRequest) -> Option<serde_json::Value> {
131 None
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ValidationWebhook {
138 pub name: String,
140 pub url: String,
142 pub failure_policy: FailurePolicy,
144 pub rules: Vec<WebhookRule>,
146 pub timeout_seconds: u32,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct MutatingWebhook {
153 pub name: String,
155 pub url: String,
157 pub failure_policy: FailurePolicy,
159 pub rules: Vec<WebhookRule>,
161 pub timeout_seconds: u32,
163 pub reinvocation_policy: ReinvocationPolicy,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169pub enum FailurePolicy {
170 Fail,
172 Ignore,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178pub enum ReinvocationPolicy {
179 Never,
181 IfNeeded,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct WebhookRule {
188 pub api_groups: Vec<String>,
190 pub api_versions: Vec<String>,
192 pub resources: Vec<String>,
194 pub operations: Vec<Operation>,
196}
197
198pub struct ResourceQuotaAdmission {
200 quotas: std::collections::HashMap<String, ResourceQuota>,
202}
203
204#[derive(Debug, Clone)]
206pub struct ResourceQuota {
207 pub cpu_limit: u64,
209 pub memory_limit: u64,
211 pub gpu_limit: u32,
213 pub workload_limit: u32,
215}
216
217impl ResourceQuotaAdmission {
218 pub fn new() -> Self {
220 Self {
221 quotas: std::collections::HashMap::new(),
222 }
223 }
224
225 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 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
269pub struct DefaultsAdmission;
271
272impl DefaultsAdmission {
273 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 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 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
329pub struct AdmissionChain {
331 validators: Vec<Arc<dyn AdmissionController>>,
333 mutators: Vec<Arc<dyn AdmissionController>>,
335}
336
337impl AdmissionChain {
338 pub fn new() -> Self {
340 Self {
341 validators: Vec::new(),
342 mutators: Vec::new(),
343 }
344 }
345
346 pub fn add_validator<T: AdmissionController + 'static>(mut self, controller: T) -> Self {
348 self.validators.push(Arc::new(controller));
349 self
350 }
351
352 pub fn add_mutator<T: AdmissionController + 'static>(mut self, controller: T) -> Self {
354 self.mutators.push(Arc::new(controller));
355 self
356 }
357
358 pub fn admit(&self, mut request: AdmissionRequest) -> AdmissionResult {
360 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 if !all_patches.is_empty() {
375 }
378
379 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}