1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GitOpsConfig {
14 pub repo_url: String,
16 pub branch: String,
18 pub path: PathBuf,
20 pub sync_interval_seconds: u64,
22 pub auth: GitOpsAuth,
24 pub auto_sync: bool,
26 pub prune: bool,
28}
29
30#[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#[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#[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#[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
71pub struct GitOpsManager {
73 config: GitOpsConfig,
74 manifests: HashMap<String, OrchestrationManifest>,
75 last_sync_status: Option<SyncStatus>,
76}
77
78impl GitOpsManager {
79 pub fn new(config: GitOpsConfig) -> Self {
81 Self {
82 config,
83 manifests: HashMap::new(),
84 last_sync_status: None,
85 }
86 }
87
88 pub async fn sync(&mut self) -> Result<SyncStatus, String> {
90 let start_time = Utc::now();
98
99 let manifests = self.discover_manifests().await?;
101 let _changes: Vec<()> = self.calculate_changes(&manifests)?;
102
103 let mut errors = Vec::new();
105
106 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(), 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 async fn discover_manifests(&self) -> Result<Vec<OrchestrationManifest>, String> {
131 Ok(Vec::new())
133 }
134
135 fn calculate_changes(&self, _manifests: &[OrchestrationManifest]) -> Result<Vec<()>, String> {
137 Ok(Vec::new())
140 }
141
142 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 pub fn get_status(&self) -> Option<&SyncStatus> {
159 self.last_sync_status.as_ref()
160 }
161
162 pub fn is_auto_sync_enabled(&self) -> bool {
164 self.config.auto_sync
165 }
166
167 pub fn get_sync_interval(&self) -> u64 {
169 self.config.sync_interval_seconds
170 }
171
172 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
194pub mod flux {
196 use super::*;
197
198 #[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 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 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
257 serde_yaml::to_string(self)
258 }
259 }
260}
261
262pub mod argocd {
264 use super::*;
265
266 #[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 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 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}