mockforge_k8s_operator/
webhook.rs1use crate::crd::{ChaosOrchestration, ChaosOrchestrationSpec};
4use cron::Schedule;
5use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::str::FromStr;
10use tracing::info;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AdmissionReview {
14 pub request: Option<AdmissionRequest>,
15 pub response: Option<AdmissionResponse>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct AdmissionRequest {
20 pub uid: String,
21 pub operation: String,
22 pub object: Option<serde_json::Value>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct AdmissionResponse {
27 pub uid: String,
28 pub allowed: bool,
29 pub status: Option<k8s_openapi::apimachinery::pkg::apis::meta::v1::Status>,
30}
31
32pub struct WebhookHandler;
34
35impl WebhookHandler {
36 pub fn new() -> Self {
38 Self
39 }
40
41 pub async fn handle_admission_review(
43 &self,
44 review: AdmissionReview,
45 ) -> Result<AdmissionReview, String> {
46 let request =
47 review.request.ok_or_else(|| "Missing request in AdmissionReview".to_string())?;
48
49 let response = match request.operation.as_str() {
50 "CREATE" | "UPDATE" => self.validate_orchestration(&request).await,
51 "DELETE" => self.validate_delete(&request).await,
52 _ => AdmissionResponse {
53 uid: request.uid.clone(),
54 allowed: true,
55 ..Default::default()
56 },
57 };
58
59 Ok(AdmissionReview {
60 request: Some(request),
61 response: Some(response),
62 })
63 }
64
65 async fn validate_orchestration(&self, request: &AdmissionRequest) -> AdmissionResponse {
67 let object = match &request.object {
68 Some(obj) => obj,
69 None => {
70 return AdmissionResponse {
71 uid: request.uid.clone(),
72 allowed: false,
73 status: Some(k8s_openapi::apimachinery::pkg::apis::meta::v1::Status {
74 message: Some("Missing object in request".to_string()),
75 ..Default::default()
76 }),
77 ..Default::default()
78 };
79 }
80 };
81
82 let orchestration: ChaosOrchestration = match serde_json::from_value(object.clone()) {
84 Ok(orch) => orch,
85 Err(e) => {
86 return AdmissionResponse {
87 uid: request.uid.clone(),
88 allowed: false,
89 status: Some(k8s_openapi::apimachinery::pkg::apis::meta::v1::Status {
90 message: Some(format!("Failed to parse ChaosOrchestration: {}", e)),
91 ..Default::default()
92 }),
93 ..Default::default()
94 };
95 }
96 };
97
98 if let Err(e) = self.validate_spec(&orchestration.spec) {
100 return AdmissionResponse {
101 uid: request.uid.clone(),
102 allowed: false,
103 status: Some(k8s_openapi::apimachinery::pkg::apis::meta::v1::Status {
104 message: Some(format!("Validation failed: {}", e)),
105 ..Default::default()
106 }),
107 ..Default::default()
108 };
109 }
110
111 info!("Validated ChaosOrchestration: {}", orchestration.spec.name);
112
113 AdmissionResponse {
114 uid: request.uid.clone(),
115 allowed: true,
116 ..Default::default()
117 }
118 }
119
120 async fn validate_delete(&self, request: &AdmissionRequest) -> AdmissionResponse {
122 AdmissionResponse {
124 uid: request.uid.clone(),
125 allowed: true,
126 ..Default::default()
127 }
128 }
129
130 fn validate_spec(&self, spec: &ChaosOrchestrationSpec) -> Result<(), String> {
132 if spec.name.is_empty() {
134 return Err("Orchestration name cannot be empty".to_string());
135 }
136
137 if spec.steps.is_empty() {
139 return Err("Orchestration must have at least one step".to_string());
140 }
141
142 for (idx, step) in spec.steps.iter().enumerate() {
144 if step.name.is_empty() {
145 return Err(format!("Step {} must have a name", idx));
146 }
147
148 if step.scenario.is_empty() {
149 return Err(format!("Step {} must specify a scenario", idx));
150 }
151 }
152
153 if let Some(schedule) = &spec.schedule {
155 if !self.is_valid_cron(schedule) {
156 return Err(format!("Invalid cron schedule: {}", schedule));
157 }
158 }
159
160 for service in &spec.target_services {
162 if service.name.is_empty() {
163 return Err("Target service name cannot be empty".to_string());
164 }
165 }
166
167 Ok(())
168 }
169
170 fn is_valid_cron(&self, schedule: &str) -> bool {
175 if schedule.is_empty() {
176 return false;
177 }
178
179 let cron_expr = if schedule.split_whitespace().count() == 5 {
183 format!("0 {}", schedule)
184 } else {
185 schedule.to_string()
186 };
187
188 Schedule::from_str(&cron_expr).is_ok()
189 }
190
191 pub fn mutate_orchestration(&self, spec: &mut ChaosOrchestrationSpec) {
193 for step in &mut spec.steps {
195 if step.duration_seconds.is_none() {
196 step.duration_seconds = Some(60); }
198 }
199 }
200}
201
202impl Default for WebhookHandler {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_validate_empty_name() {
214 let handler = WebhookHandler::new();
215 let spec = ChaosOrchestrationSpec {
216 name: "".to_string(),
217 description: None,
218 schedule: None,
219 steps: vec![],
220 variables: HashMap::new(),
221 hooks: vec![],
222 assertions: vec![],
223 enable_reporting: true,
224 target_services: vec![],
225 };
226
227 assert!(handler.validate_spec(&spec).is_err());
228 }
229
230 #[test]
231 fn test_validate_no_steps() {
232 let handler = WebhookHandler::new();
233 let spec = ChaosOrchestrationSpec {
234 name: "test".to_string(),
235 description: None,
236 schedule: None,
237 steps: vec![],
238 variables: HashMap::new(),
239 hooks: vec![],
240 assertions: vec![],
241 enable_reporting: true,
242 target_services: vec![],
243 };
244
245 assert!(handler.validate_spec(&spec).is_err());
246 }
247
248 #[test]
249 fn test_valid_cron_expressions() {
250 let handler = WebhookHandler::new();
251
252 assert!(handler.is_valid_cron("0 * * * *")); assert!(handler.is_valid_cron("*/15 * * * *")); assert!(handler.is_valid_cron("0 0 * * *")); assert!(handler.is_valid_cron("0 0 * * SUN")); assert!(handler.is_valid_cron("0 0 1 * *")); assert!(handler.is_valid_cron("0 0 * * * *")); assert!(handler.is_valid_cron("30 */5 * * * *")); }
263
264 #[test]
265 fn test_invalid_cron_expressions() {
266 let handler = WebhookHandler::new();
267
268 assert!(!handler.is_valid_cron("")); assert!(!handler.is_valid_cron("invalid")); assert!(!handler.is_valid_cron("60 * * * *")); assert!(!handler.is_valid_cron("* 25 * * *")); assert!(!handler.is_valid_cron("* * 32 * *")); }
274}