mockforge_chaos/
failure_designer.rs

1//! "What-If" Failure Designer
2//!
3//! This module provides a UI-friendly way to design failure scenarios with conditional rules.
4//! Users can specify "break all webhooks for 10% of users on Chrome only" and the system
5//! generates the appropriate chaos rules and hooks.
6
7use crate::config::{ChaosConfig, FaultInjectionConfig, LatencyConfig};
8use crate::scenarios::ChaosScenario;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use std::collections::HashMap;
12
13/// Failure design rule
14///
15/// Specifies a failure scenario with target conditions and failure type.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FailureDesignRule {
18    /// Rule name
19    pub name: String,
20    /// Target specification (endpoints, user agents, IP ranges, etc.)
21    pub target: FailureTarget,
22    /// Type of failure to inject
23    pub failure_type: FailureType,
24    /// Additional conditions for matching
25    #[serde(default)]
26    pub conditions: Vec<FailureCondition>,
27    /// Probability/percentage of requests to affect (0.0 to 1.0)
28    pub probability: f64,
29    /// Rule description
30    #[serde(default)]
31    pub description: Option<String>,
32}
33
34/// Target specification for failure injection
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct FailureTarget {
37    /// Endpoint patterns to match (supports wildcards)
38    #[serde(default)]
39    pub endpoints: Vec<String>,
40    /// User agent patterns (regex patterns)
41    #[serde(default)]
42    pub user_agents: Option<Vec<String>>,
43    /// IP address ranges (CIDR notation or specific IPs)
44    #[serde(default)]
45    pub ip_ranges: Option<Vec<String>>,
46    /// Header matching rules
47    #[serde(default)]
48    pub headers: Option<HashMap<String, String>>,
49    /// HTTP methods to match
50    #[serde(default)]
51    pub methods: Option<Vec<String>>,
52}
53
54/// Type of failure to inject
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum FailureType {
58    /// Webhook failure - break webhook execution
59    WebhookFailure {
60        /// Webhook URL pattern to match (supports wildcards)
61        webhook_pattern: String,
62    },
63    /// HTTP status code injection
64    StatusCode {
65        /// Status code to return
66        code: u16,
67    },
68    /// Latency injection
69    Latency {
70        /// Delay in milliseconds
71        delay_ms: u64,
72    },
73    /// Timeout error
74    Timeout {
75        /// Timeout duration in milliseconds
76        timeout_ms: u64,
77    },
78    /// Connection error
79    ConnectionError,
80    /// Partial response (incomplete data)
81    PartialResponse {
82        /// Percentage of response to truncate (0.0 to 1.0)
83        truncate_percentage: f64,
84    },
85}
86
87/// Condition for matching requests
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct FailureCondition {
90    /// Condition type
91    pub condition_type: ConditionType,
92    /// Field to check (e.g., "header.User-Agent", "query.param", "body.field")
93    pub field: String,
94    /// Operator for comparison
95    pub operator: ConditionOperator,
96    /// Value to compare against
97    pub value: Value,
98}
99
100/// Type of condition
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum ConditionType {
104    /// Match header value
105    Header,
106    /// Match query parameter
107    Query,
108    /// Match request body field
109    Body,
110    /// Match path parameter
111    Path,
112}
113
114/// Condition operator
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum ConditionOperator {
118    /// Equals
119    Equals,
120    /// Not equals
121    NotEquals,
122    /// Contains (for strings)
123    Contains,
124    /// Matches regex
125    Matches,
126    /// Greater than (for numbers)
127    GreaterThan,
128    /// Less than (for numbers)
129    LessThan,
130}
131
132/// Failure designer
133///
134/// Converts failure design rules into chaos configurations and hooks.
135pub struct FailureDesigner;
136
137impl FailureDesigner {
138    /// Create a new failure designer
139    pub fn new() -> Self {
140        Self
141    }
142
143    /// Validate a failure design rule
144    pub fn validate_rule(&self, rule: &FailureDesignRule) -> Result<(), String> {
145        // Validate probability
146        if rule.probability < 0.0 || rule.probability > 1.0 {
147            return Err("Probability must be between 0.0 and 1.0".to_string());
148        }
149
150        // Validate target
151        if rule.target.endpoints.is_empty() {
152            return Err("At least one endpoint must be specified".to_string());
153        }
154
155        // Validate failure type
156        match &rule.failure_type {
157            FailureType::WebhookFailure { webhook_pattern } => {
158                if webhook_pattern.is_empty() {
159                    return Err("Webhook pattern cannot be empty".to_string());
160                }
161            }
162            FailureType::StatusCode { code } => {
163                if *code < 100 || *code > 599 {
164                    return Err("Status code must be between 100 and 599".to_string());
165                }
166            }
167            FailureType::Latency { delay_ms } => {
168                if *delay_ms == 0 {
169                    return Err("Delay must be greater than 0".to_string());
170                }
171            }
172            FailureType::Timeout { timeout_ms } => {
173                if *timeout_ms == 0 {
174                    return Err("Timeout must be greater than 0".to_string());
175                }
176            }
177            FailureType::PartialResponse {
178                truncate_percentage,
179            } => {
180                if *truncate_percentage < 0.0 || *truncate_percentage > 1.0 {
181                    return Err("Truncate percentage must be between 0.0 and 1.0".to_string());
182                }
183            }
184            FailureType::ConnectionError => {
185                // No validation needed
186            }
187        }
188
189        Ok(())
190    }
191
192    /// Convert a failure design rule to a chaos scenario
193    pub fn rule_to_scenario(&self, rule: &FailureDesignRule) -> Result<ChaosScenario, String> {
194        // Validate rule first
195        self.validate_rule(rule)?;
196
197        // Create chaos config based on failure type
198        let chaos_config = match &rule.failure_type {
199            FailureType::StatusCode { code } => {
200                let fault_config = FaultInjectionConfig {
201                    enabled: true,
202                    http_errors: vec![*code],
203                    http_error_probability: rule.probability,
204                    connection_errors: false,
205                    connection_error_probability: 0.0,
206                    timeout_errors: false,
207                    timeout_ms: 5000,
208                    timeout_probability: 0.0,
209                    partial_responses: false,
210                    partial_response_probability: 0.0,
211                    payload_corruption: false,
212                    payload_corruption_probability: 0.0,
213                    corruption_type: crate::config::CorruptionType::None,
214                    error_pattern: None,
215                    mockai_enabled: false,
216                };
217
218                ChaosConfig {
219                    enabled: true,
220                    latency: None,
221                    fault_injection: Some(fault_config),
222                    rate_limit: None,
223                    traffic_shaping: None,
224                    circuit_breaker: None,
225                    bulkhead: None,
226                }
227            }
228            FailureType::Latency { delay_ms } => {
229                let latency_config = LatencyConfig {
230                    enabled: true,
231                    fixed_delay_ms: Some(*delay_ms),
232                    random_delay_range_ms: None,
233                    jitter_percent: 0.0,
234                    probability: rule.probability,
235                };
236
237                ChaosConfig {
238                    enabled: true,
239                    latency: Some(latency_config),
240                    fault_injection: None,
241                    rate_limit: None,
242                    traffic_shaping: None,
243                    circuit_breaker: None,
244                    bulkhead: None,
245                }
246            }
247            FailureType::Timeout { timeout_ms } => {
248                let fault_config = FaultInjectionConfig {
249                    enabled: true,
250                    http_errors: vec![],
251                    http_error_probability: 0.0,
252                    connection_errors: false,
253                    connection_error_probability: 0.0,
254                    timeout_errors: true,
255                    timeout_ms: *timeout_ms,
256                    timeout_probability: rule.probability,
257                    partial_responses: false,
258                    partial_response_probability: 0.0,
259                    payload_corruption: false,
260                    payload_corruption_probability: 0.0,
261                    corruption_type: crate::config::CorruptionType::None,
262                    error_pattern: None,
263                    mockai_enabled: false,
264                };
265
266                ChaosConfig {
267                    enabled: true,
268                    latency: None,
269                    fault_injection: Some(fault_config),
270                    rate_limit: None,
271                    traffic_shaping: None,
272                    circuit_breaker: None,
273                    bulkhead: None,
274                }
275            }
276            FailureType::ConnectionError => {
277                let fault_config = FaultInjectionConfig {
278                    enabled: true,
279                    http_errors: vec![],
280                    http_error_probability: 0.0,
281                    connection_errors: true,
282                    connection_error_probability: rule.probability,
283                    timeout_errors: false,
284                    timeout_ms: 5000,
285                    timeout_probability: 0.0,
286                    partial_responses: false,
287                    partial_response_probability: 0.0,
288                    payload_corruption: false,
289                    payload_corruption_probability: 0.0,
290                    corruption_type: crate::config::CorruptionType::None,
291                    error_pattern: None,
292                    mockai_enabled: false,
293                };
294
295                ChaosConfig {
296                    enabled: true,
297                    latency: None,
298                    fault_injection: Some(fault_config),
299                    rate_limit: None,
300                    traffic_shaping: None,
301                    circuit_breaker: None,
302                    bulkhead: None,
303                }
304            }
305            FailureType::PartialResponse {
306                truncate_percentage,
307            } => {
308                let fault_config = FaultInjectionConfig {
309                    enabled: true,
310                    http_errors: vec![],
311                    http_error_probability: 0.0,
312                    connection_errors: false,
313                    connection_error_probability: 0.0,
314                    timeout_errors: false,
315                    timeout_ms: 5000,
316                    timeout_probability: 0.0,
317                    partial_responses: true,
318                    partial_response_probability: rule.probability,
319                    payload_corruption: false,
320                    payload_corruption_probability: 0.0,
321                    corruption_type: crate::config::CorruptionType::None,
322                    error_pattern: None,
323                    mockai_enabled: false,
324                };
325
326                ChaosConfig {
327                    enabled: true,
328                    latency: None,
329                    fault_injection: Some(fault_config),
330                    rate_limit: None,
331                    traffic_shaping: None,
332                    circuit_breaker: None,
333                    bulkhead: None,
334                }
335            }
336            FailureType::WebhookFailure { .. } => {
337                // Webhook failures are handled via hooks, not chaos config
338                // Return a minimal config
339                ChaosConfig::default()
340            }
341        };
342
343        // Create scenario
344        let scenario = ChaosScenario::new(rule.name.clone(), chaos_config)
345            .with_description(rule.description.clone().unwrap_or_default());
346
347        Ok(scenario)
348    }
349
350    /// Generate webhook failure hook configuration
351    ///
352    /// For webhook failure rules, generates the hook configuration that will
353    /// intercept and fail webhook executions.
354    pub fn generate_webhook_hook(&self, rule: &FailureDesignRule) -> Result<Value, String> {
355        if let FailureType::WebhookFailure { webhook_pattern } = &rule.failure_type {
356            // Generate hook configuration for webhook failure
357            Ok(json!({
358                "type": "webhook_failure",
359                "name": rule.name,
360                "webhook_pattern": webhook_pattern,
361                "probability": rule.probability,
362                "target": rule.target,
363                "conditions": rule.conditions,
364            }))
365        } else {
366            Err("Rule is not a webhook failure type".to_string())
367        }
368    }
369
370    /// Generate route chaos configuration
371    ///
372    /// Converts failure design rules into route-specific chaos configurations
373    /// that can be used with RouteChaosInjector.
374    pub fn generate_route_chaos_config(&self, rule: &FailureDesignRule) -> Result<Value, String> {
375        // Validate rule
376        self.validate_rule(rule)?;
377
378        // Build route config
379        let mut route_configs = Vec::new();
380
381        for endpoint in &rule.target.endpoints {
382            let mut route_config = json!({
383                "path": endpoint,
384                "probability": rule.probability,
385            });
386
387            // Add method filter if specified
388            if let Some(methods) = &rule.target.methods {
389                route_config["methods"] = json!(methods);
390            }
391
392            // Add failure type configuration
393            match &rule.failure_type {
394                FailureType::StatusCode { code } => {
395                    route_config["fault_injection"] = json!({
396                        "enabled": true,
397                        "status_code": code,
398                    });
399                }
400                FailureType::Latency { delay_ms } => {
401                    route_config["latency"] = json!({
402                        "enabled": true,
403                        "delay_ms": delay_ms,
404                    });
405                }
406                FailureType::Timeout { timeout_ms } => {
407                    route_config["fault_injection"] = json!({
408                        "enabled": true,
409                        "timeout": true,
410                        "timeout_ms": timeout_ms,
411                    });
412                }
413                FailureType::ConnectionError => {
414                    route_config["fault_injection"] = json!({
415                        "enabled": true,
416                        "connection_error": true,
417                    });
418                }
419                FailureType::PartialResponse {
420                    truncate_percentage,
421                } => {
422                    route_config["fault_injection"] = json!({
423                        "enabled": true,
424                        "partial_response": true,
425                        "truncate_percentage": truncate_percentage,
426                    });
427                }
428                FailureType::WebhookFailure { .. } => {
429                    // Webhook failures are handled separately
430                    continue;
431                }
432            }
433
434            // Add condition matching
435            if !rule.conditions.is_empty() {
436                route_config["conditions"] = json!(rule.conditions);
437            }
438
439            // Add user agent filter
440            if let Some(user_agents) = &rule.target.user_agents {
441                route_config["user_agent_patterns"] = json!(user_agents);
442            }
443
444            // Add IP range filter
445            if let Some(ip_ranges) = &rule.target.ip_ranges {
446                route_config["ip_ranges"] = json!(ip_ranges);
447            }
448
449            // Add header filters
450            if let Some(headers) = &rule.target.headers {
451                route_config["header_filters"] = json!(headers);
452            }
453
454            route_configs.push(route_config);
455        }
456
457        Ok(json!({
458            "routes": route_configs,
459        }))
460    }
461}
462
463impl Default for FailureDesigner {
464    fn default() -> Self {
465        Self::new()
466    }
467}