1use crate::{
4 ab_testing::{ABTestConfig, ABTestingEngine, TestConclusion, VariantResults},
5 analytics::{ChaosAnalytics, TimeBucket},
6 auto_remediation::{RemediationConfig, RemediationEngine},
7 config::{
8 BulkheadConfig, ChaosConfig, CircuitBreakerConfig, FaultInjectionConfig, LatencyConfig,
9 NetworkProfile, RateLimitConfig, TrafficShapingConfig,
10 },
11 latency_metrics::LatencyMetricsTracker,
12 recommendations::{
13 Recommendation, RecommendationCategory, RecommendationEngine, RecommendationSeverity,
14 },
15 scenario_orchestrator::{OrchestratedScenario, ScenarioOrchestrator},
16 scenario_recorder::{RecordedScenario, ScenarioRecorder},
17 scenario_replay::{ReplayOptions, ReplaySpeed, ScenarioReplayEngine},
18 scenario_scheduler::{ScenarioScheduler, ScheduleType, ScheduledScenario},
19 scenarios::{ChaosScenario, PredefinedScenarios, ScenarioEngine},
20};
21use axum::{
22 extract::{Path, Query, State},
23 http::StatusCode,
24 response::{IntoResponse, Json, Response},
25 routing::{delete, get, post, put},
26 Router,
27};
28use parking_lot::RwLock as ParkingRwLock;
29use serde::{Deserialize, Serialize};
30use std::sync::Arc;
31use tokio::sync::RwLock;
32use tracing::info;
33
34#[derive(Clone)]
36pub struct ProfileManager {
37 custom_profiles: Arc<ParkingRwLock<std::collections::HashMap<String, NetworkProfile>>>,
39}
40
41impl ProfileManager {
42 pub fn new() -> Self {
44 Self {
45 custom_profiles: Arc::new(ParkingRwLock::new(std::collections::HashMap::new())),
46 }
47 }
48
49 pub fn get_all_profiles(&self) -> Vec<NetworkProfile> {
51 let mut profiles = NetworkProfile::predefined_profiles();
52 let custom = self.custom_profiles.read();
53 profiles.extend(custom.values().cloned());
54 profiles
55 }
56
57 pub fn get_profile(&self, name: &str) -> Option<NetworkProfile> {
59 for profile in NetworkProfile::predefined_profiles() {
61 if profile.name == name {
62 return Some(profile);
63 }
64 }
65 let custom = self.custom_profiles.read();
67 custom.get(name).cloned()
68 }
69
70 pub fn save_profile(&self, profile: NetworkProfile) {
72 let mut custom = self.custom_profiles.write();
73 custom.insert(profile.name.clone(), profile);
74 }
75
76 pub fn delete_profile(&self, name: &str) -> bool {
78 let mut custom = self.custom_profiles.write();
79 custom.remove(name).is_some()
80 }
81
82 pub fn get_custom_profiles(&self) -> Vec<NetworkProfile> {
84 let custom = self.custom_profiles.read();
85 custom.values().cloned().collect()
86 }
87}
88
89impl Default for ProfileManager {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95#[derive(Clone)]
97pub struct ChaosApiState {
98 pub config: Arc<RwLock<ChaosConfig>>,
99 pub scenario_engine: Arc<ScenarioEngine>,
100 pub orchestrator: Arc<tokio::sync::RwLock<ScenarioOrchestrator>>,
101 pub analytics: Arc<ChaosAnalytics>,
102 pub recommendation_engine: Arc<RecommendationEngine>,
103 pub remediation_engine: Arc<RemediationEngine>,
104 pub ab_testing_engine: Arc<tokio::sync::RwLock<ABTestingEngine>>,
105 pub recorder: Arc<ScenarioRecorder>,
106 pub replay_engine: Arc<tokio::sync::RwLock<ScenarioReplayEngine>>,
107 pub scheduler: Arc<tokio::sync::RwLock<ScenarioScheduler>>,
108 pub latency_tracker: Arc<LatencyMetricsTracker>,
109 pub profile_manager: Arc<ProfileManager>,
110 pub mockai:
111 Option<std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
112}
113
114pub fn create_chaos_api_router(
123 config: ChaosConfig,
124 mockai: Option<
125 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
126 >,
127) -> (Router, Arc<RwLock<ChaosConfig>>, Arc<LatencyMetricsTracker>, Arc<ChaosApiState>) {
128 let config_arc = Arc::new(RwLock::new(config));
129 let scenario_engine = Arc::new(ScenarioEngine::new());
130 let orchestrator = Arc::new(tokio::sync::RwLock::new(ScenarioOrchestrator::new()));
131 let analytics = Arc::new(ChaosAnalytics::new());
132 let recommendation_engine = Arc::new(RecommendationEngine::new());
133 let remediation_engine = Arc::new(RemediationEngine::new());
134 let ab_testing_engine =
135 Arc::new(tokio::sync::RwLock::new(ABTestingEngine::new(analytics.clone())));
136 let recorder = Arc::new(ScenarioRecorder::new());
137 let replay_engine = Arc::new(tokio::sync::RwLock::new(ScenarioReplayEngine::new()));
138 let scheduler = Arc::new(tokio::sync::RwLock::new(ScenarioScheduler::new()));
139 let latency_tracker = Arc::new(LatencyMetricsTracker::new());
140 let profile_manager = Arc::new(ProfileManager::new());
141
142 let latency_tracker_for_return = latency_tracker.clone();
144
145 let state = ChaosApiState {
146 config: config_arc.clone(),
147 scenario_engine,
148 orchestrator,
149 analytics,
150 recommendation_engine,
151 remediation_engine,
152 ab_testing_engine,
153 recorder,
154 replay_engine,
155 scheduler,
156 latency_tracker,
157 profile_manager,
158 mockai,
159 };
160
161 let router = Router::new()
162 .route("/api/chaos/config", get(get_config))
164 .route("/api/chaos/config", put(update_config))
165 .route("/api/chaos/config/latency", put(update_latency_config))
166 .route("/api/chaos/config/faults", put(update_fault_config))
167 .route("/api/chaos/config/rate-limit", put(update_rate_limit_config))
168 .route("/api/chaos/config/traffic", put(update_traffic_config))
169 .route("/api/chaos/config/circuit-breaker", put(update_circuit_breaker_config))
170 .route("/api/chaos/config/bulkhead", put(update_bulkhead_config))
171
172 .route("/api/chaos/protocols/grpc/status-codes", post(inject_grpc_status_codes))
174 .route("/api/chaos/protocols/grpc/stream-interruption", post(set_grpc_stream_interruption))
175 .route("/api/chaos/protocols/websocket/close-codes", post(inject_websocket_close_codes))
176 .route("/api/chaos/protocols/websocket/message-drop", post(set_websocket_message_drop))
177 .route("/api/chaos/protocols/websocket/message-corruption", post(set_websocket_message_corruption))
178 .route("/api/chaos/protocols/graphql/error-codes", post(inject_graphql_error_codes))
179 .route("/api/chaos/protocols/graphql/partial-data", post(set_graphql_partial_data))
180 .route("/api/chaos/protocols/graphql/resolver-latency", post(toggle_graphql_resolver_latency))
181
182 .route("/api/chaos/enable", post(enable_chaos))
184 .route("/api/chaos/disable", post(disable_chaos))
185 .route("/api/chaos/reset", post(reset_chaos))
186
187 .route("/api/chaos/scenarios", get(list_scenarios))
189 .route("/api/chaos/scenarios/predefined", get(list_predefined_scenarios))
190 .route("/api/chaos/scenarios/:name", post(start_scenario))
191 .route("/api/chaos/scenarios/:name", delete(stop_scenario))
192 .route("/api/chaos/scenarios", delete(stop_all_scenarios))
193
194 .route("/api/chaos/status", get(get_status))
196
197 .route("/api/chaos/metrics/latency", get(get_latency_metrics))
199 .route("/api/chaos/metrics/latency/stats", get(get_latency_stats))
200
201 .route("/api/chaos/profiles", get(list_profiles))
203 .route("/api/chaos/profiles/:name", get(get_profile))
204 .route("/api/chaos/profiles/:name/apply", post(apply_profile))
205 .route("/api/chaos/profiles", post(create_profile))
206 .route("/api/chaos/profiles/:name", delete(delete_profile))
207 .route("/api/chaos/profiles/:name/export", get(export_profile))
208 .route("/api/chaos/profiles/import", post(import_profile))
209
210 .route("/api/chaos/recording/start", post(start_recording))
212 .route("/api/chaos/recording/stop", post(stop_recording))
213 .route("/api/chaos/recording/status", get(recording_status))
214 .route("/api/chaos/recording/export", post(export_recording))
215
216 .route("/api/chaos/replay/start", post(start_replay))
218 .route("/api/chaos/replay/pause", post(pause_replay))
219 .route("/api/chaos/replay/resume", post(resume_replay))
220 .route("/api/chaos/replay/stop", post(stop_replay))
221 .route("/api/chaos/replay/status", get(replay_status))
222
223 .route("/api/chaos/orchestration/start", post(start_orchestration))
225 .route("/api/chaos/orchestration/stop", post(stop_orchestration))
226 .route("/api/chaos/orchestration/status", get(orchestration_status))
227 .route("/api/chaos/orchestration/import", post(import_orchestration))
228
229 .route("/api/chaos/schedule", post(add_schedule))
231 .route("/api/chaos/schedule/:id", get(get_schedule))
232 .route("/api/chaos/schedule/:id", delete(remove_schedule))
233 .route("/api/chaos/schedule/:id/enable", post(enable_schedule))
234 .route("/api/chaos/schedule/:id/disable", post(disable_schedule))
235 .route("/api/chaos/schedules", get(list_schedules))
241
242 .route("/api/chaos/recommendations", get(get_recommendations))
244 .route("/api/chaos/recommendations/analyze", post(analyze_and_recommend))
245 .route("/api/chaos/recommendations/category/:category", get(get_recommendations_by_category))
246 .route("/api/chaos/recommendations/severity/:severity", get(get_recommendations_by_severity))
247 .route("/api/chaos/recommendations", delete(clear_recommendations))
248
249 .route("/api/chaos/remediation/config", get(get_remediation_config))
251 .route("/api/chaos/remediation/config", put(update_remediation_config))
252 .route("/api/chaos/remediation/process", post(process_remediation))
253 .route("/api/chaos/remediation/approve/:id", post(approve_remediation))
254 .route("/api/chaos/remediation/reject/:id", post(reject_remediation))
255 .route("/api/chaos/remediation/rollback/:id", post(rollback_remediation))
256 .route("/api/chaos/remediation/actions", get(get_remediation_actions))
257 .route("/api/chaos/remediation/actions/:id", get(get_remediation_action))
258 .route("/api/chaos/remediation/approvals", get(get_approval_queue))
259 .route("/api/chaos/remediation/effectiveness/:id", get(get_remediation_effectiveness))
260 .route("/api/chaos/remediation/stats", get(get_remediation_stats))
261
262 .route("/api/chaos/ab-tests", post(create_ab_test))
264 .route("/api/chaos/ab-tests", get(get_ab_tests))
265 .route("/api/chaos/ab-tests/:id", get(get_ab_test))
266 .route("/api/chaos/ab-tests/:id/start", post(start_ab_test))
267 .route("/api/chaos/ab-tests/:id/stop", post(stop_ab_test))
268 .route("/api/chaos/ab-tests/:id/pause", post(pause_ab_test))
269 .route("/api/chaos/ab-tests/:id/resume", post(resume_ab_test))
270 .route("/api/chaos/ab-tests/:id/record/:variant", post(record_ab_test_result))
271 .route("/api/chaos/ab-tests/:id", delete(delete_ab_test))
272 .route("/api/chaos/ab-tests/stats", get(get_ab_test_stats))
273
274 .with_state(state.clone());
275
276 (router, config_arc, latency_tracker_for_return, Arc::new(state))
277}
278
279async fn get_config(State(state): State<ChaosApiState>) -> Json<ChaosConfig> {
281 let config = state.config.read().await;
282 Json(config.clone())
283}
284
285async fn update_config(
287 State(state): State<ChaosApiState>,
288 Json(new_config): Json<ChaosConfig>,
289) -> Json<StatusResponse> {
290 let mut config = state.config.write().await;
291 *config = new_config;
292 info!("Chaos configuration updated");
293 Json(StatusResponse {
294 message: "Configuration updated".to_string(),
295 })
296}
297
298async fn update_latency_config(
300 State(state): State<ChaosApiState>,
301 Json(latency_config): Json<LatencyConfig>,
302) -> Json<StatusResponse> {
303 let mut config = state.config.write().await;
304 config.latency = Some(latency_config);
305 info!("Latency configuration updated");
306 Json(StatusResponse {
307 message: "Latency configuration updated".to_string(),
308 })
309}
310
311async fn update_fault_config(
313 State(state): State<ChaosApiState>,
314 Json(fault_config): Json<FaultInjectionConfig>,
315) -> Json<StatusResponse> {
316 let mut config = state.config.write().await;
317 config.fault_injection = Some(fault_config);
318 info!("Fault injection configuration updated");
319 Json(StatusResponse {
320 message: "Fault injection configuration updated".to_string(),
321 })
322}
323
324async fn update_rate_limit_config(
326 State(state): State<ChaosApiState>,
327 Json(rate_config): Json<RateLimitConfig>,
328) -> Json<StatusResponse> {
329 let mut config = state.config.write().await;
330 config.rate_limit = Some(rate_config);
331 info!("Rate limit configuration updated");
332 Json(StatusResponse {
333 message: "Rate limit configuration updated".to_string(),
334 })
335}
336
337async fn update_traffic_config(
339 State(state): State<ChaosApiState>,
340 Json(traffic_config): Json<TrafficShapingConfig>,
341) -> Json<StatusResponse> {
342 let mut config = state.config.write().await;
343 config.traffic_shaping = Some(traffic_config);
344 info!("Traffic shaping configuration updated");
345 Json(StatusResponse {
346 message: "Traffic shaping configuration updated".to_string(),
347 })
348}
349
350async fn update_circuit_breaker_config(
352 State(state): State<ChaosApiState>,
353 Json(cb_config): Json<CircuitBreakerConfig>,
354) -> Json<StatusResponse> {
355 let mut config = state.config.write().await;
356 config.circuit_breaker = Some(cb_config);
357 info!("Circuit breaker configuration updated");
358 Json(StatusResponse {
359 message: "Circuit breaker configuration updated".to_string(),
360 })
361}
362
363async fn update_bulkhead_config(
365 State(state): State<ChaosApiState>,
366 Json(bulkhead_config): Json<BulkheadConfig>,
367) -> Json<StatusResponse> {
368 let mut config = state.config.write().await;
369 config.bulkhead = Some(bulkhead_config);
370 info!("Bulkhead configuration updated");
371 Json(StatusResponse {
372 message: "Bulkhead configuration updated".to_string(),
373 })
374}
375
376async fn enable_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
378 let mut config = state.config.write().await;
379 config.enabled = true;
380 info!("Chaos engineering enabled");
381 Json(StatusResponse {
382 message: "Chaos engineering enabled".to_string(),
383 })
384}
385
386async fn disable_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
388 let mut config = state.config.write().await;
389 config.enabled = false;
390 info!("Chaos engineering disabled");
391 Json(StatusResponse {
392 message: "Chaos engineering disabled".to_string(),
393 })
394}
395
396async fn reset_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
398 let mut config = state.config.write().await;
399 *config = ChaosConfig::default();
400 state.scenario_engine.stop_all_scenarios();
401 info!("Chaos configuration reset to defaults");
402 Json(StatusResponse {
403 message: "Chaos configuration reset".to_string(),
404 })
405}
406
407async fn list_scenarios(State(state): State<ChaosApiState>) -> Json<Vec<ChaosScenario>> {
409 let scenarios = state.scenario_engine.get_active_scenarios();
410 Json(scenarios)
411}
412
413async fn list_predefined_scenarios() -> Json<Vec<PredefinedScenarioInfo>> {
415 Json(vec![
416 PredefinedScenarioInfo {
417 name: "network_degradation".to_string(),
418 description: "Simulates degraded network conditions with high latency and packet loss"
419 .to_string(),
420 tags: vec!["network".to_string(), "latency".to_string()],
421 },
422 PredefinedScenarioInfo {
423 name: "service_instability".to_string(),
424 description: "Simulates an unstable service with random errors and timeouts"
425 .to_string(),
426 tags: vec!["service".to_string(), "errors".to_string()],
427 },
428 PredefinedScenarioInfo {
429 name: "cascading_failure".to_string(),
430 description: "Simulates a cascading failure with multiple simultaneous issues"
431 .to_string(),
432 tags: vec!["critical".to_string(), "cascading".to_string()],
433 },
434 PredefinedScenarioInfo {
435 name: "peak_traffic".to_string(),
436 description: "Simulates peak traffic conditions with aggressive rate limiting"
437 .to_string(),
438 tags: vec!["traffic".to_string(), "load".to_string()],
439 },
440 PredefinedScenarioInfo {
441 name: "slow_backend".to_string(),
442 description: "Simulates a consistently slow backend service".to_string(),
443 tags: vec!["latency".to_string(), "performance".to_string()],
444 },
445 ])
446}
447
448async fn start_scenario(
450 State(state): State<ChaosApiState>,
451 Path(name): Path<String>,
452) -> Result<Json<StatusResponse>, ChaosApiError> {
453 let scenario = match name.as_str() {
454 "network_degradation" => PredefinedScenarios::network_degradation(),
455 "service_instability" => PredefinedScenarios::service_instability(),
456 "cascading_failure" => PredefinedScenarios::cascading_failure(),
457 "peak_traffic" => PredefinedScenarios::peak_traffic(),
458 "slow_backend" => PredefinedScenarios::slow_backend(),
459 _ => return Err(ChaosApiError::NotFound(format!("Scenario '{}' not found", name))),
460 };
461
462 state.scenario_engine.start_scenario(scenario.clone());
463
464 let mut config = state.config.write().await;
466 *config = scenario.chaos_config;
467
468 info!("Started scenario: {}", name);
469 Ok(Json(StatusResponse {
470 message: format!("Scenario '{}' started", name),
471 }))
472}
473
474async fn stop_scenario(
476 State(state): State<ChaosApiState>,
477 Path(name): Path<String>,
478) -> Result<Json<StatusResponse>, ChaosApiError> {
479 if state.scenario_engine.stop_scenario(&name) {
480 info!("Stopped scenario: {}", name);
481 Ok(Json(StatusResponse {
482 message: format!("Scenario '{}' stopped", name),
483 }))
484 } else {
485 Err(ChaosApiError::NotFound(format!("Scenario '{}' not found or not running", name)))
486 }
487}
488
489async fn stop_all_scenarios(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
491 state.scenario_engine.stop_all_scenarios();
492 info!("Stopped all scenarios");
493 Json(StatusResponse {
494 message: "All scenarios stopped".to_string(),
495 })
496}
497
498async fn get_status(State(state): State<ChaosApiState>) -> Json<ChaosStatus> {
500 let config = state.config.read().await;
501 let scenarios = state.scenario_engine.get_active_scenarios();
502
503 Json(ChaosStatus {
504 enabled: config.enabled,
505 active_scenarios: scenarios.iter().map(|s| s.name.clone()).collect(),
506 latency_enabled: config.latency.as_ref().is_some_and(|l| l.enabled),
507 fault_injection_enabled: config.fault_injection.as_ref().is_some_and(|f| f.enabled),
508 rate_limit_enabled: config.rate_limit.as_ref().is_some_and(|r| r.enabled),
509 traffic_shaping_enabled: config.traffic_shaping.as_ref().is_some_and(|t| t.enabled),
510 })
511}
512
513async fn inject_grpc_status_codes(
517 State(state): State<ChaosApiState>,
518 Json(req): Json<GrpcStatusCodesRequest>,
519) -> Json<StatusResponse> {
520 let mut config = state.config.write().await;
521
522 let mut http_errors = config
524 .fault_injection
525 .as_ref()
526 .map(|f| f.http_errors.clone())
527 .unwrap_or_default();
528
529 for code in &req.status_codes {
530 let http_code = match code {
532 3 => 400, 16 => 401, 7 => 403, 5 => 404, 8 => 429, 13 => 500, 12 => 501, 14 => 503, 4 => 504, _ => 500, };
543 if !http_errors.contains(&http_code) {
544 http_errors.push(http_code);
545 }
546 }
547
548 if let Some(fault_config) = &mut config.fault_injection {
549 fault_config.http_errors = http_errors;
550 fault_config.http_error_probability = req.probability;
551 }
552
553 info!("gRPC status codes configured: {:?}", &req.status_codes);
554 Json(StatusResponse {
555 message: "gRPC status codes configured".to_string(),
556 })
557}
558
559async fn set_grpc_stream_interruption(
561 State(state): State<ChaosApiState>,
562 Json(req): Json<ProbabilityRequest>,
563) -> Json<StatusResponse> {
564 let mut config = state.config.write().await;
565
566 if let Some(fault_config) = &mut config.fault_injection {
567 fault_config.partial_response_probability = req.probability;
568 }
569
570 info!("gRPC stream interruption probability set to {}", req.probability);
571 Json(StatusResponse {
572 message: "gRPC stream interruption configured".to_string(),
573 })
574}
575
576async fn inject_websocket_close_codes(
578 State(state): State<ChaosApiState>,
579 Json(req): Json<WebSocketCloseCodesRequest>,
580) -> Json<StatusResponse> {
581 let mut config = state.config.write().await;
582
583 let mut http_errors = config
584 .fault_injection
585 .as_ref()
586 .map(|f| f.http_errors.clone())
587 .unwrap_or_default();
588
589 for code in &req.close_codes {
590 let http_code = match code {
592 1002 => 400, 1001 => 408, 1008 => 429, 1011 => 500, _ => 500,
597 };
598 if !http_errors.contains(&http_code) {
599 http_errors.push(http_code);
600 }
601 }
602
603 if let Some(fault_config) = &mut config.fault_injection {
604 fault_config.http_errors = http_errors;
605 fault_config.http_error_probability = req.probability;
606 }
607
608 info!("WebSocket close codes configured: {:?}", &req.close_codes);
609 Json(StatusResponse {
610 message: "WebSocket close codes configured".to_string(),
611 })
612}
613
614async fn set_websocket_message_drop(
616 State(state): State<ChaosApiState>,
617 Json(req): Json<ProbabilityRequest>,
618) -> Json<StatusResponse> {
619 let mut config = state.config.write().await;
620
621 if let Some(traffic_config) = &mut config.traffic_shaping {
622 traffic_config.packet_loss_percent = req.probability * 100.0;
623 }
624
625 info!("WebSocket message drop probability set to {}", req.probability);
626 Json(StatusResponse {
627 message: "WebSocket message drop configured".to_string(),
628 })
629}
630
631async fn set_websocket_message_corruption(
633 State(state): State<ChaosApiState>,
634 Json(req): Json<ProbabilityRequest>,
635) -> Json<StatusResponse> {
636 let mut config = state.config.write().await;
637
638 if let Some(fault_config) = &mut config.fault_injection {
639 fault_config.partial_response_probability = req.probability;
640 }
641
642 info!("WebSocket message corruption probability set to {}", req.probability);
643 Json(StatusResponse {
644 message: "WebSocket message corruption configured".to_string(),
645 })
646}
647
648async fn inject_graphql_error_codes(
650 State(state): State<ChaosApiState>,
651 Json(req): Json<GraphQLErrorCodesRequest>,
652) -> Json<StatusResponse> {
653 let mut config = state.config.write().await;
654
655 let mut http_errors = config
656 .fault_injection
657 .as_ref()
658 .map(|f| f.http_errors.clone())
659 .unwrap_or_default();
660
661 for code in &req.error_codes {
662 let http_code = match code.as_str() {
663 "BAD_USER_INPUT" => 400,
664 "UNAUTHENTICATED" => 401,
665 "FORBIDDEN" => 403,
666 "NOT_FOUND" => 404,
667 "INTERNAL_SERVER_ERROR" => 500,
668 "SERVICE_UNAVAILABLE" => 503,
669 _ => 500,
670 };
671 if !http_errors.contains(&http_code) {
672 http_errors.push(http_code);
673 }
674 }
675
676 if let Some(fault_config) = &mut config.fault_injection {
677 fault_config.http_errors = http_errors;
678 fault_config.http_error_probability = req.probability;
679 }
680
681 info!("GraphQL error codes configured: {:?}", &req.error_codes);
682 Json(StatusResponse {
683 message: "GraphQL error codes configured".to_string(),
684 })
685}
686
687async fn set_graphql_partial_data(
689 State(state): State<ChaosApiState>,
690 Json(req): Json<ProbabilityRequest>,
691) -> Json<StatusResponse> {
692 let mut config = state.config.write().await;
693
694 if let Some(fault_config) = &mut config.fault_injection {
695 fault_config.partial_response_probability = req.probability;
696 }
697
698 info!("GraphQL partial data probability set to {}", req.probability);
699 Json(StatusResponse {
700 message: "GraphQL partial data configured".to_string(),
701 })
702}
703
704async fn toggle_graphql_resolver_latency(
706 State(state): State<ChaosApiState>,
707 Json(req): Json<EnableRequest>,
708) -> Json<StatusResponse> {
709 let mut config = state.config.write().await;
710
711 if let Some(latency_config) = &mut config.latency {
712 latency_config.enabled = req.enabled;
713 }
714
715 info!("GraphQL resolver latency {}", if req.enabled { "enabled" } else { "disabled" });
716 Json(StatusResponse {
717 message: format!(
718 "GraphQL resolver latency {}",
719 if req.enabled { "enabled" } else { "disabled" }
720 ),
721 })
722}
723
724#[derive(Debug, Serialize)]
727struct StatusResponse {
728 message: String,
729}
730
731#[derive(Debug, Serialize)]
732struct PredefinedScenarioInfo {
733 name: String,
734 description: String,
735 tags: Vec<String>,
736}
737
738#[derive(Debug, Serialize)]
739struct ChaosStatus {
740 enabled: bool,
741 active_scenarios: Vec<String>,
742 latency_enabled: bool,
743 fault_injection_enabled: bool,
744 rate_limit_enabled: bool,
745 traffic_shaping_enabled: bool,
746}
747
748#[derive(Debug, Deserialize)]
749struct GrpcStatusCodesRequest {
750 status_codes: Vec<i32>,
751 probability: f64,
752}
753
754#[derive(Debug, Deserialize)]
755struct WebSocketCloseCodesRequest {
756 close_codes: Vec<u16>,
757 probability: f64,
758}
759
760#[derive(Debug, Deserialize)]
761struct GraphQLErrorCodesRequest {
762 error_codes: Vec<String>,
763 probability: f64,
764}
765
766#[derive(Debug, Deserialize)]
767struct ProbabilityRequest {
768 probability: f64,
769}
770
771#[derive(Debug, Deserialize)]
772struct EnableRequest {
773 enabled: bool,
774}
775
776async fn start_recording(
780 State(state): State<ChaosApiState>,
781 Json(req): Json<StartRecordingRequest>,
782) -> Result<Json<StatusResponse>, ChaosApiError> {
783 let scenario = match req.scenario_name.as_str() {
785 "network_degradation" => PredefinedScenarios::network_degradation(),
786 "service_instability" => PredefinedScenarios::service_instability(),
787 "cascading_failure" => PredefinedScenarios::cascading_failure(),
788 "peak_traffic" => PredefinedScenarios::peak_traffic(),
789 "slow_backend" => PredefinedScenarios::slow_backend(),
790 _ => {
791 let active_scenarios = state.scenario_engine.get_active_scenarios();
793 active_scenarios
794 .into_iter()
795 .find(|s| s.name == req.scenario_name)
796 .ok_or_else(|| {
797 ChaosApiError::NotFound(format!("Scenario '{}' not found", req.scenario_name))
798 })?
799 }
800 };
801
802 match state.recorder.start_recording(scenario) {
804 Ok(_) => {
805 info!("Recording started for scenario: {}", req.scenario_name);
806 Ok(Json(StatusResponse {
807 message: format!("Recording started for scenario: {}", req.scenario_name),
808 }))
809 }
810 Err(err) => Err(ChaosApiError::NotFound(err)),
811 }
812}
813
814async fn stop_recording(
816 State(state): State<ChaosApiState>,
817) -> Result<Json<StatusResponse>, ChaosApiError> {
818 match state.recorder.stop_recording() {
819 Ok(recording) => {
820 info!(
821 "Recording stopped for scenario: {} ({} events)",
822 recording.scenario.name,
823 recording.events.len()
824 );
825 Ok(Json(StatusResponse {
826 message: format!(
827 "Recording stopped for scenario: {} ({} events, {}ms)",
828 recording.scenario.name,
829 recording.events.len(),
830 recording.total_duration_ms
831 ),
832 }))
833 }
834 Err(err) => Err(ChaosApiError::NotFound(err)),
835 }
836}
837
838async fn recording_status(State(state): State<ChaosApiState>) -> Json<RecordingStatusResponse> {
840 if let Some(recording) = state.recorder.get_current_recording() {
841 Json(RecordingStatusResponse {
842 is_recording: true,
843 scenario_name: Some(recording.scenario.name),
844 events_recorded: recording.events.len(),
845 })
846 } else {
847 Json(RecordingStatusResponse {
848 is_recording: false,
849 scenario_name: None,
850 events_recorded: 0,
851 })
852 }
853}
854
855async fn export_recording(
857 State(state): State<ChaosApiState>,
858 Json(req): Json<ExportRequest>,
859) -> Result<Json<StatusResponse>, ChaosApiError> {
860 if state.recorder.get_current_recording().is_some() {
862 return Err(ChaosApiError::NotFound(
863 "Cannot export while recording is in progress. Stop recording first.".to_string(),
864 ));
865 }
866
867 let recordings = state.recorder.get_recordings();
869 if recordings.is_empty() {
870 return Err(ChaosApiError::NotFound("No recordings available to export".to_string()));
871 }
872
873 let recording = recordings.last().unwrap();
874
875 match recording.save_to_file(&req.path) {
877 Ok(_) => {
878 info!("Recording exported to: {}", req.path);
879 Ok(Json(StatusResponse {
880 message: format!(
881 "Recording exported to: {} ({} events)",
882 req.path,
883 recording.events.len()
884 ),
885 }))
886 }
887 Err(err) => Err(ChaosApiError::NotFound(format!("Failed to export recording: {}", err))),
888 }
889}
890
891async fn start_replay(
893 State(state): State<ChaosApiState>,
894 Json(req): Json<StartReplayRequest>,
895) -> Result<Json<StatusResponse>, ChaosApiError> {
896 let recorded = RecordedScenario::load_from_file(&req.path)
898 .map_err(|e| ChaosApiError::NotFound(format!("Failed to load recording: {}", e)))?;
899
900 let speed = match req.speed {
902 Some(s) if s > 0.0 => ReplaySpeed::Custom(s),
903 Some(0.0) => ReplaySpeed::Fast,
904 _ => ReplaySpeed::RealTime,
905 };
906
907 let options = ReplayOptions {
908 speed,
909 loop_replay: req.loop_replay.unwrap_or(false),
910 skip_initial_delay: false,
911 event_type_filter: None,
912 };
913
914 let mut replay_engine = state.replay_engine.write().await;
916 match replay_engine.replay(recorded.clone(), options).await {
917 Ok(_) => {
918 info!("Replay started for scenario: {}", recorded.scenario.name);
919 Ok(Json(StatusResponse {
920 message: format!(
921 "Replay started for scenario: {} ({} events)",
922 recorded.scenario.name,
923 recorded.events.len()
924 ),
925 }))
926 }
927 Err(err) => Err(ChaosApiError::NotFound(err)),
928 }
929}
930
931async fn pause_replay(
933 State(state): State<ChaosApiState>,
934) -> Result<Json<StatusResponse>, ChaosApiError> {
935 let replay_engine = state.replay_engine.read().await;
936 match replay_engine.pause().await {
937 Ok(_) => {
938 info!("Replay paused");
939 Ok(Json(StatusResponse {
940 message: "Replay paused".to_string(),
941 }))
942 }
943 Err(err) => Err(ChaosApiError::NotFound(err)),
944 }
945}
946
947async fn resume_replay(
949 State(state): State<ChaosApiState>,
950) -> Result<Json<StatusResponse>, ChaosApiError> {
951 let replay_engine = state.replay_engine.read().await;
952 match replay_engine.resume().await {
953 Ok(_) => {
954 info!("Replay resumed");
955 Ok(Json(StatusResponse {
956 message: "Replay resumed".to_string(),
957 }))
958 }
959 Err(err) => Err(ChaosApiError::NotFound(err)),
960 }
961}
962
963async fn stop_replay(
965 State(state): State<ChaosApiState>,
966) -> Result<Json<StatusResponse>, ChaosApiError> {
967 let replay_engine = state.replay_engine.read().await;
968 match replay_engine.stop().await {
969 Ok(_) => {
970 info!("Replay stopped");
971 Ok(Json(StatusResponse {
972 message: "Replay stopped".to_string(),
973 }))
974 }
975 Err(err) => Err(ChaosApiError::NotFound(err)),
976 }
977}
978
979async fn replay_status(State(state): State<ChaosApiState>) -> Json<ReplayStatusResponse> {
981 let replay_engine = state.replay_engine.read().await;
982 if let Some(status) = replay_engine.get_status() {
983 Json(ReplayStatusResponse {
984 is_replaying: status.is_playing,
985 scenario_name: Some(status.scenario_name),
986 progress: status.progress,
987 })
988 } else {
989 Json(ReplayStatusResponse {
990 is_replaying: false,
991 scenario_name: None,
992 progress: 0.0,
993 })
994 }
995}
996
997async fn start_orchestration(
999 State(state): State<ChaosApiState>,
1000 Json(req): Json<OrchestratedScenarioRequest>,
1001) -> Result<Json<StatusResponse>, ChaosApiError> {
1002 use crate::scenario_orchestrator::ScenarioStep;
1003
1004 let mut orchestrated = OrchestratedScenario::new(req.name.clone());
1006
1007 for step_value in req.steps {
1009 let step = serde_json::from_value::<ScenarioStep>(step_value)
1010 .map_err(|e| ChaosApiError::NotFound(format!("Invalid step: {}", e)))?;
1011 orchestrated = orchestrated.add_step(step);
1012 }
1013
1014 if req.parallel.unwrap_or(false) {
1016 orchestrated = orchestrated.with_parallel_execution();
1017 }
1018
1019 let mut orchestrator = state.orchestrator.write().await;
1021 orchestrator
1022 .execute(orchestrated.clone())
1023 .await
1024 .map_err(|e| ChaosApiError::NotFound(format!("Failed to start orchestration: {}", e)))?;
1025
1026 info!("Started orchestration '{}'", req.name);
1027
1028 Ok(Json(StatusResponse {
1029 message: format!("Orchestration '{}' started successfully", req.name),
1030 }))
1031}
1032
1033async fn stop_orchestration(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
1035 let orchestrator = state.orchestrator.read().await;
1036
1037 if orchestrator.is_running() {
1038 info!("Orchestration stop requested");
1041 Json(StatusResponse {
1042 message: "Orchestration stop requested (will complete current step)".to_string(),
1043 })
1044 } else {
1045 Json(StatusResponse {
1046 message: "No orchestration currently running".to_string(),
1047 })
1048 }
1049}
1050
1051async fn orchestration_status(
1053 State(state): State<ChaosApiState>,
1054) -> Json<OrchestrationStatusResponse> {
1055 let orchestrator = state.orchestrator.read().await;
1056
1057 if let Some(status) = orchestrator.get_status() {
1058 Json(OrchestrationStatusResponse {
1059 is_running: status.is_running,
1060 name: Some(status.name.clone()),
1061 progress: status.progress,
1062 })
1063 } else {
1064 Json(OrchestrationStatusResponse {
1065 is_running: false,
1066 name: None,
1067 progress: 0.0,
1068 })
1069 }
1070}
1071
1072async fn import_orchestration(
1074 State(_state): State<ChaosApiState>,
1075 Json(req): Json<ImportRequest>,
1076) -> Result<Json<StatusResponse>, ChaosApiError> {
1077 let orchestrated = if req.format == "json" {
1079 OrchestratedScenario::from_json(&req.content)
1080 .map_err(|e| ChaosApiError::NotFound(format!("Invalid JSON: {}", e)))?
1081 } else if req.format == "yaml" {
1082 OrchestratedScenario::from_yaml(&req.content)
1083 .map_err(|e| ChaosApiError::NotFound(format!("Invalid YAML: {}", e)))?
1084 } else {
1085 return Err(ChaosApiError::NotFound(
1086 "Unsupported format. Use 'json' or 'yaml'".to_string(),
1087 ));
1088 };
1089
1090 info!("Imported orchestration: {}", orchestrated.name);
1091
1092 Ok(Json(StatusResponse {
1093 message: format!(
1094 "Orchestration '{}' imported successfully ({} steps)",
1095 orchestrated.name,
1096 orchestrated.steps.len()
1097 ),
1098 }))
1099}
1100
1101async fn add_schedule(
1103 State(state): State<ChaosApiState>,
1104 Json(req): Json<ScheduledScenarioRequest>,
1105) -> Result<Json<StatusResponse>, ChaosApiError> {
1106 let scenario = serde_json::from_value::<ChaosScenario>(req.scenario)
1108 .map_err(|e| ChaosApiError::NotFound(format!("Invalid scenario: {}", e)))?;
1109
1110 let schedule = serde_json::from_value::<ScheduleType>(req.schedule)
1112 .map_err(|e| ChaosApiError::NotFound(format!("Invalid schedule: {}", e)))?;
1113
1114 let scheduled = ScheduledScenario::new(req.id.clone(), scenario, schedule);
1116
1117 let scheduler = state.scheduler.read().await;
1119 scheduler.add_schedule(scheduled);
1120
1121 info!("Schedule '{}' added", req.id);
1122 Ok(Json(StatusResponse {
1123 message: format!("Schedule '{}' added", req.id),
1124 }))
1125}
1126
1127async fn get_schedule(
1129 State(state): State<ChaosApiState>,
1130 Path(id): Path<String>,
1131) -> Result<Json<ScheduledScenario>, ChaosApiError> {
1132 let scheduler = state.scheduler.read().await;
1133 match scheduler.get_schedule(&id) {
1134 Some(scheduled) => Ok(Json(scheduled)),
1135 None => Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id))),
1136 }
1137}
1138
1139async fn remove_schedule(
1141 State(state): State<ChaosApiState>,
1142 Path(id): Path<String>,
1143) -> Result<Json<StatusResponse>, ChaosApiError> {
1144 let scheduler = state.scheduler.read().await;
1145 match scheduler.remove_schedule(&id) {
1146 Some(_) => {
1147 info!("Schedule '{}' removed", id);
1148 Ok(Json(StatusResponse {
1149 message: format!("Schedule '{}' removed", id),
1150 }))
1151 }
1152 None => Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id))),
1153 }
1154}
1155
1156async fn enable_schedule(
1158 State(state): State<ChaosApiState>,
1159 Path(id): Path<String>,
1160) -> Result<Json<StatusResponse>, ChaosApiError> {
1161 let scheduler = state.scheduler.read().await;
1162 match scheduler.enable_schedule(&id) {
1163 Ok(_) => {
1164 info!("Schedule '{}' enabled", id);
1165 Ok(Json(StatusResponse {
1166 message: format!("Schedule '{}' enabled", id),
1167 }))
1168 }
1169 Err(err) => Err(ChaosApiError::NotFound(err)),
1170 }
1171}
1172
1173async fn disable_schedule(
1175 State(state): State<ChaosApiState>,
1176 Path(id): Path<String>,
1177) -> Result<Json<StatusResponse>, ChaosApiError> {
1178 let scheduler = state.scheduler.read().await;
1179 match scheduler.disable_schedule(&id) {
1180 Ok(_) => {
1181 info!("Schedule '{}' disabled", id);
1182 Ok(Json(StatusResponse {
1183 message: format!("Schedule '{}' disabled", id),
1184 }))
1185 }
1186 Err(err) => Err(ChaosApiError::NotFound(err)),
1187 }
1188}
1189
1190#[allow(dead_code)]
1210async fn trigger_schedule_by_path(
1211 State(state): State<ChaosApiState>,
1212 Path(id): Path<String>,
1213) -> Result<Json<StatusResponse>, ChaosApiError> {
1214 let scheduler = state.scheduler.read().await;
1215 let schedule_exists = scheduler.get_schedule(&id).is_some();
1216
1217 if !schedule_exists {
1218 return Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id)));
1219 }
1220
1221 let trigger_result = scheduler.trigger_now(&id).await;
1222
1223 match trigger_result {
1224 Ok(_) => {
1225 info!("Schedule '{}' triggered", id);
1226 Ok(Json(StatusResponse {
1227 message: format!("Schedule '{}' triggered", id),
1228 }))
1229 }
1230 Err(err) => Err(ChaosApiError::NotFound(err)),
1231 }
1232}
1233
1234async fn list_schedules(State(state): State<ChaosApiState>) -> Json<Vec<ScheduleSummary>> {
1236 let scheduler = state.scheduler.read().await;
1237 let schedules = scheduler.get_all_schedules();
1238 let summaries = schedules
1239 .into_iter()
1240 .map(|s| ScheduleSummary {
1241 id: s.id,
1242 scenario_name: s.scenario.name,
1243 enabled: s.enabled,
1244 next_execution: s.next_execution.map(|t| t.to_rfc3339()),
1245 })
1246 .collect();
1247 Json(summaries)
1248}
1249
1250#[derive(Debug, Deserialize)]
1253struct StartRecordingRequest {
1254 scenario_name: String,
1255}
1256
1257#[derive(Debug, Deserialize)]
1258struct ExportRequest {
1259 path: String,
1260}
1261
1262#[derive(Debug, Serialize)]
1263struct RecordingStatusResponse {
1264 is_recording: bool,
1265 scenario_name: Option<String>,
1266 events_recorded: usize,
1267}
1268
1269#[derive(Debug, Deserialize)]
1270struct StartReplayRequest {
1271 path: String,
1272 speed: Option<f64>,
1273 loop_replay: Option<bool>,
1274}
1275
1276#[derive(Debug, Serialize)]
1277struct ReplayStatusResponse {
1278 is_replaying: bool,
1279 scenario_name: Option<String>,
1280 progress: f64,
1281}
1282
1283#[derive(Debug, Deserialize)]
1284struct OrchestratedScenarioRequest {
1285 name: String,
1286 steps: Vec<serde_json::Value>,
1287 parallel: Option<bool>,
1288}
1289
1290#[derive(Debug, Serialize)]
1291struct OrchestrationStatusResponse {
1292 is_running: bool,
1293 name: Option<String>,
1294 progress: f64,
1295}
1296
1297#[derive(Debug, Deserialize)]
1298struct ImportRequest {
1299 content: String,
1300 format: String, }
1302
1303#[derive(Debug, Deserialize)]
1304struct ScheduledScenarioRequest {
1305 id: String,
1306 scenario: serde_json::Value,
1307 schedule: serde_json::Value,
1308}
1309
1310#[derive(Debug, Deserialize, Serialize)]
1311struct ScheduleSummary {
1312 id: String,
1313 scenario_name: String,
1314 enabled: bool,
1315 next_execution: Option<String>,
1316}
1317
1318async fn get_recommendations(
1322 State(state): State<ChaosApiState>,
1323) -> Json<Vec<crate::recommendations::Recommendation>> {
1324 Json(state.recommendation_engine.get_recommendations())
1325}
1326
1327async fn analyze_and_recommend(State(state): State<ChaosApiState>) -> Json<AnalyzeResponse> {
1329 use chrono::{Duration, Utc};
1330
1331 let end = Utc::now();
1333 let start = end - Duration::hours(24);
1334
1335 let buckets = state.analytics.get_metrics(start, end, TimeBucket::Hour);
1336 let impact = state.analytics.get_impact_analysis(start, end, TimeBucket::Hour);
1337
1338 let recommendations = state.recommendation_engine.analyze_and_recommend(&buckets, &impact);
1339
1340 Json(AnalyzeResponse {
1341 total_recommendations: recommendations.len(),
1342 high_priority: recommendations
1343 .iter()
1344 .filter(|r| {
1345 matches!(
1346 r.severity,
1347 RecommendationSeverity::High | RecommendationSeverity::Critical
1348 )
1349 })
1350 .count(),
1351 recommendations,
1352 })
1353}
1354
1355async fn get_recommendations_by_category(
1357 State(state): State<ChaosApiState>,
1358 Path(category): Path<String>,
1359) -> Result<Json<Vec<crate::recommendations::Recommendation>>, StatusCode> {
1360 let category = match category.as_str() {
1361 "latency" => RecommendationCategory::Latency,
1362 "fault_injection" => RecommendationCategory::FaultInjection,
1363 "rate_limit" => RecommendationCategory::RateLimit,
1364 "traffic_shaping" => RecommendationCategory::TrafficShaping,
1365 "circuit_breaker" => RecommendationCategory::CircuitBreaker,
1366 "bulkhead" => RecommendationCategory::Bulkhead,
1367 "scenario" => RecommendationCategory::Scenario,
1368 "coverage" => RecommendationCategory::Coverage,
1369 _ => return Err(StatusCode::BAD_REQUEST),
1370 };
1371
1372 Ok(Json(state.recommendation_engine.get_recommendations_by_category(category)))
1373}
1374
1375async fn get_recommendations_by_severity(
1377 State(state): State<ChaosApiState>,
1378 Path(severity): Path<String>,
1379) -> Result<Json<Vec<crate::recommendations::Recommendation>>, StatusCode> {
1380 let severity = match severity.as_str() {
1381 "info" => RecommendationSeverity::Info,
1382 "low" => RecommendationSeverity::Low,
1383 "medium" => RecommendationSeverity::Medium,
1384 "high" => RecommendationSeverity::High,
1385 "critical" => RecommendationSeverity::Critical,
1386 _ => return Err(StatusCode::BAD_REQUEST),
1387 };
1388
1389 Ok(Json(state.recommendation_engine.get_recommendations_by_severity(severity)))
1390}
1391
1392async fn clear_recommendations(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
1394 state.recommendation_engine.clear();
1395 Json(StatusResponse {
1396 message: "Recommendations cleared".to_string(),
1397 })
1398}
1399
1400#[derive(Debug, Serialize)]
1401struct AnalyzeResponse {
1402 total_recommendations: usize,
1403 high_priority: usize,
1404 recommendations: Vec<crate::recommendations::Recommendation>,
1405}
1406
1407async fn get_remediation_config(State(state): State<ChaosApiState>) -> Json<RemediationConfig> {
1411 Json(state.remediation_engine.get_config())
1412}
1413
1414async fn update_remediation_config(
1416 State(state): State<ChaosApiState>,
1417 Json(config): Json<RemediationConfig>,
1418) -> Json<StatusResponse> {
1419 state.remediation_engine.update_config(config);
1420 Json(StatusResponse {
1421 message: "Remediation configuration updated".to_string(),
1422 })
1423}
1424
1425#[derive(Debug, Deserialize)]
1426struct ProcessRemediationRequest {
1427 recommendation: Recommendation,
1428}
1429
1430async fn process_remediation(
1432 State(state): State<ChaosApiState>,
1433 Json(req): Json<ProcessRemediationRequest>,
1434) -> Result<Json<serde_json::Value>, StatusCode> {
1435 match state.remediation_engine.process_recommendation(&req.recommendation) {
1436 Ok(action_id) => Ok(Json(serde_json::json!({
1437 "success": true,
1438 "action_id": action_id,
1439 "message": "Recommendation processed"
1440 }))),
1441 Err(err) => Ok(Json(serde_json::json!({
1442 "success": false,
1443 "error": err
1444 }))),
1445 }
1446}
1447
1448#[derive(Debug, Deserialize)]
1449struct ApproveRequest {
1450 approver: String,
1451}
1452
1453async fn approve_remediation(
1455 State(state): State<ChaosApiState>,
1456 Path(id): Path<String>,
1457 Json(req): Json<ApproveRequest>,
1458) -> Result<Json<StatusResponse>, StatusCode> {
1459 match state.remediation_engine.approve_action(&id, &req.approver) {
1460 Ok(_) => Ok(Json(StatusResponse {
1461 message: format!("Action {} approved", id),
1462 })),
1463 Err(_err) => Err(StatusCode::BAD_REQUEST),
1464 }
1465}
1466
1467#[derive(Debug, Deserialize)]
1468struct RejectRequest {
1469 reason: String,
1470}
1471
1472async fn reject_remediation(
1474 State(state): State<ChaosApiState>,
1475 Path(id): Path<String>,
1476 Json(req): Json<RejectRequest>,
1477) -> Result<Json<StatusResponse>, StatusCode> {
1478 match state.remediation_engine.reject_action(&id, &req.reason) {
1479 Ok(_) => Ok(Json(StatusResponse {
1480 message: format!("Action {} rejected", id),
1481 })),
1482 Err(_err) => Err(StatusCode::BAD_REQUEST),
1483 }
1484}
1485
1486async fn rollback_remediation(
1488 State(state): State<ChaosApiState>,
1489 Path(id): Path<String>,
1490) -> Result<Json<StatusResponse>, StatusCode> {
1491 match state.remediation_engine.rollback_action(&id) {
1492 Ok(_) => Ok(Json(StatusResponse {
1493 message: format!("Action {} rolled back", id),
1494 })),
1495 Err(_err) => Err(StatusCode::BAD_REQUEST),
1496 }
1497}
1498
1499async fn get_remediation_actions(
1501 State(state): State<ChaosApiState>,
1502) -> Json<Vec<crate::auto_remediation::RemediationAction>> {
1503 Json(state.remediation_engine.get_active_actions())
1504}
1505
1506async fn get_remediation_action(
1508 State(state): State<ChaosApiState>,
1509 Path(id): Path<String>,
1510) -> Result<Json<crate::auto_remediation::RemediationAction>, StatusCode> {
1511 match state.remediation_engine.get_action(&id) {
1512 Some(action) => Ok(Json(action)),
1513 None => Err(StatusCode::NOT_FOUND),
1514 }
1515}
1516
1517async fn get_approval_queue(
1519 State(state): State<ChaosApiState>,
1520) -> Json<Vec<crate::auto_remediation::ApprovalRequest>> {
1521 Json(state.remediation_engine.get_approval_queue())
1522}
1523
1524async fn get_remediation_effectiveness(
1526 State(state): State<ChaosApiState>,
1527 Path(id): Path<String>,
1528) -> Result<Json<crate::auto_remediation::EffectivenessMetrics>, StatusCode> {
1529 match state.remediation_engine.get_effectiveness(&id) {
1530 Some(metrics) => Ok(Json(metrics)),
1531 None => Err(StatusCode::NOT_FOUND),
1532 }
1533}
1534
1535async fn get_remediation_stats(
1537 State(state): State<ChaosApiState>,
1538) -> Json<crate::auto_remediation::RemediationStats> {
1539 Json(state.remediation_engine.get_stats())
1540}
1541
1542async fn create_ab_test(
1546 State(state): State<ChaosApiState>,
1547 Json(config): Json<ABTestConfig>,
1548) -> Result<Json<serde_json::Value>, StatusCode> {
1549 let engine = state.ab_testing_engine.read().await;
1550 match engine.create_test(config) {
1551 Ok(test_id) => Ok(Json(serde_json::json!({
1552 "success": true,
1553 "test_id": test_id
1554 }))),
1555 Err(err) => Ok(Json(serde_json::json!({
1556 "success": false,
1557 "error": err
1558 }))),
1559 }
1560}
1561
1562async fn get_ab_tests(State(state): State<ChaosApiState>) -> Json<Vec<crate::ab_testing::ABTest>> {
1564 let engine = state.ab_testing_engine.read().await;
1565 Json(engine.get_all_tests())
1566}
1567
1568async fn get_ab_test(
1570 State(state): State<ChaosApiState>,
1571 Path(id): Path<String>,
1572) -> Result<Json<crate::ab_testing::ABTest>, StatusCode> {
1573 let engine = state.ab_testing_engine.read().await;
1574 match engine.get_test(&id) {
1575 Some(test) => Ok(Json(test)),
1576 None => Err(StatusCode::NOT_FOUND),
1577 }
1578}
1579
1580async fn start_ab_test(
1582 State(state): State<ChaosApiState>,
1583 Path(id): Path<String>,
1584) -> Result<Json<StatusResponse>, StatusCode> {
1585 let engine = state.ab_testing_engine.read().await;
1586 match engine.start_test(&id) {
1587 Ok(_) => Ok(Json(StatusResponse {
1588 message: format!("Test {} started", id),
1589 })),
1590 Err(_err) => Err(StatusCode::BAD_REQUEST),
1591 }
1592}
1593
1594async fn stop_ab_test(
1596 State(state): State<ChaosApiState>,
1597 Path(id): Path<String>,
1598) -> Result<Json<TestConclusion>, StatusCode> {
1599 let engine = state.ab_testing_engine.read().await;
1600 match engine.stop_test(&id) {
1601 Ok(conclusion) => Ok(Json(conclusion)),
1602 Err(_err) => Err(StatusCode::BAD_REQUEST),
1603 }
1604}
1605
1606async fn pause_ab_test(
1608 State(state): State<ChaosApiState>,
1609 Path(id): Path<String>,
1610) -> Result<Json<StatusResponse>, StatusCode> {
1611 let engine = state.ab_testing_engine.read().await;
1612 match engine.pause_test(&id) {
1613 Ok(_) => Ok(Json(StatusResponse {
1614 message: format!("Test {} paused", id),
1615 })),
1616 Err(_err) => Err(StatusCode::BAD_REQUEST),
1617 }
1618}
1619
1620async fn resume_ab_test(
1622 State(state): State<ChaosApiState>,
1623 Path(id): Path<String>,
1624) -> Result<Json<StatusResponse>, StatusCode> {
1625 let engine = state.ab_testing_engine.read().await;
1626 match engine.resume_test(&id) {
1627 Ok(_) => Ok(Json(StatusResponse {
1628 message: format!("Test {} resumed", id),
1629 })),
1630 Err(_err) => Err(StatusCode::BAD_REQUEST),
1631 }
1632}
1633
1634async fn record_ab_test_result(
1636 State(state): State<ChaosApiState>,
1637 Path((id, variant)): Path<(String, String)>,
1638 Json(results): Json<VariantResults>,
1639) -> Result<Json<StatusResponse>, StatusCode> {
1640 let engine = state.ab_testing_engine.read().await;
1641 match engine.record_variant_result(&id, &variant, results) {
1642 Ok(_) => Ok(Json(StatusResponse {
1643 message: format!("Results recorded for variant {}", variant),
1644 })),
1645 Err(_err) => Err(StatusCode::BAD_REQUEST),
1646 }
1647}
1648
1649async fn delete_ab_test(
1651 State(state): State<ChaosApiState>,
1652 Path(id): Path<String>,
1653) -> Result<Json<StatusResponse>, StatusCode> {
1654 let engine = state.ab_testing_engine.read().await;
1655 match engine.delete_test(&id) {
1656 Ok(_) => Ok(Json(StatusResponse {
1657 message: format!("Test {} deleted", id),
1658 })),
1659 Err(_err) => Err(StatusCode::BAD_REQUEST),
1660 }
1661}
1662
1663async fn get_ab_test_stats(
1665 State(state): State<ChaosApiState>,
1666) -> Json<crate::ab_testing::ABTestStats> {
1667 let engine = state.ab_testing_engine.read().await;
1668 Json(engine.get_stats())
1669}
1670
1671async fn get_latency_metrics(State(state): State<ChaosApiState>) -> Json<LatencyMetricsResponse> {
1675 let samples = state.latency_tracker.get_samples();
1676 Json(LatencyMetricsResponse { samples })
1677}
1678
1679async fn get_latency_stats(
1681 State(state): State<ChaosApiState>,
1682) -> Json<crate::latency_metrics::LatencyStats> {
1683 let stats = state.latency_tracker.get_stats();
1684 Json(stats)
1685}
1686
1687#[derive(Debug, Serialize)]
1688struct LatencyMetricsResponse {
1689 samples: Vec<crate::latency_metrics::LatencySample>,
1690}
1691
1692async fn list_profiles(State(state): State<ChaosApiState>) -> Json<Vec<NetworkProfile>> {
1696 let profiles = state.profile_manager.get_all_profiles();
1697 Json(profiles)
1698}
1699
1700async fn get_profile(
1702 State(state): State<ChaosApiState>,
1703 Path(name): Path<String>,
1704) -> Result<Json<NetworkProfile>, ChaosApiError> {
1705 match state.profile_manager.get_profile(&name) {
1706 Some(profile) => Ok(Json(profile)),
1707 None => Err(ChaosApiError::NotFound(format!("Profile '{}' not found", name))),
1708 }
1709}
1710
1711async fn apply_profile(
1713 State(state): State<ChaosApiState>,
1714 Path(name): Path<String>,
1715) -> Result<Json<StatusResponse>, ChaosApiError> {
1716 let profile = state
1717 .profile_manager
1718 .get_profile(&name)
1719 .ok_or_else(|| ChaosApiError::NotFound(format!("Profile '{}' not found", name)))?;
1720
1721 let mut config = state.config.write().await;
1723 *config = profile.chaos_config.clone();
1724
1725 info!("Applied profile: {}", name);
1726 Ok(Json(StatusResponse {
1727 message: format!("Profile '{}' applied successfully", name),
1728 }))
1729}
1730
1731async fn create_profile(
1733 State(state): State<ChaosApiState>,
1734 Json(profile): Json<NetworkProfile>,
1735) -> Result<Json<StatusResponse>, ChaosApiError> {
1736 for builtin in NetworkProfile::predefined_profiles() {
1738 if builtin.name == profile.name {
1739 return Err(ChaosApiError::NotFound(format!(
1740 "Cannot create profile '{}': name conflicts with built-in profile",
1741 profile.name
1742 )));
1743 }
1744 }
1745
1746 let mut custom_profile = profile;
1748 custom_profile.builtin = false;
1749
1750 state.profile_manager.save_profile(custom_profile.clone());
1751 info!("Created custom profile: {}", custom_profile.name);
1752 Ok(Json(StatusResponse {
1753 message: format!("Profile '{}' created successfully", custom_profile.name),
1754 }))
1755}
1756
1757async fn delete_profile(
1759 State(state): State<ChaosApiState>,
1760 Path(name): Path<String>,
1761) -> Result<Json<StatusResponse>, ChaosApiError> {
1762 for builtin in NetworkProfile::predefined_profiles() {
1764 if builtin.name == name {
1765 return Err(ChaosApiError::NotFound(format!(
1766 "Cannot delete built-in profile '{}'",
1767 name
1768 )));
1769 }
1770 }
1771
1772 if state.profile_manager.delete_profile(&name) {
1773 info!("Deleted custom profile: {}", name);
1774 Ok(Json(StatusResponse {
1775 message: format!("Profile '{}' deleted successfully", name),
1776 }))
1777 } else {
1778 Err(ChaosApiError::NotFound(format!("Profile '{}' not found", name)))
1779 }
1780}
1781
1782async fn export_profile(
1784 State(state): State<ChaosApiState>,
1785 Path(name): Path<String>,
1786 Query(params): Query<std::collections::HashMap<String, String>>,
1787) -> Result<Response, ChaosApiError> {
1788 let profile = state
1789 .profile_manager
1790 .get_profile(&name)
1791 .ok_or_else(|| ChaosApiError::NotFound(format!("Profile '{}' not found", name)))?;
1792
1793 let format = params.get("format").map(|s| s.as_str()).unwrap_or("json");
1794
1795 if format == "yaml" {
1796 let yaml = serde_yaml::to_string(&profile).map_err(|e| {
1797 ChaosApiError::NotFound(format!("Failed to serialize profile to YAML: {}", e))
1798 })?;
1799 Ok(Response::builder()
1800 .status(StatusCode::OK)
1801 .header("Content-Type", "application/yaml")
1802 .body(axum::body::Body::from(yaml))
1803 .unwrap()
1804 .into_response())
1805 } else {
1806 let json = serde_json::to_value(&profile)
1808 .map_err(|e| ChaosApiError::NotFound(format!("Failed to serialize profile: {}", e)))?;
1809 Ok(Json(json).into_response())
1810 }
1811}
1812
1813async fn import_profile(
1815 State(state): State<ChaosApiState>,
1816 Json(req): Json<ImportProfileRequest>,
1817) -> Result<Json<StatusResponse>, ChaosApiError> {
1818 let profile: NetworkProfile = if req.format == "yaml" {
1819 serde_yaml::from_str(&req.content)
1820 .map_err(|e| ChaosApiError::NotFound(format!("Failed to parse YAML: {}", e)))?
1821 } else {
1822 serde_json::from_str(&req.content)
1823 .map_err(|e| ChaosApiError::NotFound(format!("Failed to parse JSON: {}", e)))?
1824 };
1825
1826 for builtin in NetworkProfile::predefined_profiles() {
1828 if builtin.name == profile.name {
1829 return Err(ChaosApiError::NotFound(format!(
1830 "Cannot import profile '{}': name conflicts with built-in profile",
1831 profile.name
1832 )));
1833 }
1834 }
1835
1836 let mut custom_profile = profile;
1838 custom_profile.builtin = false;
1839
1840 state.profile_manager.save_profile(custom_profile.clone());
1841 info!("Imported profile: {}", custom_profile.name);
1842 Ok(Json(StatusResponse {
1843 message: format!("Profile '{}' imported successfully", custom_profile.name),
1844 }))
1845}
1846
1847#[derive(Debug, Deserialize)]
1848struct ImportProfileRequest {
1849 content: String,
1850 format: String, }
1852
1853#[derive(Debug)]
1856enum ChaosApiError {
1857 NotFound(String),
1858}
1859
1860impl IntoResponse for ChaosApiError {
1861 fn into_response(self) -> Response {
1862 let (status, message) = match self {
1863 ChaosApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
1864 };
1865
1866 (status, Json(serde_json::json!({ "error": message }))).into_response()
1867 }
1868}