mockforge_chaos/
gitops.rs

1//! GitOps workflow support for chaos orchestrations
2//!
3//! Provides integration with GitOps tools like Flux and ArgoCD for
4//! managing chaos orchestrations declaratively.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11/// GitOps repository configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GitOpsConfig {
14    /// Repository URL
15    pub repo_url: String,
16    /// Branch to watch
17    pub branch: String,
18    /// Path within repository
19    pub path: PathBuf,
20    /// Sync interval in seconds
21    pub sync_interval_seconds: u64,
22    /// Authentication
23    pub auth: GitOpsAuth,
24    /// Auto-sync enabled
25    pub auto_sync: bool,
26    /// Prune on sync (delete removed orchestrations)
27    pub prune: bool,
28}
29
30/// Authentication for Git repository
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(tag = "type")]
33pub enum GitOpsAuth {
34    #[serde(rename = "ssh")]
35    SSH { private_key_path: PathBuf },
36    #[serde(rename = "token")]
37    Token { token: String },
38    #[serde(rename = "basic")]
39    Basic { username: String, password: String },
40}
41
42/// GitOps sync status
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SyncStatus {
45    pub last_sync: DateTime<Utc>,
46    pub commit_hash: String,
47    pub status: SyncState,
48    pub orchestrations_synced: usize,
49    pub errors: Vec<String>,
50}
51
52/// Sync state
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "lowercase")]
55pub enum SyncState {
56    Synced,
57    OutOfSync,
58    Syncing,
59    Failed,
60}
61
62/// Orchestration manifest in GitOps repository
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct OrchestrationManifest {
65    pub file_path: PathBuf,
66    pub content: serde_json::Value,
67    pub hash: String,
68    pub last_modified: DateTime<Utc>,
69}
70
71/// GitOps manager
72pub struct GitOpsManager {
73    config: GitOpsConfig,
74    manifests: HashMap<String, OrchestrationManifest>,
75    last_sync_status: Option<SyncStatus>,
76}
77
78impl GitOpsManager {
79    /// Create a new GitOps manager
80    pub fn new(config: GitOpsConfig) -> Self {
81        Self {
82            config,
83            manifests: HashMap::new(),
84            last_sync_status: None,
85        }
86    }
87
88    /// Sync orchestrations from Git repository
89    pub async fn sync(&mut self) -> Result<SyncStatus, String> {
90        // In a real implementation, this would:
91        // 1. Clone/pull the Git repository
92        // 2. Scan for orchestration YAML files
93        // 3. Parse and validate them
94        // 4. Apply changes (create/update/delete orchestrations)
95        // 5. Return sync status
96
97        let start_time = Utc::now();
98
99        // Simulate sync process
100        let manifests = self.discover_manifests().await?;
101        let _changes: Vec<()> = self.calculate_changes(&manifests)?;
102
103        // Apply changes - not implemented
104        let mut errors = Vec::new();
105
106        // Cleanup (prune) if enabled
107        if self.config.prune {
108            if let Err(e) = self.prune_removed_orchestrations(&manifests).await {
109                errors.push(format!("Failed to prune: {}", e));
110            }
111        }
112
113        let status = SyncStatus {
114            last_sync: start_time,
115            commit_hash: "abc123def456".to_string(), // Would be actual git hash
116            status: if errors.is_empty() {
117                SyncState::Synced
118            } else {
119                SyncState::Failed
120            },
121            orchestrations_synced: manifests.len(),
122            errors,
123        };
124
125        self.last_sync_status = Some(status.clone());
126        Ok(status)
127    }
128
129    /// Discover orchestration manifests in repository
130    async fn discover_manifests(&self) -> Result<Vec<OrchestrationManifest>, String> {
131        // In real implementation: scan repository for YAML files
132        Ok(Vec::new())
133    }
134
135    /// Calculate changes between current and desired state
136    fn calculate_changes(&self, _manifests: &[OrchestrationManifest]) -> Result<Vec<()>, String> {
137        // Compare manifests with currently deployed orchestrations
138        // Return list of changes (create, update, delete)
139        Ok(Vec::new())
140    }
141
142    /// Prune orchestrations that are no longer in Git
143    async fn prune_removed_orchestrations(
144        &mut self,
145        current_manifests: &[OrchestrationManifest],
146    ) -> Result<(), String> {
147        let current_names: Vec<String> = current_manifests
148            .iter()
149            .map(|m| m.file_path.to_string_lossy().to_string())
150            .collect();
151
152        self.manifests.retain(|name, _| current_names.contains(name));
153
154        Ok(())
155    }
156
157    /// Get current sync status
158    pub fn get_status(&self) -> Option<&SyncStatus> {
159        self.last_sync_status.as_ref()
160    }
161
162    /// Check if auto-sync is enabled
163    pub fn is_auto_sync_enabled(&self) -> bool {
164        self.config.auto_sync
165    }
166
167    /// Get sync interval
168    pub fn get_sync_interval(&self) -> u64 {
169        self.config.sync_interval_seconds
170    }
171
172    /// Start auto-sync loop
173    pub async fn start_auto_sync(&mut self) -> Result<(), String> {
174        if !self.config.auto_sync {
175            return Err("Auto-sync is not enabled".to_string());
176        }
177
178        loop {
179            match self.sync().await {
180                Ok(status) => {
181                    println!("Sync completed: {:?}", status.status);
182                }
183                Err(e) => {
184                    eprintln!("Sync failed: {}", e);
185                }
186            }
187
188            tokio::time::sleep(tokio::time::Duration::from_secs(self.config.sync_interval_seconds))
189                .await;
190        }
191    }
192}
193
194/// Flux integration
195pub mod flux {
196    use super::*;
197
198    /// Flux Kustomization configuration
199    #[derive(Debug, Clone, Serialize, Deserialize)]
200    pub struct FluxKustomization {
201        pub api_version: String,
202        pub kind: String,
203        pub metadata: FluxMetadata,
204        pub spec: FluxSpec,
205    }
206
207    #[derive(Debug, Clone, Serialize, Deserialize)]
208    pub struct FluxMetadata {
209        pub name: String,
210        pub namespace: String,
211    }
212
213    #[derive(Debug, Clone, Serialize, Deserialize)]
214    pub struct FluxSpec {
215        pub interval: String,
216        pub path: String,
217        pub prune: bool,
218        pub source_ref: SourceRef,
219    }
220
221    #[derive(Debug, Clone, Serialize, Deserialize)]
222    #[serde(rename_all = "camelCase")]
223    pub struct SourceRef {
224        pub kind: String,
225        pub name: String,
226    }
227
228    impl FluxKustomization {
229        /// Create a new Flux Kustomization for MockForge orchestrations
230        pub fn new_for_orchestrations(
231            name: String,
232            namespace: String,
233            git_repo: String,
234            path: String,
235        ) -> Self {
236            Self {
237                api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
238                kind: "Kustomization".to_string(),
239                metadata: FluxMetadata {
240                    name: name.clone(),
241                    namespace,
242                },
243                spec: FluxSpec {
244                    interval: "5m".to_string(),
245                    path,
246                    prune: true,
247                    source_ref: SourceRef {
248                        kind: "GitRepository".to_string(),
249                        name: git_repo,
250                    },
251                },
252            }
253        }
254
255        /// Convert to YAML
256        pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
257            serde_yaml::to_string(self)
258        }
259    }
260}
261
262/// ArgoCD integration
263pub mod argocd {
264    use super::*;
265
266    /// ArgoCD Application configuration
267    #[derive(Debug, Clone, Serialize, Deserialize)]
268    pub struct ArgoApplication {
269        pub api_version: String,
270        pub kind: String,
271        pub metadata: ArgoMetadata,
272        pub spec: ArgoSpec,
273    }
274
275    #[derive(Debug, Clone, Serialize, Deserialize)]
276    pub struct ArgoMetadata {
277        pub name: String,
278        pub namespace: String,
279    }
280
281    #[derive(Debug, Clone, Serialize, Deserialize)]
282    pub struct ArgoSpec {
283        pub project: String,
284        pub source: ArgoSource,
285        pub destination: ArgoDestination,
286        pub sync_policy: Option<SyncPolicy>,
287    }
288
289    #[derive(Debug, Clone, Serialize, Deserialize)]
290    #[serde(rename_all = "camelCase")]
291    pub struct ArgoSource {
292        pub repo_url: String,
293        pub target_revision: String,
294        pub path: String,
295    }
296
297    #[derive(Debug, Clone, Serialize, Deserialize)]
298    pub struct ArgoDestination {
299        pub server: String,
300        pub namespace: String,
301    }
302
303    #[derive(Debug, Clone, Serialize, Deserialize)]
304    #[serde(rename_all = "camelCase")]
305    pub struct SyncPolicy {
306        pub automated: Option<AutomatedSync>,
307        pub sync_options: Vec<String>,
308    }
309
310    #[derive(Debug, Clone, Serialize, Deserialize)]
311    pub struct AutomatedSync {
312        pub prune: bool,
313        #[serde(rename = "selfHeal")]
314        pub self_heal: bool,
315    }
316
317    impl ArgoApplication {
318        /// Create a new ArgoCD Application for MockForge orchestrations
319        pub fn new_for_orchestrations(
320            name: String,
321            namespace: String,
322            repo_url: String,
323            path: String,
324            auto_sync: bool,
325        ) -> Self {
326            Self {
327                api_version: "argoproj.io/v1alpha1".to_string(),
328                kind: "Application".to_string(),
329                metadata: ArgoMetadata {
330                    name: name.clone(),
331                    namespace: namespace.clone(),
332                },
333                spec: ArgoSpec {
334                    project: "default".to_string(),
335                    source: ArgoSource {
336                        repo_url,
337                        target_revision: "HEAD".to_string(),
338                        path,
339                    },
340                    destination: ArgoDestination {
341                        server: "https://kubernetes.default.svc".to_string(),
342                        namespace,
343                    },
344                    sync_policy: if auto_sync {
345                        Some(SyncPolicy {
346                            automated: Some(AutomatedSync {
347                                prune: true,
348                                self_heal: true,
349                            }),
350                            sync_options: vec!["CreateNamespace=true".to_string()],
351                        })
352                    } else {
353                        None
354                    },
355                },
356            }
357        }
358
359        /// Convert to YAML
360        pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
361            serde_yaml::to_string(self)
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_gitops_manager_creation() {
372        let config = GitOpsConfig {
373            repo_url: "https://github.com/example/chaos-configs".to_string(),
374            branch: "main".to_string(),
375            path: PathBuf::from("orchestrations"),
376            sync_interval_seconds: 300,
377            auth: GitOpsAuth::Token {
378                token: "test-token".to_string(),
379            },
380            auto_sync: true,
381            prune: true,
382        };
383
384        let manager = GitOpsManager::new(config);
385        assert!(manager.is_auto_sync_enabled());
386        assert_eq!(manager.get_sync_interval(), 300);
387    }
388
389    #[test]
390    fn test_flux_kustomization() {
391        let kustomization = flux::FluxKustomization::new_for_orchestrations(
392            "chaos-orchestrations".to_string(),
393            "chaos-testing".to_string(),
394            "chaos-repo".to_string(),
395            "./orchestrations".to_string(),
396        );
397
398        assert_eq!(kustomization.metadata.name, "chaos-orchestrations");
399        assert!(kustomization.spec.prune);
400    }
401
402    #[test]
403    fn test_argocd_application() {
404        let app = argocd::ArgoApplication::new_for_orchestrations(
405            "chaos-app".to_string(),
406            "chaos-testing".to_string(),
407            "https://github.com/example/chaos".to_string(),
408            "./orchestrations".to_string(),
409            true,
410        );
411
412        assert_eq!(app.metadata.name, "chaos-app");
413        assert!(app.spec.sync_policy.is_some());
414    }
415}