Files
archy/core/container/src/dependency_resolver.rs
T

263 lines
7.8 KiB
Rust
Raw Normal View History

2026-01-24 22:01:51 +00:00
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<String, AppManifest>,
}
impl DependencyResolver {
pub fn new() -> Self {
Self {
manifests: IndexMap::new(),
}
}
2026-01-24 22:01:51 +00:00
pub fn add_manifest(&mut self, manifest: AppManifest) {
self.manifests.insert(manifest.app.id.clone(), manifest);
}
2026-01-24 22:01:51 +00:00
pub fn resolve_dependencies(&self, app_id: &str) -> Result<Vec<String>, DependencyError> {
let mut visited = HashSet::new();
let mut visiting = HashSet::new();
let mut result = Vec::new();
2026-01-24 22:01:51 +00:00
self.resolve_recursive(app_id, &mut visited, &mut visiting, &mut result)?;
// Result is already in installation order (dependencies first)
2026-01-24 22:01:51 +00:00
Ok(result)
}
2026-01-24 22:01:51 +00:00
fn resolve_recursive(
&self,
app_id: &str,
visited: &mut HashSet<String>,
visiting: &mut HashSet<String>,
result: &mut Vec<String>,
) -> Result<(), DependencyError> {
if visited.contains(app_id) {
return Ok(());
}
2026-01-24 22:01:51 +00:00
if visiting.contains(app_id) {
return Err(DependencyError::CircularDependency(format!(
"Circular dependency detected involving: {}",
app_id
)));
2026-01-24 22:01:51 +00:00
}
2026-01-24 22:01:51 +00:00
visiting.insert(app_id.to_string());
let manifest = self.manifests.get(app_id).ok_or_else(|| {
DependencyError::MissingDependency(format!("App not found: {}", app_id))
})?;
2026-01-24 22:01:51 +00:00
// Resolve all dependencies first
for dep in &manifest.app.dependencies {
match dep {
Dependency::App {
app_id: dep_id,
version: _,
} => {
2026-01-24 22:01:51 +00:00
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)?;
}
}
}
2026-01-24 22:01:51 +00:00
visiting.remove(app_id);
visited.insert(app_id.to_string());
2026-01-24 22:01:51 +00:00
if !result.contains(&app_id.to_string()) {
result.push(app_id.to_string());
}
2026-01-24 22:01:51 +00:00
Ok(())
}
2026-01-24 22:01:51 +00:00
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))
})?;
2026-01-24 22:01:51 +00:00
// Check for port conflicts
let mut port_usage: HashMap<u16, String> = HashMap::new();
2026-01-24 22:01:51 +00:00
for (id, m) in &self.manifests {
if id == app_id {
continue;
}
2026-01-24 22:01:51 +00:00
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
)));
2026-01-24 22:01:51 +00:00
}
port_usage.insert(port.host, id.clone());
}
}
2026-01-24 22:01:51 +00:00
// 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
)));
2026-01-24 22:01:51 +00:00
}
}
2026-01-24 22:01:51 +00:00
Ok(())
}
2026-01-24 22:01:51 +00:00
pub fn calculate_resources(&self, app_ids: &[String]) -> ResourceRequirements {
let mut total = ResourceRequirements {
cpu: 0,
memory_mb: 0,
disk_gb: 0,
};
2026-01-24 22:01:51 +00:00
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;
}
2026-01-24 22:01:51 +00:00
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;
}
}
2026-01-24 22:01:51 +00:00
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;
}
}
}
}
2026-01-24 22:01:51 +00:00
total
}
}
fn parse_memory(s: &str) -> Result<u32, ()> {
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<u32, ()> {
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::{AppDefinition, AppManifest, ContainerConfig};
2026-01-24 22:01:51 +00:00
fn create_test_manifest(id: &str, deps: Vec<Dependency>) -> AppManifest {
AppManifest {
app: AppDefinition {
id: id.to_string(),
name: format!("Test {}", id),
version: "1.0.0".to_string(),
description: None,
container: ContainerConfig {
image: Some(format!("test/{}:latest", id)),
2026-01-24 22:01:51 +00:00
image_signature: None,
pull_policy: "if-not-present".to_string(),
build: None,
2026-01-24 22:01:51 +00:00
},
dependencies: deps,
resources: Default::default(),
security: Default::default(),
ports: vec![],
volumes: vec![],
environment: vec![],
health_check: None,
devices: vec![],
extensions: Default::default(),
},
}
}
2026-01-24 22:01:51 +00:00
#[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())],
));
2026-01-24 22:01:51 +00:00
let deps = resolver.resolve_dependencies("app2").unwrap();
assert_eq!(deps, vec!["app1", "app2"]);
}
2026-01-24 22:01:51 +00:00
#[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())],
));
2026-01-24 22:01:51 +00:00
let result = resolver.resolve_dependencies("app1");
assert!(result.is_err());
}
}