use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitOpsConfig {
pub repo_url: String,
pub branch: String,
pub path: PathBuf,
pub sync_interval_seconds: u64,
pub auth: GitOpsAuth,
pub auto_sync: bool,
pub prune: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum GitOpsAuth {
#[serde(rename = "ssh")]
SSH { private_key_path: PathBuf },
#[serde(rename = "token")]
Token { token: String },
#[serde(rename = "basic")]
Basic { username: String, password: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStatus {
pub last_sync: DateTime<Utc>,
pub commit_hash: String,
pub status: SyncState,
pub orchestrations_synced: usize,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SyncState {
Synced,
OutOfSync,
Syncing,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestrationManifest {
pub file_path: PathBuf,
pub content: serde_json::Value,
pub hash: String,
pub last_modified: DateTime<Utc>,
}
pub struct GitOpsManager {
config: GitOpsConfig,
manifests: HashMap<String, OrchestrationManifest>,
last_sync_status: Option<SyncStatus>,
}
impl GitOpsManager {
pub fn new(config: GitOpsConfig) -> Self {
Self {
config,
manifests: HashMap::new(),
last_sync_status: None,
}
}
pub async fn sync(&mut self) -> Result<SyncStatus, String> {
let start_time = Utc::now();
let manifests = self.discover_manifests().await?;
let _changes: Vec<()> = self.calculate_changes(&manifests)?;
let mut errors = Vec::new();
if self.config.prune {
if let Err(e) = self.prune_removed_orchestrations(&manifests).await {
errors.push(format!("Failed to prune: {}", e));
}
}
let status = SyncStatus {
last_sync: start_time,
commit_hash: "abc123def456".to_string(), status: if errors.is_empty() {
SyncState::Synced
} else {
SyncState::Failed
},
orchestrations_synced: manifests.len(),
errors,
};
self.last_sync_status = Some(status.clone());
Ok(status)
}
async fn discover_manifests(&self) -> Result<Vec<OrchestrationManifest>, String> {
Ok(Vec::new())
}
fn calculate_changes(&self, _manifests: &[OrchestrationManifest]) -> Result<Vec<()>, String> {
Ok(Vec::new())
}
async fn prune_removed_orchestrations(
&mut self,
current_manifests: &[OrchestrationManifest],
) -> Result<(), String> {
let current_names: Vec<String> = current_manifests
.iter()
.map(|m| m.file_path.to_string_lossy().to_string())
.collect();
self.manifests.retain(|name, _| current_names.contains(name));
Ok(())
}
pub fn get_status(&self) -> Option<&SyncStatus> {
self.last_sync_status.as_ref()
}
pub fn is_auto_sync_enabled(&self) -> bool {
self.config.auto_sync
}
pub fn get_sync_interval(&self) -> u64 {
self.config.sync_interval_seconds
}
pub async fn start_auto_sync(&mut self) -> Result<(), String> {
if !self.config.auto_sync {
return Err("Auto-sync is not enabled".to_string());
}
loop {
match self.sync().await {
Ok(status) => {
println!("Sync completed: {:?}", status.status);
}
Err(e) => {
eprintln!("Sync failed: {}", e);
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(self.config.sync_interval_seconds))
.await;
}
}
}
pub mod flux {
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluxKustomization {
pub api_version: String,
pub kind: String,
pub metadata: FluxMetadata,
pub spec: FluxSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluxMetadata {
pub name: String,
pub namespace: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluxSpec {
pub interval: String,
pub path: String,
pub prune: bool,
pub source_ref: SourceRef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceRef {
pub kind: String,
pub name: String,
}
impl FluxKustomization {
pub fn new_for_orchestrations(
name: String,
namespace: String,
git_repo: String,
path: String,
) -> Self {
Self {
api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
kind: "Kustomization".to_string(),
metadata: FluxMetadata {
name: name.clone(),
namespace,
},
spec: FluxSpec {
interval: "5m".to_string(),
path,
prune: true,
source_ref: SourceRef {
kind: "GitRepository".to_string(),
name: git_repo,
},
},
}
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
}
}
pub mod argocd {
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgoApplication {
pub api_version: String,
pub kind: String,
pub metadata: ArgoMetadata,
pub spec: ArgoSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgoMetadata {
pub name: String,
pub namespace: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgoSpec {
pub project: String,
pub source: ArgoSource,
pub destination: ArgoDestination,
pub sync_policy: Option<SyncPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArgoSource {
pub repo_url: String,
pub target_revision: String,
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgoDestination {
pub server: String,
pub namespace: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncPolicy {
pub automated: Option<AutomatedSync>,
pub sync_options: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutomatedSync {
pub prune: bool,
#[serde(rename = "selfHeal")]
pub self_heal: bool,
}
impl ArgoApplication {
pub fn new_for_orchestrations(
name: String,
namespace: String,
repo_url: String,
path: String,
auto_sync: bool,
) -> Self {
Self {
api_version: "argoproj.io/v1alpha1".to_string(),
kind: "Application".to_string(),
metadata: ArgoMetadata {
name: name.clone(),
namespace: namespace.clone(),
},
spec: ArgoSpec {
project: "default".to_string(),
source: ArgoSource {
repo_url,
target_revision: "HEAD".to_string(),
path,
},
destination: ArgoDestination {
server: "https://kubernetes.default.svc".to_string(),
namespace,
},
sync_policy: if auto_sync {
Some(SyncPolicy {
automated: Some(AutomatedSync {
prune: true,
self_heal: true,
}),
sync_options: vec!["CreateNamespace=true".to_string()],
})
} else {
None
},
},
}
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gitops_manager_creation() {
let config = GitOpsConfig {
repo_url: "https://github.com/example/chaos-configs".to_string(),
branch: "main".to_string(),
path: PathBuf::from("orchestrations"),
sync_interval_seconds: 300,
auth: GitOpsAuth::Token {
token: "test-token".to_string(),
},
auto_sync: true,
prune: true,
};
let manager = GitOpsManager::new(config);
assert!(manager.is_auto_sync_enabled());
assert_eq!(manager.get_sync_interval(), 300);
}
#[test]
fn test_flux_kustomization() {
let kustomization = flux::FluxKustomization::new_for_orchestrations(
"chaos-orchestrations".to_string(),
"chaos-testing".to_string(),
"chaos-repo".to_string(),
"./orchestrations".to_string(),
);
assert_eq!(kustomization.metadata.name, "chaos-orchestrations");
assert!(kustomization.spec.prune);
}
#[test]
fn test_argocd_application() {
let app = argocd::ArgoApplication::new_for_orchestrations(
"chaos-app".to_string(),
"chaos-testing".to_string(),
"https://github.com/example/chaos".to_string(),
"./orchestrations".to_string(),
true,
);
assert_eq!(app.metadata.name, "chaos-app");
assert!(app.spec.sync_policy.is_some());
}
}