Skip to main content

mockforge_http/management/
chaos_admin.rs

1//! Chaos engineering + network profile admin endpoints
2//! (`GET|POST /__mockforge/chaos/config`,
3//! `GET /__mockforge/network/profiles`,
4//! `POST /__mockforge/network/profile/apply`).
5//!
6//! Split out of the original `management/ai_gen.rs` under #656 — these
7//! handlers were never AI-related, they just happened to live in the
8//! same file. They read and write `ManagementState.chaos_api_state` and
9//! `ManagementState.server_config`.
10//!
11//! Stays in `mockforge-http`: chaos-coupled handlers are explicitly
12//! out of scope for the #555 / #656 drain — `mockforge-chaos` would
13//! need its own extraction bucket before these could move anywhere.
14
15use axum::{
16    extract::State,
17    http::StatusCode,
18    response::{IntoResponse, Json},
19};
20use serde::Deserialize;
21use tracing::*;
22
23use super::ManagementState;
24
25/// Get current chaos engineering configuration
26pub(crate) async fn get_chaos_config(State(_state): State<ManagementState>) -> impl IntoResponse {
27    #[cfg(feature = "chaos")]
28    {
29        if let Some(chaos_state) = &_state.chaos_api_state {
30            let config = chaos_state.config.read().await;
31            // Convert ChaosConfig to JSON response format
32            Json(serde_json::json!({
33                "enabled": config.enabled,
34                "latency": config.latency.as_ref().map(|l| serde_json::to_value(l).unwrap_or(serde_json::Value::Null)),
35                "fault_injection": config.fault_injection.as_ref().map(|f| serde_json::to_value(f).unwrap_or(serde_json::Value::Null)),
36                "rate_limit": config.rate_limit.as_ref().map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)),
37                "traffic_shaping": config.traffic_shaping.as_ref().map(|t| serde_json::to_value(t).unwrap_or(serde_json::Value::Null)),
38            }))
39            .into_response()
40        } else {
41            // Chaos API not available, return default
42            Json(serde_json::json!({
43                "enabled": false,
44                "latency": null,
45                "fault_injection": null,
46                "rate_limit": null,
47                "traffic_shaping": null,
48            }))
49            .into_response()
50        }
51    }
52    #[cfg(not(feature = "chaos"))]
53    {
54        // Chaos feature not enabled
55        Json(serde_json::json!({
56            "enabled": false,
57            "latency": null,
58            "fault_injection": null,
59            "rate_limit": null,
60            "traffic_shaping": null,
61        }))
62        .into_response()
63    }
64}
65
66/// Request to update chaos configuration
67#[derive(Debug, Deserialize)]
68pub struct ChaosConfigUpdate {
69    /// Whether to enable chaos engineering
70    pub enabled: Option<bool>,
71    /// Latency configuration
72    pub latency: Option<serde_json::Value>,
73    /// Fault injection configuration
74    pub fault_injection: Option<serde_json::Value>,
75    /// Rate limiting configuration
76    pub rate_limit: Option<serde_json::Value>,
77    /// Traffic shaping configuration
78    pub traffic_shaping: Option<serde_json::Value>,
79}
80
81/// Update chaos engineering configuration
82pub(crate) async fn update_chaos_config(
83    State(_state): State<ManagementState>,
84    Json(_config_update): Json<ChaosConfigUpdate>,
85) -> impl IntoResponse {
86    #[cfg(feature = "chaos")]
87    {
88        if let Some(chaos_state) = &_state.chaos_api_state {
89            use mockforge_chaos::config::{
90                FaultInjectionConfig, LatencyConfig, RateLimitConfig, TrafficShapingConfig,
91            };
92
93            let mut config = chaos_state.config.write().await;
94
95            // Update enabled flag if provided
96            if let Some(enabled) = _config_update.enabled {
97                config.enabled = enabled;
98            }
99
100            // Update latency config if provided
101            if let Some(latency_json) = _config_update.latency {
102                if let Ok(latency) = serde_json::from_value::<LatencyConfig>(latency_json) {
103                    config.latency = Some(latency);
104                }
105            }
106
107            // Update fault injection config if provided
108            if let Some(fault_json) = _config_update.fault_injection {
109                if let Ok(fault) = serde_json::from_value::<FaultInjectionConfig>(fault_json) {
110                    config.fault_injection = Some(fault);
111                }
112            }
113
114            // Update rate limit config if provided
115            if let Some(rate_json) = _config_update.rate_limit {
116                if let Ok(rate) = serde_json::from_value::<RateLimitConfig>(rate_json) {
117                    config.rate_limit = Some(rate);
118                }
119            }
120
121            // Update traffic shaping config if provided
122            if let Some(traffic_json) = _config_update.traffic_shaping {
123                if let Ok(traffic) = serde_json::from_value::<TrafficShapingConfig>(traffic_json) {
124                    config.traffic_shaping = Some(traffic);
125                }
126            }
127
128            // Reinitialize middleware injectors with new config
129            // The middleware will pick up the changes on the next request
130            drop(config);
131
132            info!("Chaos configuration updated successfully");
133            Json(serde_json::json!({
134                "success": true,
135                "message": "Chaos configuration updated and applied"
136            }))
137            .into_response()
138        } else {
139            (
140                StatusCode::SERVICE_UNAVAILABLE,
141                Json(serde_json::json!({
142                    "success": false,
143                    "error": "Chaos API not available",
144                    "message": "Chaos engineering is not enabled or configured"
145                })),
146            )
147                .into_response()
148        }
149    }
150    #[cfg(not(feature = "chaos"))]
151    {
152        (
153            StatusCode::NOT_IMPLEMENTED,
154            Json(serde_json::json!({
155                "success": false,
156                "error": "Chaos feature not enabled",
157                "message": "Chaos engineering feature is not compiled into this build"
158            })),
159        )
160            .into_response()
161    }
162}
163
164/// List available network profiles
165pub(crate) async fn list_network_profiles() -> impl IntoResponse {
166    use mockforge_chaos::core_network_profiles::NetworkProfileCatalog;
167
168    let catalog = NetworkProfileCatalog::default();
169    let profiles: Vec<serde_json::Value> = catalog
170        .list_profiles_with_description()
171        .iter()
172        .map(|(name, description)| {
173            serde_json::json!({
174                "name": name,
175                "description": description,
176            })
177        })
178        .collect();
179
180    Json(serde_json::json!({
181        "profiles": profiles
182    }))
183    .into_response()
184}
185
186#[derive(Debug, Deserialize)]
187/// Request to apply a network profile
188pub struct ApplyNetworkProfileRequest {
189    /// Name of the network profile to apply
190    pub profile_name: String,
191}
192
193/// Apply a network profile
194pub(crate) async fn apply_network_profile(
195    State(state): State<ManagementState>,
196    Json(request): Json<ApplyNetworkProfileRequest>,
197) -> impl IntoResponse {
198    use mockforge_chaos::core_network_profiles::NetworkProfileCatalog;
199
200    let catalog = NetworkProfileCatalog::default();
201    if let Some(profile) = catalog.get(&request.profile_name) {
202        // Apply profile to server configuration if available
203        // NetworkProfile contains latency and traffic_shaping configs
204        if let Some(server_config) = &state.server_config {
205            let mut config = server_config.write().await;
206
207            // Apply network profile's traffic shaping to core config
208            use mockforge_core::config::NetworkShapingConfig;
209
210            // Convert NetworkProfile's TrafficShapingConfig to NetworkShapingConfig
211            // NetworkProfile uses mockforge_core::traffic_shaping::TrafficShapingConfig
212            // which has bandwidth and burst_loss fields
213            let network_shaping = NetworkShapingConfig {
214                enabled: profile.traffic_shaping.bandwidth.enabled
215                    || profile.traffic_shaping.burst_loss.enabled,
216                bandwidth_limit_bps: profile.traffic_shaping.bandwidth.max_bytes_per_sec * 8, // Convert bytes to bits
217                packet_loss_percent: profile.traffic_shaping.burst_loss.loss_rate_during_burst,
218                max_connections: 1000, // Default value
219            };
220
221            // Update chaos config if it exists, or create it
222            // Chaos config is in observability.chaos, not core.chaos
223            if let Some(ref mut chaos) = config.observability.chaos {
224                chaos.traffic_shaping = Some(network_shaping);
225            } else {
226                // Create minimal chaos config with traffic shaping
227                use mockforge_core::config::ChaosEngConfig;
228                config.observability.chaos = Some(ChaosEngConfig {
229                    enabled: true,
230                    latency: None,
231                    fault_injection: None,
232                    rate_limit: None,
233                    traffic_shaping: Some(network_shaping),
234                    scenario: None,
235                });
236            }
237
238            info!("Network profile '{}' applied to server configuration", request.profile_name);
239        } else {
240            warn!("Server configuration not available in ManagementState - profile applied but not persisted");
241        }
242
243        // Also update chaos API state if available
244        #[cfg(feature = "chaos")]
245        {
246            if let Some(chaos_state) = &state.chaos_api_state {
247                use mockforge_chaos::config::TrafficShapingConfig;
248
249                let mut chaos_config = chaos_state.config.write().await;
250                // Apply profile's traffic shaping to chaos API state
251                let chaos_traffic_shaping = TrafficShapingConfig {
252                    enabled: profile.traffic_shaping.bandwidth.enabled
253                        || profile.traffic_shaping.burst_loss.enabled,
254                    bandwidth_limit_bps: profile.traffic_shaping.bandwidth.max_bytes_per_sec * 8, // Convert bytes to bits
255                    packet_loss_percent: profile.traffic_shaping.burst_loss.loss_rate_during_burst,
256                    max_connections: 0,
257                    connection_timeout_ms: 30000,
258                };
259                chaos_config.traffic_shaping = Some(chaos_traffic_shaping);
260                chaos_config.enabled = true; // Enable chaos when applying a profile
261                drop(chaos_config);
262                info!("Network profile '{}' applied to chaos API state", request.profile_name);
263            }
264        }
265
266        Json(serde_json::json!({
267            "success": true,
268            "message": format!("Network profile '{}' applied", request.profile_name),
269            "profile": {
270                "name": profile.name,
271                "description": profile.description,
272            }
273        }))
274        .into_response()
275    } else {
276        (
277            StatusCode::NOT_FOUND,
278            Json(serde_json::json!({
279                "error": "Profile not found",
280                "message": format!("Network profile '{}' not found", request.profile_name)
281            })),
282        )
283            .into_response()
284    }
285}