diff --git a/Cargo.lock b/Cargo.lock index 00e973fde4..65c4d74e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3115,7 +3115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", - "quick-error", + "quick-error 2.0.1", ] [[package]] @@ -3638,6 +3638,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -3948,6 +3957,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -4550,6 +4568,35 @@ dependencies = [ "perry-hir", ] +[[package]] +name = "perry-container-compose" +version = "0.5.494" +dependencies = [ + "anyhow", + "async-trait", + "atty", + "clap", + "console", + "dashmap 5.5.3", + "dialoguer", + "dotenvy", + "hex", + "indexmap", + "md-5", + "once_cell", + "proptest", + "rand 0.8.6", + "regex", + "serde", + "serde_json", + "serde_yaml", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "which 6.0.3", +] + [[package]] name = "perry-diagnostics" version = "0.5.494" @@ -5220,6 +5267,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.11.1", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.31" @@ -5280,6 +5346,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-error" version = "2.0.1" @@ -5450,6 +5522,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rav1e" version = "0.8.1" @@ -5494,7 +5575,7 @@ dependencies = [ "avif-serialize", "imgref", "loop9", - "quick-error", + "quick-error 2.0.1", "rav1e", "rayon", "rgb", @@ -5904,6 +5985,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -6182,6 +6275,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "servo_arc" version = "0.3.0" @@ -6219,6 +6325,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shell-words" version = "1.1.1" @@ -7004,6 +7119,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.11.3" @@ -7013,7 +7137,7 @@ dependencies = [ "fax", "flate2", "half", - "quick-error", + "quick-error 2.0.1", "weezl", "zune-jpeg", ] @@ -7384,6 +7508,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -7490,6 +7644,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.9.0" @@ -7563,6 +7723,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -7687,6 +7853,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -7705,6 +7877,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 8f2566a743..0bef8e4ea4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "crates/perry-ui-testkit", "crates/perry-doc-tests", "crates/perry-updater", + "crates/perry-container-compose", ] # Only build platform-independent crates by default. # Platform-specific UI crates (perry-ui-macos, perry-ui-ios, etc.) must be built @@ -56,6 +57,7 @@ default-members = [ "crates/perry-codegen-wear-tiles", "crates/perry-codegen-wasm", "crates/perry-updater", + "crates/perry-container-compose", ] # Aggressive release optimizations for small, fast binaries @@ -193,3 +195,4 @@ perry-codegen-wear-tiles = { path = "crates/perry-codegen-wear-tiles" } perry-codegen-wasm = { path = "crates/perry-codegen-wasm" } perry-ui-testkit = { path = "crates/perry-ui-testkit" } perry-updater = { path = "crates/perry-updater" } +perry-container-compose = { path = "crates/perry-container-compose" } diff --git a/crates/perry-container-compose/src/capabilities.rs b/crates/perry-container-compose/src/capabilities.rs index 92dbf756e7..f991d7a3bb 100644 --- a/crates/perry-container-compose/src/capabilities.rs +++ b/crates/perry-container-compose/src/capabilities.rs @@ -505,7 +505,7 @@ pub fn required_features(spec: &crate::types::ComposeSpec) -> std::collections:: for (_svc_name, svc) in &spec.services { // privileged: true → privileged - if svc.privileged.unwrap_or(false) { + if svc.privileged.as_ref().and_then(|v| v.as_bool()).unwrap_or(false) { needed.insert("privileged"); } @@ -533,7 +533,7 @@ pub fn required_features(spec: &crate::types::ComposeSpec) -> std::collections:: } // read_only: true → read_only_rootfs - if svc.read_only.unwrap_or(false) { + if svc.read_only.as_ref().and_then(|v| v.as_bool()).unwrap_or(false) { needed.insert("read_only_rootfs"); } @@ -573,10 +573,9 @@ pub fn required_features(spec: &crate::types::ComposeSpec) -> std::collections:: // volumes with :Z or :z suffix → selinux_mount_labels if let Some(volumes) = &svc.volumes { for v in volumes { - if let Some(s) = v.as_str() { - if s.ends_with(":Z") || s.ends_with(":z") { - needed.insert("selinux_mount_labels"); - } + let s = v.to_string_form(); + if s.ends_with(":Z") || s.ends_with(":z") { + needed.insert("selinux_mount_labels"); } } } diff --git a/crates/perry-container-compose/src/compose.rs b/crates/perry-container-compose/src/compose.rs index 2e7fb26e07..010f717b6c 100644 --- a/crates/perry-container-compose/src/compose.rs +++ b/crates/perry-container-compose/src/compose.rs @@ -159,7 +159,12 @@ impl ComposeEngine { .and_then(|v| v.get(decl_name)) .and_then(|c| c.as_ref()); if let Some(cfg) = cfg_opt { - if cfg.external.unwrap_or(false) { + let is_external = match &cfg.external { + Some(serde_yaml::Value::Bool(b)) => *b, + Some(serde_yaml::Value::Mapping(_)) => true, + _ => false, + }; + if is_external { // External: use `name:` if set, else literal declaration name. return cfg.name.clone().unwrap_or_else(|| decl_name.to_string()); } @@ -181,7 +186,12 @@ impl ComposeEngine { .and_then(|n| n.get(decl_name)) .and_then(|c| c.as_ref()); if let Some(cfg) = cfg_opt { - if cfg.external.unwrap_or(false) { + let is_external = match &cfg.external { + Some(serde_yaml::Value::Bool(b)) => *b, + Some(serde_yaml::Value::Mapping(_)) => true, + _ => false, + }; + if is_external { return cfg.name.clone().unwrap_or_else(|| decl_name.to_string()); } if let Some(explicit) = &cfg.name { @@ -199,7 +209,11 @@ impl ComposeEngine { .as_ref() .and_then(|v| v.get(decl_name)) .and_then(|c| c.as_ref()) - .and_then(|c| c.external) + .and_then(|c| match &c.external { + Some(serde_yaml::Value::Bool(b)) => Some(*b), + Some(serde_yaml::Value::Mapping(_)) => Some(true), + _ => None, + }) .unwrap_or(false) } @@ -211,7 +225,11 @@ impl ComposeEngine { .as_ref() .and_then(|n| n.get(decl_name)) .and_then(|c| c.as_ref()) - .and_then(|c| c.external) + .and_then(|c| match &c.external { + Some(serde_yaml::Value::Bool(b)) => Some(*b), + Some(serde_yaml::Value::Mapping(_)) => Some(true), + _ => None, + }) .unwrap_or(false) } @@ -421,49 +439,14 @@ impl ComposeEngine { let container_spec = ContainerSpec { image: image_to_use, name: Some(container_name.clone()), - ports: Some( - svc.ports - .as_ref() - .map(|p| { - p.iter() - .map(|ps| match ps { - crate::types::PortSpec::Short(v) => match v { - serde_yaml::Value::String(s) => s.clone(), - serde_yaml::Value::Number(n) => n.to_string(), - _ => v.as_str().unwrap_or_default().to_string(), - }, - crate::types::PortSpec::Long(lp) => { - let publ = lp - .published - .as_ref() - .map(|v| match v { - serde_yaml::Value::String(s) => s.clone(), - serde_yaml::Value::Number(n) => n.to_string(), - _ => v.as_str().unwrap_or_default().to_string(), - }) - .unwrap_or_default(); - let target = match &lp.target { - serde_yaml::Value::String(s) => s.clone(), - serde_yaml::Value::Number(n) => n.to_string(), - _ => lp.target.as_str().unwrap_or_default().to_string(), - }; - format!("{}:{}", publ, target) - } - }) - .collect() - }) - .unwrap_or_default(), - ), + ports: Some(svc.port_strings()), volumes: Some( svc.volumes .as_ref() .map(|v| { v.iter() .map(|vs| { - let raw = match vs { - serde_yaml::Value::String(s) => s.clone(), - _ => vs.as_str().unwrap_or_default().to_string(), - }; + let raw = vs.to_string_form(); // Namespace named-volume references: // "named:/path" → "_named:/path" // "named:/path:ro" → "_named:/path:ro" @@ -500,44 +483,14 @@ impl ComposeEngine { }) .unwrap_or_default(), ), - env: Some(match &svc.environment { - Some(crate::types::ListOrDict::Dict(d)) => d - .iter() - .map(|(k, v)| { - ( - k.clone(), - v.as_ref() - .map(|vv| match vv { - serde_yaml::Value::String(s) => s.clone(), - serde_yaml::Value::Number(n) => n.to_string(), - serde_yaml::Value::Bool(b) => b.to_string(), - _ => vv.as_str().unwrap_or_default().to_string(), - }) - .unwrap_or_default(), - ) - }) - .collect(), - Some(crate::types::ListOrDict::List(l)) => l - .iter() - .filter_map(|s| s.split_once('=')) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - None => HashMap::new(), - }), - cmd: Some(match &svc.command { - Some(serde_yaml::Value::String(s)) => vec![s.clone()], - Some(serde_yaml::Value::Sequence(seq)) => seq - .iter() - .map(|v| v.as_str().unwrap_or_default().to_string()) - .collect(), - _ => vec![], - }), - entrypoint: None, + env: Some(svc.resolved_env()), + cmd: svc.command_list(), + entrypoint: svc.entrypoint_list(), network: network.clone(), rm: None, - read_only: svc.read_only, + read_only: svc.read_only.as_ref().and_then(|v| v.as_bool()), labels: Some(labels), - privileged: svc.privileged, + privileged: svc.privileged.as_ref().and_then(|v| v.as_bool()), user: svc.user.clone(), workdir: svc.working_dir.clone(), cap_add: svc.cap_add.clone(), @@ -591,7 +544,7 @@ impl ComposeEngine { // (apple/container) with a structured warning, so the user // knows the policy wasn't honored. let mut profile = crate::backend::SecurityProfile { - read_only_root: svc.read_only.unwrap_or(false), + read_only_root: svc.read_only.as_ref().and_then(|v| v.as_bool()).unwrap_or(false), seccomp: None, no_new_privileges: false, }; diff --git a/crates/perry-container-compose/src/project.rs b/crates/perry-container-compose/src/project.rs index 31e7e71f04..0b56dc3434 100644 --- a/crates/perry-container-compose/src/project.rs +++ b/crates/perry-container-compose/src/project.rs @@ -2,8 +2,7 @@ use crate::config::ProjectConfig; use crate::error::{ComposeError, Result}; use crate::types::ComposeSpec; use crate::yaml; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; pub struct ComposeProject { pub spec: ComposeSpec, diff --git a/crates/perry-container-compose/src/service.rs b/crates/perry-container-compose/src/service.rs index 0e752e55eb..6d3e6964cb 100644 --- a/crates/perry-container-compose/src/service.rs +++ b/crates/perry-container-compose/src/service.rs @@ -1,4 +1,3 @@ -use crate::error::Result; use md5::{Digest, Md5}; pub fn generate_name(input: &str) -> String { diff --git a/crates/perry-container-compose/src/types.rs b/crates/perry-container-compose/src/types.rs index cceaf06d74..776ed402ec 100644 --- a/crates/perry-container-compose/src/types.rs +++ b/crates/perry-container-compose/src/types.rs @@ -256,9 +256,14 @@ impl PortSpec { #[serde(rename_all = "snake_case")] pub struct ComposeServiceNetworkConfig { pub aliases: Option>, + pub interface_name: Option, pub ipv4_address: Option, pub ipv6_address: Option, - pub priority: Option, + pub link_local_ips: Option>, + pub mac_address: Option, + pub driver_opts: Option>, + pub priority: Option, + pub gw_priority: Option, } /// `networks` field on a service: list or map @@ -278,6 +283,76 @@ impl ServiceNetworks { } } +// ============ EnvFileEntry ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EnvFileEntry { + Short(String), + Long(EnvFileConfig), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnvFileConfig { + pub path: String, + #[serde(default = "default_true")] + pub required: bool, + pub format: Option, +} + +fn default_true() -> bool { + true +} + +// ============ Ulimit ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Ulimit { + Single(i64), + SoftHard { soft: i64, hard: i64 }, +} + +// ============ DeviceEntry ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DeviceEntry { + Short(String), + Long(DeviceRequest), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeviceRequest { + pub source: String, + pub target: Option, + pub permissions: Option, +} + +// ============ BlkioConfig ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlkioConfig { + pub device_read_bps: Option>, + pub device_read_iops: Option>, + pub device_write_bps: Option>, + pub device_write_iops: Option>, + pub weight: Option, + pub weight_device: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlkioLimit { + pub path: String, + pub rate: serde_yaml::Value, // string or int +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlkioWeight { + pub path: String, + pub weight: u16, +} + // ============ Build ============ /// Build configuration (string shorthand or full object) @@ -296,24 +371,24 @@ pub struct ComposeServiceBuild { pub containerfile: Option, pub dockerfile_inline: Option, pub args: Option, - pub ssh: Option, + pub ssh: Option, pub labels: Option, pub cache_from: Option>, pub cache_to: Option>, pub no_cache: Option, pub additional_contexts: Option>, pub network: Option, - pub provenance: Option, - pub sbom: Option, + pub provenance: Option, // bool or string + pub sbom: Option, // bool or string pub pull: Option, pub target: Option, - pub shm_size: Option, + pub shm_size: Option, // int or string pub extra_hosts: Option, pub isolation: Option, pub privileged: Option, - pub secrets: Option>, + pub secrets: Option>, pub tags: Option>, - pub ulimits: Option, + pub ulimits: Option>, pub platforms: Option>, pub entitlements: Option>, } @@ -338,12 +413,92 @@ impl BuildSpec { } } +// ============ Extends ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExtendsSpec { + pub file: Option, + pub service: String, +} + +// ============ Develop ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DevelopConfig { + pub watch: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EnvFileSpec { + String(String), + List(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WatchEntry { + pub path: String, + pub action: String, // sync, rebuild, sync+restart + pub target: Option, + pub ignore: Option>, +} + +// ============ Lifecycle Hooks ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LifecycleHook { + pub command: StringOrList, + pub user: Option, + pub privileged: Option, + pub working_dir: Option, + pub environment: Option, +} + +// ============ CredentialSpec ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CredentialSpec { + pub config: Option, + pub file: Option, + pub registry: Option, +} + +// ============ Models ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComposeModel { + pub name: Option, + pub model: String, + pub context_size: Option, + pub runtime_flags: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ServiceModels { + List(Vec), + Map(IndexMap), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceModelConfig { + pub endpoint_var: Option, + pub model_var: Option, +} + // ============ Healthcheck ============ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum HealthcheckTest { + String(String), + List(Vec), +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct ComposeHealthcheck { - pub test: serde_yaml::Value, + pub test: Option, pub interval: Option, pub timeout: Option, pub retries: Option, @@ -361,10 +516,35 @@ pub struct ComposeDeployment { pub replicas: Option, pub labels: Option, pub resources: Option, - pub restart_policy: Option, - pub placement: Option, - pub update_config: Option, - pub rollback_config: Option, + pub restart_policy: Option, + pub placement: Option, + pub update_config: Option, + pub rollback_config: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ComposeRestartPolicy { + pub condition: Option, + pub delay: Option, + pub max_attempts: Option, + pub window: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ComposePlacement { + pub constraints: Option>, + pub preferences: Option>, + pub max_replicas_per_node: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ComposeUpdateConfig { + pub parallelism: Option, + pub delay: Option, + pub failure_action: Option, + pub monitor: Option, + pub max_failure_ratio: Option, + pub order: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -376,9 +556,20 @@ pub struct ComposeDeploymentResources { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ComposeResourceSpec { - pub cpus: Option, + pub cpus: Option, // float or string pub memory: Option, pub pids: Option, + pub devices: Option>, + pub generic_resources: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ComposeDeviceRequest { + pub driver: Option, + pub count: Option, + pub device_ids: Option>, + pub capabilities: Option>, + pub options: Option, } // ============ Logging ============ @@ -413,9 +604,9 @@ pub struct ComposeNetworkIpam { pub struct ComposeNetwork { pub name: Option, pub driver: Option, - pub driver_opts: Option>, + pub driver_opts: Option>, pub ipam: Option, - pub external: Option, + pub external: Option, // bool or object pub internal: Option, pub enable_ipv4: Option, pub enable_ipv6: Option, @@ -431,13 +622,29 @@ pub struct ComposeNetwork { pub struct ComposeVolume { pub name: Option, pub driver: Option, - pub driver_opts: Option>, - pub external: Option, + pub driver_opts: Option>, + pub external: Option, // bool or object pub labels: Option, } // ============ Secret ============ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ServiceSecret { + Short(String), + Long(ServiceSecretConfig), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceSecretConfig { + pub source: String, + pub target: Option, + pub uid: Option, + pub gid: Option, + pub mode: Option, // Can be octal string or int +} + /// Top-level secret definition #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] @@ -445,15 +652,31 @@ pub struct ComposeSecret { pub name: Option, pub environment: Option, pub file: Option, - pub external: Option, + pub external: Option, // bool or object pub labels: Option, pub driver: Option, - pub driver_opts: Option>, + pub driver_opts: Option>, pub template_driver: Option, } // ============ Config ============ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ServiceConfigRef { + Short(String), + Long(ServiceConfigConfig), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceConfigConfig { + pub source: String, + pub target: Option, + pub uid: Option, + pub gid: Option, + pub mode: Option, // Can be octal string or int +} + /// Top-level config definition (compose-spec `config` object) #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] @@ -462,7 +685,7 @@ pub struct ComposeConfig { pub content: Option, pub environment: Option, pub file: Option, - pub external: Option, + pub external: Option, // bool or object pub labels: Option, pub template_driver: Option, } @@ -475,56 +698,89 @@ pub struct ComposeConfig { pub struct ComposeService { pub image: Option, pub build: Option, - pub command: Option, - pub entrypoint: Option, - pub environment: Option, - pub env_file: Option, - pub ports: Option>, - pub volumes: Option>, - pub networks: Option, - pub depends_on: Option, - pub restart: Option, - pub healthcheck: Option, + pub blkio_config: Option, + pub command: Option, pub container_name: Option, - pub labels: Option, - pub hostname: Option, - pub user: Option, - pub working_dir: Option, - pub privileged: Option, - pub read_only: Option, - pub stdin_open: Option, - pub tty: Option, - pub stop_signal: Option, - pub stop_grace_period: Option, - pub network_mode: Option, - pub pid: Option, + pub cpu_count: Option, + pub cpu_percent: Option, + pub cpu_shares: Option, + pub cpu_period: Option, + pub cpu_quota: Option, + pub cpu_rt_period: Option, + pub cpu_rt_runtime: Option, + pub cpus: Option, + pub cpuset: Option, pub cap_add: Option>, pub cap_drop: Option>, - pub security_opt: Option>, - pub sysctls: Option, - pub ulimits: Option, - pub logging: Option, + pub cgroup: Option, + pub cgroup_parent: Option, + pub configs: Option>, + pub credential_spec: Option, + pub depends_on: Option, pub deploy: Option, - pub develop: Option, - pub secrets: Option>, - pub configs: Option>, - pub expose: Option>, + pub device_cgroup_rules: Option>, + pub devices: Option>, + pub dns: Option, + pub dns_opt: Option>, + pub dns_search: Option, + pub domainname: Option, + pub entrypoint: Option, + pub env_file: Option, + pub environment: Option, + pub expose: Option>, + pub extends: Option, + pub external_links: Option>, pub extra_hosts: Option, - pub dns: Option, - pub dns_search: Option, - pub tmpfs: Option, - pub shm_size: Option, + pub group_add: Option>, + pub healthcheck: Option, + pub hostname: Option, + pub init: Option, // bool or string + pub ipc: Option, + pub isolation: Option, + pub labels: Option, + pub links: Option>, + pub logging: Option, + pub models: Option, + pub network_mode: Option, + pub networks: Option, + pub mac_address: Option, pub mem_limit: Option, + pub mem_reservation: Option, + pub mem_swappiness: Option, pub memswap_limit: Option, - pub cpus: Option, - pub cpu_shares: Option, + pub oom_kill_disable: Option, + pub oom_score_adj: Option, + pub pid: Option, + pub pids_limit: Option, pub platform: Option, - pub pull_policy: Option, + pub ports: Option>, + pub privileged: Option, pub profiles: Option>, - pub scale: Option, - pub extends: Option, - pub post_start: Option>, - pub pre_stop: Option>, + pub pull_policy: Option, + pub read_only: Option, + pub restart: Option, + pub runtime: Option, + pub scale: Option, + pub secrets: Option>, + pub security_opt: Option>, + pub shm_size: Option, + pub stdin_open: Option, + pub stop_grace_period: Option, + pub stop_signal: Option, + pub storage_opt: Option>, + pub sysctls: Option, + pub tmpfs: Option, + pub tty: Option, + pub ulimits: Option>, + pub user: Option, + pub userns_mode: Option, + pub uts: Option, + pub volumes: Option>, + pub volumes_from: Option>, + pub working_dir: Option, + pub develop: Option, + pub post_start: Option>, + pub pre_stop: Option>, } impl ComposeService { @@ -565,14 +821,7 @@ impl ComposeService { .as_deref() .unwrap_or(&[]) .iter() - .filter_map(|v| { - // Try to parse as VolumeEntry (short or long) - if let Ok(short) = serde_yaml::from_value::(v.clone()) { - return Some(short.to_string_form()); - } - // Fallback: string representation - Some(yaml_value_to_str(v)) - }) + .map(|v| v.to_string_form()) .collect() } @@ -583,14 +832,12 @@ impl ComposeService { /// Get command as a list of strings. pub fn command_list(&self) -> Option> { - self.command.as_ref().map(|c| match c { - serde_yaml::Value::String(s) => vec![s.clone()], - serde_yaml::Value::Sequence(arr) => arr - .iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect(), - _ => vec![], - }) + self.command.as_ref().map(|c| c.to_list()) + } + + /// Get entrypoint as a list of strings. + pub fn entrypoint_list(&self) -> Option> { + self.entrypoint.as_ref().map(|e| e.to_list()) } /// Build a `ContainerSpec` from this service's compose-spec config. @@ -619,12 +866,12 @@ impl ComposeService { volumes: Some(self.volume_strings()), env: Some(self.resolved_env()), cmd: self.command_list(), - entrypoint: None, + entrypoint: self.entrypoint_list(), network, rm: None, - read_only: self.read_only, + read_only: self.read_only.as_ref().and_then(|v| v.as_bool()), labels, - privileged: self.privileged, + privileged: self.privileged.as_ref().and_then(|v| v.as_bool()), user: self.user.clone(), workdir: self.working_dir.clone(), cap_add: self.cap_add.clone(), @@ -722,6 +969,41 @@ impl ComposeService { } } +// ============ Include ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum IncludeEntry { + Short(String), + Long(IncludeConfig), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IncludeConfig { + pub path: StringOrList, + pub project_directory: Option, + pub env_file: Option, +} + +// ============ Expose ============ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ExposeSpec { + String(String), + Integer(u16), +} + +// ============ Namespace Modes ============ + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum NamespaceMode { + Host(String), // e.g. "host" + Service(String), // e.g. "service:foo" + Other(String), +} + // ============ ComposeSpec ============ /// Root compose spec (compose-spec §root) @@ -735,8 +1017,8 @@ pub struct ComposeSpec { pub volumes: Option>>, pub secrets: Option>>, pub configs: Option>>, - pub include: Option>, - pub models: Option>, + pub include: Option>, + pub models: Option>, #[serde(flatten)] pub extensions: IndexMap, } diff --git a/crates/perry-container-compose/tests/fixtures_tests.rs b/crates/perry-container-compose/tests/fixtures_tests.rs index f12a27f7c0..5e8f010009 100644 --- a/crates/perry-container-compose/tests/fixtures_tests.rs +++ b/crates/perry-container-compose/tests/fixtures_tests.rs @@ -59,7 +59,7 @@ fn external_network_parses_with_external_flag() { let spec = ComposeSpec::parse_str(&fixture("external-network")).expect("parse"); let nets = spec.networks.expect("networks"); let shared = nets.get("shared").expect("shared net").clone().expect("non-null"); - assert_eq!(shared.external, Some(true)); + assert_eq!(shared.external, Some(serde_yaml::Value::Bool(true))); assert_eq!(shared.name.as_deref(), Some("production_shared_v1")); } diff --git a/crates/perry-container-compose/tests/functional_orchestration.rs b/crates/perry-container-compose/tests/functional_orchestration.rs index b8416b597b..71f7477e70 100644 --- a/crates/perry-container-compose/tests/functional_orchestration.rs +++ b/crates/perry-container-compose/tests/functional_orchestration.rs @@ -15,9 +15,9 @@ use perry_container_compose::backend::ContainerBackend; use perry_container_compose::compose::ComposeEngine; -use perry_container_compose::testing::mock_backend::{InspectMode, MockBackend, RecordedCall}; +use perry_container_compose::testing::mock_backend::{MockBackend, RecordedCall}; use perry_container_compose::types::{ - ComposeNetwork, ComposeService, ComposeSpec, ComposeVolume, ServiceNetworks, + ComposeNetwork, ComposeService, ComposeSpec, ComposeVolume, ServiceNetworks, VolumeEntry, }; use indexmap::IndexMap; use std::sync::Arc; @@ -44,7 +44,7 @@ fn svc_with_net(image: &str, net: &str) -> ComposeService { fn svc_with_vol(image: &str, vol: &str) -> ComposeService { ComposeService { image: Some(image.to_string()), - volumes: Some(vec![serde_yaml::Value::String(vol.to_string())]), + volumes: Some(vec![VolumeEntry::Short(vol.to_string())]), ..Default::default() } }