use serde::{Deserialize, Serialize}; use std::collections::HashMap; use thiserror::Error; #[derive(Debug, Error)] pub enum ManifestError { #[error("Invalid manifest: {0}")] Invalid(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("YAML parse error: {0}")] Yaml(#[from] serde_yaml::Error), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppManifest { pub app: AppDefinition, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppDefinition { pub id: String, pub name: String, pub version: String, pub description: Option, #[serde(default)] pub container: ContainerConfig, #[serde(default)] pub dependencies: Vec, #[serde(default)] pub resources: ResourceLimits, #[serde(default)] pub security: SecurityPolicy, #[serde(default)] pub ports: Vec, #[serde(default)] pub volumes: Vec, #[serde(default)] pub environment: Vec, #[serde(default)] pub health_check: Option, #[serde(default)] pub devices: Vec, #[serde(flatten)] pub extensions: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ContainerConfig { pub image: String, #[serde(default)] pub image_signature: Option, #[serde(default = "default_pull_policy")] pub pull_policy: String, } fn default_pull_policy() -> String { "if-not-present".to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Dependency { Storage { storage: String, }, App { app_id: String, version: Option, }, Simple(String), } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ResourceLimits { #[serde(default)] pub cpu_limit: Option, #[serde(default)] pub memory_limit: Option, #[serde(default)] pub disk_limit: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SecurityPolicy { #[serde(default)] pub capabilities: Vec, #[serde(default = "default_true")] pub readonly_root: bool, #[serde(default = "default_network_policy")] pub network_policy: String, #[serde(default)] pub apparmor_profile: Option, } fn default_true() -> bool { true } fn default_network_policy() -> String { "isolated".to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PortMapping { pub host: u16, pub container: u16, #[serde(default)] pub protocol: String, } impl From<(u16, u16)> for PortMapping { fn from((host, container): (u16, u16)) -> Self { PortMapping { host, container, protocol: "tcp".to_string(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Volume { #[serde(rename = "type")] pub volume_type: String, pub source: String, pub target: String, #[serde(default)] pub options: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthCheck { #[serde(rename = "type")] pub check_type: String, pub endpoint: Option, pub path: Option, #[serde(default = "default_interval")] pub interval: String, #[serde(default = "default_timeout")] pub timeout: String, #[serde(default = "default_retries")] pub retries: u32, } fn default_interval() -> String { "30s".to_string() } fn default_timeout() -> String { "5s".to_string() } fn default_retries() -> u32 { 3 } impl AppManifest { pub fn from_file(path: &std::path::Path) -> Result { let content = std::fs::read_to_string(path)?; Self::parse(&content) } pub fn parse(content: &str) -> Result { let manifest: AppManifest = serde_yaml::from_str(content)?; manifest.validate()?; Ok(manifest) } pub fn validate(&self) -> Result<(), ManifestError> { if self.app.id.is_empty() { return Err(ManifestError::Invalid("app.id cannot be empty".to_string())); } if self.app.container.image.is_empty() { return Err(ManifestError::Invalid( "container.image cannot be empty".to_string(), )); } // Validate version format (semantic versioning) if !self.app.version.chars().any(|c| c.is_ascii_digit()) { return Err(ManifestError::Invalid( "app.version must contain at least one digit".to_string(), )); } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_manifest_parse() { let yaml = r#" app: id: test-app name: Test App version: 1.0.0 container: image: test/image:latest "#; let manifest = AppManifest::parse(yaml).unwrap(); assert_eq!(manifest.app.id, "test-app"); assert_eq!(manifest.app.name, "Test App"); assert_eq!(manifest.app.version, "1.0.0"); } #[test] fn test_manifest_validation() { let yaml = r#" app: id: "" name: Test version: 1.0.0 container: image: test/image:latest "#; let result = AppManifest::parse(yaml); assert!(result.is_err()); } }