use crate::manifest::{AppManifest, Dependency}; use indexmap::IndexMap; use std::collections::{HashMap, HashSet}; use thiserror::Error; #[derive(Debug, Error)] pub enum DependencyError { #[error("Circular dependency detected: {0}")] CircularDependency(String), #[error("Missing dependency: {0}")] MissingDependency(String), #[error("Version conflict: {0}")] VersionConflict(String), } pub struct DependencyResolver { manifests: IndexMap, } impl DependencyResolver { pub fn new() -> Self { Self { manifests: IndexMap::new(), } } pub fn add_manifest(&mut self, manifest: AppManifest) { self.manifests.insert(manifest.app.id.clone(), manifest); } pub fn resolve_dependencies(&self, app_id: &str) -> Result, DependencyError> { let mut visited = HashSet::new(); let mut visiting = HashSet::new(); let mut result = Vec::new(); self.resolve_recursive(app_id, &mut visited, &mut visiting, &mut result)?; // Result is already in installation order (dependencies first) Ok(result) } fn resolve_recursive( &self, app_id: &str, visited: &mut HashSet, visiting: &mut HashSet, result: &mut Vec, ) -> Result<(), DependencyError> { if visited.contains(app_id) { return Ok(()); } if visiting.contains(app_id) { return Err(DependencyError::CircularDependency( format!("Circular dependency detected involving: {}", app_id) )); } visiting.insert(app_id.to_string()); let manifest = self.manifests.get(app_id) .ok_or_else(|| DependencyError::MissingDependency( format!("App not found: {}", app_id) ))?; // Resolve all dependencies first for dep in &manifest.app.dependencies { match dep { Dependency::App { app_id: dep_id, version: _ } => { self.resolve_recursive(dep_id, visited, visiting, result)?; } Dependency::Storage { storage: _ } => { // Storage dependencies are checked but don't require other apps } Dependency::Simple(dep_id) => { self.resolve_recursive(dep_id, visited, visiting, result)?; } } } visiting.remove(app_id); visited.insert(app_id.to_string()); if !result.contains(&app_id.to_string()) { result.push(app_id.to_string()); } Ok(()) } pub fn check_conflicts(&self, app_id: &str) -> Result<(), DependencyError> { let manifest = self.manifests.get(app_id) .ok_or_else(|| DependencyError::MissingDependency( format!("App not found: {}", app_id) ))?; // Check for port conflicts let mut port_usage: HashMap = HashMap::new(); for (id, m) in &self.manifests { if id == app_id { continue; } for port in &m.app.ports { if let Some(existing) = port_usage.get(&port.host) { return Err(DependencyError::VersionConflict( format!("Port {} already used by {}", port.host, existing) )); } port_usage.insert(port.host, id.clone()); } } // Check for new app's ports for port in &manifest.app.ports { if let Some(existing) = port_usage.get(&port.host) { return Err(DependencyError::VersionConflict( format!("Port {} already used by {}", port.host, existing) )); } } Ok(()) } pub fn calculate_resources(&self, app_ids: &[String]) -> ResourceRequirements { let mut total = ResourceRequirements { cpu: 0, memory_mb: 0, disk_gb: 0, }; for app_id in app_ids { if let Some(manifest) = self.manifests.get(app_id) { if let Some(cpu) = manifest.app.resources.cpu_limit { total.cpu += cpu; } if let Some(memory) = &manifest.app.resources.memory_limit { // Parse memory string (e.g., "1Gi", "512Mi") if let Ok(mb) = parse_memory(memory) { total.memory_mb += mb; } } if let Some(disk) = &manifest.app.resources.disk_limit { // Parse disk string (e.g., "10Gi", "500Mi") if let Ok(gb) = parse_disk(disk) { total.disk_gb += gb; } } } } total } } fn parse_memory(s: &str) -> Result { let s = s.trim().to_lowercase(); if s.ends_with("gi") { let num: f64 = s.trim_end_matches("gi").parse().map_err(|_| ())?; Ok((num * 1024.0) as u32) } else if s.ends_with("mi") { let num: f64 = s.trim_end_matches("mi").parse().map_err(|_| ())?; Ok(num as u32) } else { Err(()) } } fn parse_disk(s: &str) -> Result { let s = s.trim().to_lowercase(); if s.ends_with("gi") { let num: f64 = s.trim_end_matches("gi").parse().map_err(|_| ())?; Ok(num as u32) } else if s.ends_with("ti") { let num: f64 = s.trim_end_matches("ti").parse().map_err(|_| ())?; Ok((num * 1024.0) as u32) } else { Err(()) } } #[derive(Debug, Clone)] pub struct ResourceRequirements { pub cpu: u32, pub memory_mb: u32, pub disk_gb: u32, } impl Default for DependencyResolver { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::manifest::{AppManifest, AppDefinition, ContainerConfig}; fn create_test_manifest(id: &str, deps: Vec) -> AppManifest { AppManifest { app: AppDefinition { id: id.to_string(), name: format!("Test {}", id), version: "1.0.0".to_string(), description: None, container: ContainerConfig { image: format!("test/{}:latest", id), image_signature: None, pull_policy: "if-not-present".to_string(), }, dependencies: deps, resources: Default::default(), security: Default::default(), ports: vec![], volumes: vec![], environment: vec![], health_check: None, devices: vec![], extensions: Default::default(), }, } } #[test] fn test_simple_dependency() { let mut resolver = DependencyResolver::new(); resolver.add_manifest(create_test_manifest("app1", vec![])); resolver.add_manifest(create_test_manifest("app2", vec![ Dependency::Simple("app1".to_string()) ])); let deps = resolver.resolve_dependencies("app2").unwrap(); assert_eq!(deps, vec!["app1", "app2"]); } #[test] fn test_circular_dependency() { let mut resolver = DependencyResolver::new(); resolver.add_manifest(create_test_manifest("app1", vec![ Dependency::Simple("app2".to_string()) ])); resolver.add_manifest(create_test_manifest("app2", vec![ Dependency::Simple("app1".to_string()) ])); let result = resolver.resolve_dependencies("app1"); assert!(result.is_err()); } }