From c349d130d3e23c8b4d72ab8cc71160ea2616eaf1 Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Fri, 6 Jun 2025 12:55:31 +0200 Subject: [PATCH 1/6] Refactor into submodules --- src/lib.rs | 893 +------------------------------------------------ src/network.rs | 85 +++++ src/secret.rs | 70 ++++ src/service.rs | 670 +++++++++++++++++++++++++++++++++++++ src/volume.rs | 96 ++++++ 5 files changed, 933 insertions(+), 881 deletions(-) create mode 100644 src/network.rs create mode 100644 src/secret.rs create mode 100644 src/service.rs create mode 100644 src/volume.rs diff --git a/src/lib.rs b/src/lib.rs index 13f6e62..f9cb876 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ use derive_builder::*; #[cfg(feature = "indexmap")] use indexmap::IndexMap; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "yml")] use serde_yml as serde_yaml; #[cfg(not(feature = "indexmap"))] @@ -12,6 +12,16 @@ use std::str::FromStr; use serde_yaml::Value; +mod service; +mod network; +mod volume; +mod secret; + +pub use service::*; +pub use network::*; +pub use volume::*; +pub use secret::*; + #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] @@ -59,335 +69,6 @@ impl Compose { } } -#[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)] -#[builder(setter(into), default)] -pub struct Service { - #[serde(skip_serializing_if = "Option::is_none")] - pub hostname: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub domainname: Option, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub privileged: bool, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub read_only: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub healthcheck: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub deploy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub image: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub container_name: Option, - #[serde(skip_serializing_if = "Option::is_none", rename = "build")] - pub build_: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pid: Option, - #[serde(default, skip_serializing_if = "Ports::is_empty")] - pub ports: Ports, - #[serde(default, skip_serializing_if = "Environment::is_empty")] - pub environment: Environment, - #[serde(skip_serializing_if = "Option::is_none")] - pub network_mode: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub devices: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub restart: Option, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, - #[serde(skip_serializing_if = "Option::is_none")] - pub tmpfs: Option, - #[serde(default, skip_serializing_if = "Ulimits::is_empty")] - pub ulimits: Ulimits, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub volumes: Vec, - #[serde(default, skip_serializing_if = "Networks::is_empty")] - pub networks: Networks, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cap_add: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cap_drop: Vec, - #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")] - pub depends_on: DependsOnOptions, - #[serde(skip_serializing_if = "Option::is_none")] - pub command: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub entrypoint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub env_file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_grace_period: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub profiles: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub links: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dns: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub ipc: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub net: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_signal: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub user: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub userns_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub working_dir: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub expose: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub volumes_from: Vec, - #[cfg(feature = "indexmap")] - #[serde( - default, - deserialize_with = "de_extends_indexmap", - skip_serializing_if = "IndexMap::is_empty" - )] - pub extends: IndexMap, - #[cfg(not(feature = "indexmap"))] - #[serde( - default, - deserialize_with = "de_extends_hashmap", - skip_serializing_if = "HashMap::is_empty" - )] - pub extends: HashMap, - #[serde(skip_serializing_if = "Option::is_none")] - pub logging: Option, - #[serde(default, skip_serializing_if = "is_zero")] - pub scale: i64, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub init: bool, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub stdin_open: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub shm_size: Option, - #[cfg(feature = "indexmap")] - #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] - pub extensions: IndexMap, - #[cfg(not(feature = "indexmap"))] - #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] - pub extensions: HashMap, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub extra_hosts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub group_add: Vec, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub tty: bool, - #[serde(default, skip_serializing_if = "SysCtls::is_empty")] - pub sysctls: SysCtls, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub security_opt: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub secrets: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pull_policy: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cgroup_parent: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mem_limit: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mem_reservation: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mem_swappiness: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub runtime: Option, -} - -#[cfg(feature = "indexmap")] -fn de_extends_indexmap<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - if let Some(value_str) = value.as_str() { - let mut map = IndexMap::new(); - map.insert("service".to_string(), value_str.to_string()); - return Ok(map); - } - - if let Some(value_map) = value.as_mapping() { - let mut map = IndexMap::new(); - for (k, v) in value_map { - if !k.is_string() || !v.is_string() { - return Err(serde::de::Error::custom( - "extends must must have string type for both Keys and Values".to_string(), - )); - } - //Should be safe due to previous check - map.insert( - k.as_str().unwrap().to_string(), - v.as_str().unwrap().to_string(), - ); - } - return Ok(map); - } - - Err(serde::de::Error::custom( - "extends must either be a map or a string".to_string(), - )) -} - -#[cfg(not(feature = "indexmap"))] -fn de_extends_hashmap<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - if let Some(value_str) = value.as_str() { - let mut map = HashMap::new(); - map.insert("service".to_string(), value_str.to_string()); - return Ok(map); - } - - if let Some(value_map) = value.as_mapping() { - let mut map = HashMap::new(); - for (k, v) in value_map { - if !k.is_string() || !v.is_string() { - return Err(serde::de::Error::custom( - "extends must must have string type for both Keys and Values".to_string(), - )); - } - //Should be safe due to previous check - map.insert( - k.as_str().unwrap().to_string(), - v.as_str().unwrap().to_string(), - ); - } - return Ok(map); - } - - Err(serde::de::Error::custom( - "extends must either be a map or a string".to_string(), - )) -} - -impl Service { - pub fn image(&self) -> &str { - self.image.as_deref().unwrap_or_default() - } - - pub fn network_mode(&self) -> &str { - self.network_mode.as_deref().unwrap_or_default() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum EnvFile { - Simple(String), - List(Vec), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum DependsOnOptions { - Simple(Vec), - #[cfg(feature = "indexmap")] - Conditional(IndexMap), - #[cfg(not(feature = "indexmap"))] - Conditional(HashMap), -} - -impl Default for DependsOnOptions { - fn default() -> Self { - Self::Simple(Vec::new()) - } -} - -impl DependsOnOptions { - pub fn is_empty(&self) -> bool { - match self { - Self::Simple(v) => v.is_empty(), - Self::Conditional(m) => m.is_empty(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -pub struct DependsCondition { - pub condition: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct LoggingParameters { - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[cfg(feature = "indexmap")] - #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option>, - #[cfg(not(feature = "indexmap"))] - #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum Ports { - Short(Vec), - Long(Vec), -} - -impl Default for Ports { - fn default() -> Self { - Self::Short(Vec::default()) - } -} - -impl Ports { - pub fn is_empty(&self) -> bool { - match self { - Self::Short(v) => v.is_empty(), - Self::Long(v) => v.is_empty(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Port { - pub target: u16, - #[serde(skip_serializing_if = "Option::is_none")] - pub host_ip: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub published: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub protocol: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum PublishedPort { - Single(u16), - Range(String), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum Environment { - List(Vec), - #[cfg(feature = "indexmap")] - KvPair(IndexMap>), - #[cfg(not(feature = "indexmap"))] - KvPair(HashMap>), -} - -impl Default for Environment { - fn default() -> Self { - Self::List(Vec::new()) - } -} - -impl Environment { - pub fn is_empty(&self) -> bool { - match self { - Self::List(v) => v.is_empty(), - Self::KvPair(m) => m.is_empty(), - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default, Ord, PartialOrd)] #[serde(try_from = "String")] pub struct Extension(String); @@ -427,550 +108,6 @@ impl fmt::Display for ExtensionParseError { impl std::error::Error for ExtensionParseError {} -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Services(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Services(pub HashMap>); - -impl Services { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Labels { - List(Vec), - #[cfg(feature = "indexmap")] - Map(IndexMap), - #[cfg(not(feature = "indexmap"))] - Map(HashMap), -} - -impl Default for Labels { - fn default() -> Self { - Self::List(Vec::new()) - } -} - -impl Labels { - pub fn is_empty(&self) -> bool { - match self { - Self::List(v) => v.is_empty(), - Self::Map(m) => m.is_empty(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Tmpfs { - Simple(String), - List(Vec), -} - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct Ulimits(pub IndexMap); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct Ulimits(pub HashMap); - -impl Ulimits { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Ulimit { - Single(i64), - SoftHard { soft: i64, hard: i64 }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Networks { - Simple(Vec), - Advanced(AdvancedNetworks), -} - -impl Default for Networks { - fn default() -> Self { - Self::Simple(Vec::new()) - } -} - -impl Networks { - pub fn is_empty(&self) -> bool { - match self { - Self::Simple(n) => n.is_empty(), - Self::Advanced(n) => n.0.is_empty(), - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum BuildStep { - Simple(String), - Advanced(AdvancedBuildStep), -} - -#[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] -#[serde(deny_unknown_fields)] -#[builder(setter(into), default)] -pub struct AdvancedBuildStep { - pub context: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dockerfile: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub args: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub shm_size: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub target: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub network: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cache_from: Vec, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum BuildArgs { - Simple(String), - List(Vec), - #[cfg(feature = "indexmap")] - KvPair(IndexMap), - #[cfg(not(feature = "indexmap"))] - KvPair(HashMap), -} - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AdvancedNetworks(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AdvancedNetworks(pub HashMap>); - -#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct AdvancedNetworkSettings { - #[serde(skip_serializing_if = "Option::is_none")] - pub ipv4_address: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ipv6_address: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub aliases: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum SysCtls { - List(Vec), - #[cfg(feature = "indexmap")] - Map(IndexMap>), - #[cfg(not(feature = "indexmap"))] - Map(HashMap>), -} - -impl Default for SysCtls { - fn default() -> Self { - Self::List(Vec::new()) - } -} - -impl SysCtls { - pub fn is_empty(&self) -> bool { - match self { - Self::List(v) => v.is_empty(), - Self::Map(m) => m.is_empty(), - } - } -} - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct TopLevelVolumes(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct TopLevelVolumes(pub HashMap>); - -impl TopLevelVolumes { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeVolume { - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[cfg(feature = "indexmap")] - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub driver_opts: IndexMap>, - #[cfg(not(feature = "indexmap"))] - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub driver_opts: HashMap>, - #[serde(skip_serializing_if = "Option::is_none")] - pub external: Option, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum ExternalVolume { - Bool(bool), - Name { name: String }, -} - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeNetworks(pub IndexMap>); - -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeNetworks(pub HashMap>); - -impl ComposeNetworks { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum ComposeNetwork { - Detailed(ComposeNetworkSettingDetails), - Bool(bool), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct ComposeNetworkSettingDetails { - pub name: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct ExternalNetworkSettingBool(bool); - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct NetworkSettings { - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub attachable: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[cfg(feature = "indexmap")] - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub driver_opts: IndexMap>, - #[cfg(not(feature = "indexmap"))] - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub driver_opts: HashMap>, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub enable_ipv6: bool, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub internal: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub external: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ipam: Option, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct Ipam { - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub config: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct IpamConfig { - pub subnet: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub gateway: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct Deploy { - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub replicas: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub labels: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub update_config: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub resources: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub restart_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub placement: Option, -} - -fn is_zero(val: &i64) -> bool { - *val == 0 -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct Healthcheck { - #[serde(skip_serializing_if = "Option::is_none")] - pub test: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub interval: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, - #[serde(default, skip_serializing_if = "is_zero")] - pub retries: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_period: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_interval: Option, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub disable: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum HealthcheckTest { - Single(String), - Multiple(Vec), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct Limits { - #[serde(skip_serializing_if = "Option::is_none")] - pub cpus: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub devices: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct Device { - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub count: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub device_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub capabilities: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg(feature = "indexmap")] - pub options: Option>, - #[cfg(not(feature = "indexmap"))] - pub options: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Placement { - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub constraints: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub preferences: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct Preferences { - pub spread: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct Resources { - pub limits: Option, - pub reservations: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct RestartPolicy { - #[serde(skip_serializing_if = "Option::is_none")] - pub condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delay: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_attempts: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub window: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct UpdateConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub parallelism: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delay: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub failure_action: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub monitor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_failure_ratio: Option, -} - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeSecrets( - #[serde(with = "serde_yaml::with::singleton_map_recursive")] - pub IndexMap>, -); - -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeSecrets( - #[serde(with = "serde_yaml::with::singleton_map_recursive")] - pub HashMap>, -); - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(deny_unknown_fields, rename_all = "snake_case")] -pub enum ComposeSecret { - File(String), - Environment(String), - #[serde(untagged)] - External { - external: bool, - name: String, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Secrets { - Simple(Vec), - Advanced(Vec), -} - -impl Default for Secrets { - fn default() -> Self { - Self::Simple(Vec::new()) - } -} - -impl Secrets { - pub fn is_empty(&self) -> bool { - match self { - Self::Simple(v) => v.is_empty(), - Self::Advanced(v) => v.is_empty(), - } - } -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct AdvancedSecrets { - pub source: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub target: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub uid: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub gid: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mode: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(rename_all = "lowercase")] -pub enum PullPolicy { - Always, - Never, - #[serde(alias = "if_not_present")] - Missing, - Build, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Volumes { - Simple(String), - Advanced(AdvancedVolumes), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct AdvancedVolumes { - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - pub target: String, - #[serde(rename = "type")] - pub _type: String, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub read_only: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bind: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub volume: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tmpfs: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Bind { - pub propagation: Option, - pub create_host_path: Option, - pub selinux: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Volume { - #[serde(skip_serializing_if = "Option::is_none")] - pub nocopy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subpath: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct TmpfsSettings { - pub size: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Command { - Simple(String), - Args(Vec), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Entrypoint { - Simple(String), - List(Vec), -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] #[serde(untagged)] pub enum SingleValue { @@ -993,13 +130,6 @@ impl fmt::Display for SingleValue { } } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Group { - Named(String), - Gid(u32), -} - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum MapOrEmpty { @@ -1039,3 +169,4 @@ where } } } + diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..011cd61 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,85 @@ +// Network related structures extracted from lib.rs + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +#[cfg(not(feature = "indexmap"))] +use std::collections::HashMap; + +use crate::{Labels, SingleValue, MapOrEmpty}; + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeNetworks(pub IndexMap>); + +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeNetworks(pub HashMap>); + +impl ComposeNetworks { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum ComposeNetwork { + Detailed(ComposeNetworkSettingDetails), + Bool(bool), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct ComposeNetworkSettingDetails { + pub name: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct ExternalNetworkSettingBool(bool); + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct NetworkSettings { + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub attachable: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[cfg(feature = "indexmap")] + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub driver_opts: IndexMap>, + #[cfg(not(feature = "indexmap"))] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub driver_opts: HashMap>, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub enable_ipv6: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub internal: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub external: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipam: Option, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Ipam { + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub config: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct IpamConfig { + pub subnet: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway: Option, +} + diff --git a/src/secret.rs b/src/secret.rs new file mode 100644 index 0000000..cb04e51 --- /dev/null +++ b/src/secret.rs @@ -0,0 +1,70 @@ +// Secret related structures extracted from lib.rs + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +#[cfg(not(feature = "indexmap"))] +use std::collections::HashMap; + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeSecrets( + #[serde(with = "serde_yaml::with::singleton_map_recursive")] + pub IndexMap>, +); + +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeSecrets( + #[serde(with = "serde_yaml::with::singleton_map_recursive")] + pub HashMap>, +); + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum ComposeSecret { + File(String), + Environment(String), + #[serde(untagged)] + External { + external: bool, + name: String, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Secrets { + Simple(Vec), + Advanced(Vec), +} + +impl Default for Secrets { + fn default() -> Self { + Self::Simple(Vec::new()) + } +} + +impl Secrets { + pub fn is_empty(&self) -> bool { + match self { + Self::Simple(v) => v.is_empty(), + Self::Advanced(v) => v.is_empty(), + } + } +} + +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct AdvancedSecrets { + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub uid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..4be7ea3 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,670 @@ +// Service related structures and enums extracted from lib.rs + +use derive_builder::*; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +use serde::{Deserialize, Deserializer, Serialize}; +#[cfg(not(feature = "indexmap"))] +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; + +use serde_yaml::Value; + +use crate::{Labels, SingleValue, Volumes, MapOrEmpty, Secrets}; + +#[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)] +#[builder(setter(into), default)] +pub struct Service { + #[serde(skip_serializing_if = "Option::is_none")] + pub hostname: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domainname: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub privileged: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub read_only: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub healthcheck: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub deploy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub container_name: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "build")] + pub build_: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub pid: Option, + #[serde(default, skip_serializing_if = "Ports::is_empty")] + pub ports: Ports, + #[serde(default, skip_serializing_if = "Environment::is_empty")] + pub environment: Environment, + #[serde(skip_serializing_if = "Option::is_none")] + pub network_mode: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub devices: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub restart: Option, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, + #[serde(skip_serializing_if = "Option::is_none")] + pub tmpfs: Option, + #[serde(default, skip_serializing_if = "Ulimits::is_empty")] + pub ulimits: Ulimits, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub volumes: Vec, + #[serde(default, skip_serializing_if = "Networks::is_empty")] + pub networks: Networks, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cap_add: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cap_drop: Vec, + #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")] + pub depends_on: DependsOnOptions, + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub entrypoint: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub env_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_grace_period: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub profiles: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub links: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub dns: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub net: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_signal: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub userns_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub working_dir: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub expose: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub volumes_from: Vec, + #[cfg(feature = "indexmap")] + #[serde( + default, + deserialize_with = "de_extends_indexmap", + skip_serializing_if = "IndexMap::is_empty" + )] + pub extends: IndexMap, + #[cfg(not(feature = "indexmap"))] + #[serde( + default, + deserialize_with = "de_extends_hashmap", + skip_serializing_if = "HashMap::is_empty" + )] + pub extends: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub logging: Option, + #[serde(default, skip_serializing_if = "is_zero")] + pub scale: i64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub init: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub stdin_open: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub shm_size: Option, + #[cfg(feature = "indexmap")] + #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] + pub extensions: IndexMap, + #[cfg(not(feature = "indexmap"))] + #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] + pub extensions: HashMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub extra_hosts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub group_add: Vec, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub tty: bool, + #[serde(default, skip_serializing_if = "SysCtls::is_empty")] + pub sysctls: SysCtls, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub security_opt: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub secrets: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pull_policy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cgroup_parent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mem_limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mem_reservation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mem_swappiness: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub runtime: Option, +} + +#[cfg(feature = "indexmap")] +fn de_extends_indexmap<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + if let Some(value_str) = value.as_str() { + let mut map = IndexMap::new(); + map.insert("service".to_string(), value_str.to_string()); + return Ok(map); + } + + if let Some(value_map) = value.as_mapping() { + let mut map = IndexMap::new(); + for (k, v) in value_map { + if !k.is_string() || !v.is_string() { + return Err(serde::de::Error::custom( + "extends must must have string type for both Keys and Values".to_string(), + )); + } + map.insert( + k.as_str().unwrap().to_string(), + v.as_str().unwrap().to_string(), + ); + } + return Ok(map); + } + + Err(serde::de::Error::custom( + "extends must either be a map or a string".to_string(), + )) +} + +#[cfg(not(feature = "indexmap"))] +fn de_extends_hashmap<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + if let Some(value_str) = value.as_str() { + let mut map = HashMap::new(); + map.insert("service".to_string(), value_str.to_string()); + return Ok(map); + } + + if let Some(value_map) = value.as_mapping() { + let mut map = HashMap::new(); + for (k, v) in value_map { + if !k.is_string() || !v.is_string() { + return Err(serde::de::Error::custom( + "extends must must have string type for both Keys and Values".to_string(), + )); + } + map.insert( + k.as_str().unwrap().to_string(), + v.as_str().unwrap().to_string(), + ); + } + return Ok(map); + } + + Err(serde::de::Error::custom( + "extends must either be a map or a string".to_string(), + )) +} + +impl Service { + pub fn image(&self) -> &str { + self.image.as_deref().unwrap_or_default() + } + + pub fn network_mode(&self) -> &str { + self.network_mode.as_deref().unwrap_or_default() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum EnvFile { + Simple(String), + List(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum DependsOnOptions { + Simple(Vec), + #[cfg(feature = "indexmap")] + Conditional(IndexMap), + #[cfg(not(feature = "indexmap"))] + Conditional(HashMap), +} + +impl Default for DependsOnOptions { + fn default() -> Self { + Self::Simple(Vec::new()) + } +} + +impl DependsOnOptions { + pub fn is_empty(&self) -> bool { + match self { + Self::Simple(v) => v.is_empty(), + Self::Conditional(m) => m.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub struct DependsCondition { + pub condition: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct LoggingParameters { + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[cfg(feature = "indexmap")] + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, + #[cfg(not(feature = "indexmap"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum Ports { + Short(Vec), + Long(Vec), +} + +impl Default for Ports { + fn default() -> Self { + Self::Short(Vec::default()) + } +} + +impl Ports { + pub fn is_empty(&self) -> bool { + match self { + Self::Short(v) => v.is_empty(), + Self::Long(v) => v.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Port { + pub target: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub published: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum PublishedPort { + Single(u16), + Range(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum Environment { + List(Vec), + #[cfg(feature = "indexmap")] + KvPair(IndexMap>), + #[cfg(not(feature = "indexmap"))] + KvPair(HashMap>), +} + +impl Default for Environment { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl Environment { + pub fn is_empty(&self) -> bool { + match self { + Self::List(v) => v.is_empty(), + Self::KvPair(m) => m.is_empty(), + } + } +} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Services(pub IndexMap>); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Services(pub HashMap>); + +impl Services { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum Labels { + List(Vec), + #[cfg(feature = "indexmap")] + Map(IndexMap), + #[cfg(not(feature = "indexmap"))] + Map(HashMap), +} + +impl Default for Labels { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl Labels { + pub fn is_empty(&self) -> bool { + match self { + Self::List(v) => v.is_empty(), + Self::Map(m) => m.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum Tmpfs { + Simple(String), + List(Vec), +} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct Ulimits(pub IndexMap); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct Ulimits(pub HashMap); + +impl Ulimits { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum Ulimit { + Single(i64), + SoftHard { soft: i64, hard: i64 }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum Networks { + Simple(Vec), + Advanced(AdvancedNetworks), +} + +impl Default for Networks { + fn default() -> Self { + Self::Simple(Vec::new()) + } +} + +impl Networks { + pub fn is_empty(&self) -> bool { + match self { + Self::Simple(n) => n.is_empty(), + Self::Advanced(n) => n.0.is_empty(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum BuildStep { + Simple(String), + Advanced(AdvancedBuildStep), +} + +#[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] +#[serde(deny_unknown_fields)] +#[builder(setter(into), default)] +pub struct AdvancedBuildStep { + pub context: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dockerfile: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub shm_size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub network: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cache_from: Vec, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum BuildArgs { + Simple(String), + List(Vec), + #[cfg(feature = "indexmap")] + KvPair(IndexMap), + #[cfg(not(feature = "indexmap"))] + KvPair(HashMap), +} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AdvancedNetworks(pub IndexMap>); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AdvancedNetworks(pub HashMap>); + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct AdvancedNetworkSettings { + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv4_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv6_address: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub aliases: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum SysCtls { + List(Vec), + #[cfg(feature = "indexmap")] + Map(IndexMap>), + #[cfg(not(feature = "indexmap"))] + Map(HashMap>), +} + +impl Default for SysCtls { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl SysCtls { + pub fn is_empty(&self) -> bool { + match self { + Self::List(v) => v.is_empty(), + Self::Map(m) => m.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct Deploy { + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub replicas: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub labels: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub update_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub resources: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub restart_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub placement: Option, +} + +fn is_zero(val: &i64) -> bool { + *val == 0 +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Healthcheck { + #[serde(skip_serializing_if = "Option::is_none")] + pub test: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub interval: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + #[serde(default, skip_serializing_if = "is_zero")] + pub retries: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_interval: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub disable: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum HealthcheckTest { + Single(String), + Multiple(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct Limits { + #[serde(skip_serializing_if = "Option::is_none")] + pub cpus: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub devices: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct Device { + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub device_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub capabilities: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "indexmap")] + pub options: Option>, + #[cfg(not(feature = "indexmap"))] + pub options: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Placement { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub constraints: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub preferences: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Preferences { + pub spread: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct Resources { + pub limits: Option, + pub reservations: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct RestartPolicy { + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_attempts: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub window: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct UpdateConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub parallelism: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub failure_action: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub monitor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_failure_ratio: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum PullPolicy { + Always, + Never, + #[serde(alias = "if_not_present")] + Missing, + Build, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Command { + Simple(String), + Args(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Entrypoint { + Simple(String), + List(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub enum Group { + Named(String), + Gid(u32), +} + diff --git a/src/volume.rs b/src/volume.rs new file mode 100644 index 0000000..80f88a2 --- /dev/null +++ b/src/volume.rs @@ -0,0 +1,96 @@ +// Volume related structures extracted from lib.rs + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +#[cfg(not(feature = "indexmap"))] +use std::collections::HashMap; + +use crate::{Labels, SingleValue, MapOrEmpty}; + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopLevelVolumes(pub IndexMap>); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopLevelVolumes(pub HashMap>); + +impl TopLevelVolumes { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeVolume { + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[cfg(feature = "indexmap")] + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub driver_opts: IndexMap>, + #[cfg(not(feature = "indexmap"))] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub driver_opts: HashMap>, + #[serde(skip_serializing_if = "Option::is_none")] + pub external: Option, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum ExternalVolume { + Bool(bool), + Name { name: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Volumes { + Simple(String), + Advanced(AdvancedVolumes), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct AdvancedVolumes { + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + pub target: String, + #[serde(rename = "type")] + pub _type: String, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub read_only: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bind: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub volume: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tmpfs: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Bind { + pub propagation: Option, + pub create_host_path: Option, + pub selinux: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Volume { + #[serde(skip_serializing_if = "Option::is_none")] + pub nocopy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub subpath: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct TmpfsSettings { + pub size: u64, +} + From 4a00e1ea0dc12e011d4f621b577b4a480be9400e Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Fri, 6 Jun 2025 13:05:07 +0200 Subject: [PATCH 2/6] refactor: remove unused imports from lib.rs and service.rs --- src/lib.rs | 2 -- src/service.rs | 7 ++----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f9cb876..6bfae6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -use derive_builder::*; #[cfg(feature = "indexmap")] use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -169,4 +168,3 @@ where } } } - diff --git a/src/service.rs b/src/service.rs index 4be7ea3..1689fbb 100644 --- a/src/service.rs +++ b/src/service.rs @@ -6,13 +6,10 @@ use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize}; #[cfg(not(feature = "indexmap"))] use std::collections::HashMap; -use std::convert::TryFrom; -use std::fmt; -use std::str::FromStr; use serde_yaml::Value; -use crate::{Labels, SingleValue, Volumes, MapOrEmpty, Secrets}; +use crate::{SingleValue, Volumes, MapOrEmpty, Secrets}; #[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)] #[builder(setter(into), default)] @@ -663,8 +660,8 @@ pub enum Entrypoint { } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] pub enum Group { Named(String), Gid(u32), } - From 7cf33a0308645676009df25fdc48cc6a488cfa1e Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Fri, 6 Jun 2025 13:11:45 +0200 Subject: [PATCH 3/6] chore: update README and add CI/CD configuration files --- .github/dependabot.yml | 9 ++++ .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++ .github/workflows/code-quality.yml | 72 ++++++++++++++++++++++++++++ README.md | 2 + 4 files changed, 160 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/code-quality.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..284450f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..83a91fd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + - nightly + features: + - "" # default features + - "--no-default-features --features indexmap,yaml" + - "--no-default-features --features indexmap,yml" + - "--no-default-features --features yaml" + - "--no-default-features --features yml" + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust ${{ matrix.rust }} + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: ${{ matrix.features }} + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: ${{ matrix.features }} + + # Test on Windows and macOS with stable Rust only + test-platforms: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..c0bb7be --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,72 @@ +name: Code Quality + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + + - name: Clippy check + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features -- -D warnings + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Check documentation + uses: actions-rs/cargo@v1 + with: + command: doc + args: --no-deps --all-features + env: + RUSTDOCFLAGS: -D warnings \ No newline at end of file diff --git a/README.md b/README.md index 740088f..5455337 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Docker Compose Types =========== [![crates.io](https://img.shields.io/crates/v/docker-compose-types.svg)](https://crates.io/crates/docker-compose-types) +[![CI](https://github.com/stephanbuys/docker-compose-types/actions/workflows/ci.yml/badge.svg)](https://github.com/stephanbuys/docker-compose-types/actions/workflows/ci.yml) +[![Code Quality](https://github.com/stephanbuys/docker-compose-types/actions/workflows/code-quality.yml/badge.svg)](https://github.com/stephanbuys/docker-compose-types/actions/workflows/code-quality.yml) Contributions are very welcome, the idea behind this crate is to allow for safe serialization of docker-compose files with as little room for error as possible. From 5bf94bffae9ff802fb58599815b5bbf239fe391b Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Fri, 6 Jun 2025 13:12:13 +0200 Subject: [PATCH 4/6] refactor: reorder imports and update module visibility in lib.rs, network.rs, secret.rs, service.rs, and volume.rs --- .github/dependabot-ver.yml | 11 ----------- src/lib.rs | 8 ++++---- src/network.rs | 5 ++--- src/secret.rs | 3 +-- src/service.rs | 4 ++-- src/volume.rs | 5 ++--- 6 files changed, 11 insertions(+), 25 deletions(-) delete mode 100644 .github/dependabot-ver.yml diff --git a/.github/dependabot-ver.yml b/.github/dependabot-ver.yml deleted file mode 100644 index ac6621f..0000000 --- a/.github/dependabot-ver.yml +++ /dev/null @@ -1,11 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" diff --git a/src/lib.rs b/src/lib.rs index 6bfae6d..db35d1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,15 +11,15 @@ use std::str::FromStr; use serde_yaml::Value; -mod service; mod network; -mod volume; mod secret; +mod service; +mod volume; -pub use service::*; pub use network::*; -pub use volume::*; pub use secret::*; +pub use service::*; +pub use volume::*; #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/src/network.rs b/src/network.rs index 011cd61..94f1d3c 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,12 +1,12 @@ // Network related structures extracted from lib.rs -use serde::{Deserialize, Serialize}; #[cfg(feature = "indexmap")] use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; #[cfg(not(feature = "indexmap"))] use std::collections::HashMap; -use crate::{Labels, SingleValue, MapOrEmpty}; +use crate::{Labels, MapOrEmpty, SingleValue}; #[cfg(feature = "indexmap")] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] @@ -82,4 +82,3 @@ pub struct IpamConfig { #[serde(skip_serializing_if = "Option::is_none")] pub gateway: Option, } - diff --git a/src/secret.rs b/src/secret.rs index cb04e51..87e1905 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -1,8 +1,8 @@ // Secret related structures extracted from lib.rs -use serde::{Deserialize, Serialize}; #[cfg(feature = "indexmap")] use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; #[cfg(not(feature = "indexmap"))] use std::collections::HashMap; @@ -67,4 +67,3 @@ pub struct AdvancedSecrets { #[serde(default, skip_serializing_if = "Option::is_none")] pub mode: Option, } - diff --git a/src/service.rs b/src/service.rs index 1689fbb..16dfe29 100644 --- a/src/service.rs +++ b/src/service.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use serde_yaml::Value; -use crate::{SingleValue, Volumes, MapOrEmpty, Secrets}; +use crate::{MapOrEmpty, Secrets, SingleValue, Volumes}; #[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)] #[builder(setter(into), default)] @@ -429,7 +429,7 @@ impl Networks { #[serde(untagged)] pub enum BuildStep { Simple(String), - Advanced(AdvancedBuildStep), + Advanced(Box), } #[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] diff --git a/src/volume.rs b/src/volume.rs index 80f88a2..35af965 100644 --- a/src/volume.rs +++ b/src/volume.rs @@ -1,12 +1,12 @@ // Volume related structures extracted from lib.rs -use serde::{Deserialize, Serialize}; #[cfg(feature = "indexmap")] use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; #[cfg(not(feature = "indexmap"))] use std::collections::HashMap; -use crate::{Labels, SingleValue, MapOrEmpty}; +use crate::{Labels, MapOrEmpty, SingleValue}; #[cfg(feature = "indexmap")] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] @@ -93,4 +93,3 @@ pub struct Volume { pub struct TmpfsSettings { pub size: u64, } - From dbd2095a794e338a02d9e5643db714864947304f Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Fri, 6 Jun 2025 14:36:44 +0200 Subject: [PATCH 5/6] refactor: enhance documentation for Compose file structures and service configurations --- src/lib.rs | 17 +++++- src/network.rs | 28 ++++++++++ src/secret.rs | 19 +++++++ src/service.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++++- src/volume.rs | 34 ++++++++++++ 5 files changed, 244 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index db35d1e..845ae6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,39 +21,54 @@ pub use secret::*; pub use service::*; pub use volume::*; +/// Represents a Docker Compose document, supporting different formats and versions. #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum ComposeFile { + /// Version 2 and above compose file structure. V2Plus(Compose), #[cfg(feature = "indexmap")] + /// Legacy v1 service definitions as a map of service names to `Service`. V1(IndexMap), #[cfg(not(feature = "indexmap"))] + /// Legacy v1 service definitions as a standard `HashMap`. V1(HashMap), + /// Single service definition format, wrapping one `Service` instance. Single(SingleService), } +/// Wrapper for a single `Service` when using the single service format. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] pub struct SingleService { + /// The single service defined in the document ('service'). pub service: Service, } +/// Core Compose model containing version, services, volumes, networks, and extensions. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] pub struct Compose { + /// Compose specification version string (e.g., '"3.8"'). #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, - #[serde(skip_serializing_if = "Option::is_none")] + /// Optional project name override ('name'). pub name: Option, + /// Service definitions map ('services'). #[serde(default, skip_serializing_if = "Services::is_empty")] pub services: Services, + /// Top-level volume definitions ('volumes'). #[serde(default, skip_serializing_if = "TopLevelVolumes::is_empty")] pub volumes: TopLevelVolumes, + /// Network definitions ('networks'). #[serde(default, skip_serializing_if = "ComposeNetworks::is_empty")] pub networks: ComposeNetworks, + /// Optional single service inline support ('service'). #[serde(skip_serializing_if = "Option::is_none")] pub service: Option, + /// Top-level secret definitions ('secrets'). #[serde(skip_serializing_if = "Option::is_none")] pub secrets: Option, + /// Extension fields (keys starting with 'x-') flattened into a map. #[cfg(feature = "indexmap")] #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] pub extensions: IndexMap, diff --git a/src/network.rs b/src/network.rs index 94f1d3c..a5324c4 100644 --- a/src/network.rs +++ b/src/network.rs @@ -8,10 +8,14 @@ use std::collections::HashMap; use crate::{Labels, MapOrEmpty, SingleValue}; +/// Container for network definitions in a Compose file. +/// Maps network names to their configuration settings. #[cfg(feature = "indexmap")] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct ComposeNetworks(pub IndexMap>); +/// Container for network definitions in a Compose file. +/// Maps network names to their configuration settings. #[cfg(not(feature = "indexmap"))] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct ComposeNetworks(pub HashMap>); @@ -22,63 +26,87 @@ impl ComposeNetworks { } } +/// Represents a network configuration in a Compose file. +/// Can be either a detailed configuration or a simple boolean value. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum ComposeNetwork { + /// Detailed network configuration with specific settings. Detailed(ComposeNetworkSettingDetails), + /// Simple boolean flag for enabling/disabling a network. Bool(bool), } +/// Detailed configuration for a network in a Compose file. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct ComposeNetworkSettingDetails { + /// Name of the network. pub name: String, } +/// Boolean wrapper for external network settings. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct ExternalNetworkSettingBool(bool); +/// Configuration settings for a network in a Compose file. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] #[serde(deny_unknown_fields)] pub struct NetworkSettings { + /// Whether the network can be attached to by external containers. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub attachable: bool, + /// Network driver to use for this network. #[serde(skip_serializing_if = "Option::is_none")] pub driver: Option, + /// Driver-specific options for this network. #[cfg(feature = "indexmap")] #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub driver_opts: IndexMap>, + /// Driver-specific options for this network. #[cfg(not(feature = "indexmap"))] #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub driver_opts: HashMap>, + /// Enable IPv6 networking on this network. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub enable_ipv6: bool, + /// Create an internal network that is isolated from external networks. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub internal: bool, + /// Specifies that this network is externally created. #[serde(skip_serializing_if = "Option::is_none")] pub external: Option, + /// IP Address Management configuration for this network. #[serde(skip_serializing_if = "Option::is_none")] pub ipam: Option, + /// Metadata labels for the network. #[serde(default, skip_serializing_if = "Labels::is_empty")] pub labels: Labels, + /// Custom name for the network. #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, } +/// IP Address Management configuration for a network. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct Ipam { + /// IPAM driver to use. #[serde(skip_serializing_if = "Option::is_none")] pub driver: Option, + /// List of IPAM configuration blocks. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub config: Vec, } +/// Configuration block for IPAM settings. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct IpamConfig { + /// Subnet in CIDR format. pub subnet: String, + /// Gateway address for the subnet. #[serde(skip_serializing_if = "Option::is_none")] pub gateway: Option, } diff --git a/src/secret.rs b/src/secret.rs index 87e1905..37b70da 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; #[cfg(not(feature = "indexmap"))] use std::collections::HashMap; +/// Container for secret definitions in a Compose file. +/// Maps secret names to their configuration settings. #[cfg(feature = "indexmap")] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct ComposeSecrets( @@ -13,6 +15,8 @@ pub struct ComposeSecrets( pub IndexMap>, ); +/// Container for secret definitions in a Compose file. +/// Maps secret names to their configuration settings. #[cfg(not(feature = "indexmap"))] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct ComposeSecrets( @@ -20,22 +24,31 @@ pub struct ComposeSecrets( pub HashMap>, ); +/// Represents a secret configuration in a Compose file. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub enum ComposeSecret { + /// Secret sourced from a file. File(String), + /// Secret sourced from an environment variable. Environment(String), + /// Secret that is externally managed. #[serde(untagged)] External { + /// Whether the secret is external. external: bool, + /// Name of the external secret. name: String, }, } +/// Represents secret configurations for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum Secrets { + /// Simple list of secret names. Simple(Vec), + /// Advanced secret configurations with detailed settings. Advanced(Vec), } @@ -54,16 +67,22 @@ impl Secrets { } } +/// Advanced secret configuration with detailed settings. #[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct AdvancedSecrets { + /// Name of the secret in the Compose file. pub source: String, + /// Name of the file to mount in the container. #[serde(skip_serializing_if = "Option::is_none")] pub target: Option, + /// UID of the secret file in the container. #[serde(default, skip_serializing_if = "Option::is_none")] pub uid: Option, + /// GID of the secret file in the container. #[serde(default, skip_serializing_if = "Option::is_none")] pub gid: Option, + /// File mode of the secret file in the container (octal). #[serde(default, skip_serializing_if = "Option::is_none")] pub mode: Option, } diff --git a/src/service.rs b/src/service.rs index 16dfe29..ad95e9a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -11,83 +11,122 @@ use serde_yaml::Value; use crate::{MapOrEmpty, Secrets, SingleValue, Volumes}; +/// Represents a service defined in the Compose file, mapping container configuration options. #[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)] #[builder(setter(into), default)] pub struct Service { + /// The hostname for the container ('hostname' in Compose). #[serde(skip_serializing_if = "Option::is_none")] pub hostname: Option, + /// The domain name for the container ('domainname' in Compose). #[serde(skip_serializing_if = "Option::is_none")] pub domainname: Option, + /// Give extended privileges to this container ('privileged'). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub privileged: bool, + /// Mount the container's root filesystem as read-only ('read_only'). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub read_only: bool, + /// Healthcheck configuration for the service ('healthcheck'). #[serde(skip_serializing_if = "Option::is_none")] pub healthcheck: Option, + /// Deployment configuration options ('deploy'). #[serde(skip_serializing_if = "Option::is_none")] pub deploy: Option, + /// Image to use for the service ('image'). #[serde(skip_serializing_if = "Option::is_none")] pub image: Option, + /// Custom container name ('container_name'). #[serde(skip_serializing_if = "Option::is_none")] pub container_name: Option, + /// Build configuration for the service ('build'). #[serde(skip_serializing_if = "Option::is_none", rename = "build")] pub build_: Option, + /// PID namespace to use for the container ('pid'). #[serde(skip_serializing_if = "Option::is_none")] pub pid: Option, + /// Port mappings for the service ('ports'). #[serde(default, skip_serializing_if = "Ports::is_empty")] pub ports: Ports, + /// Environment variables for the container ('environment'). #[serde(default, skip_serializing_if = "Environment::is_empty")] pub environment: Environment, + /// Network mode for the service ('network_mode'). #[serde(skip_serializing_if = "Option::is_none")] pub network_mode: Option, + /// Devices to expose to the container ('devices'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub devices: Vec, + /// Restart policy for the service ('restart'). #[serde(skip_serializing_if = "Option::is_none")] pub restart: Option, + /// Labels for the service ('labels'). #[serde(default, skip_serializing_if = "Labels::is_empty")] pub labels: Labels, + /// Mounts a tmpfs mount into the container ('tmpfs'). #[serde(skip_serializing_if = "Option::is_none")] pub tmpfs: Option, + /// Resource limit configurations ('ulimits'). #[serde(default, skip_serializing_if = "Ulimits::is_empty")] pub ulimits: Ulimits, + /// Volume configurations for the service ('volumes'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub volumes: Vec, + /// Network configurations for the service ('networks'). #[serde(default, skip_serializing_if = "Networks::is_empty")] pub networks: Networks, + /// Additional capabilities to add to the container ('cap_add'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub cap_add: Vec, + /// Capabilities to drop from the container ('cap_drop'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub cap_drop: Vec, + /// Dependencies for the service ('depends_on'). #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")] pub depends_on: DependsOnOptions, + /// Command to run in the container ('command'). #[serde(skip_serializing_if = "Option::is_none")] pub command: Option, + /// Entrypoint for the container ('entrypoint'). #[serde(skip_serializing_if = "Option::is_none")] pub entrypoint: Option, + /// Environment file to load ('env_file'). #[serde(skip_serializing_if = "Option::is_none")] pub env_file: Option, + /// Grace period for stopping the container ('stop_grace_period'). #[serde(skip_serializing_if = "Option::is_none")] pub stop_grace_period: Option, + /// Profiles the service is part of ('profiles'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub profiles: Vec, + /// Linked services ('links'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub links: Vec, + /// DNS servers for the container ('dns'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub dns: Vec, + /// IPC namespace to use for the container ('ipc'). #[serde(skip_serializing_if = "Option::is_none")] pub ipc: Option, + /// Network to use for the container ('net'). #[serde(skip_serializing_if = "Option::is_none")] pub net: Option, + /// Signal to stop the container ('stop_signal'). #[serde(skip_serializing_if = "Option::is_none")] pub stop_signal: Option, + /// User to run the container as ('user'). #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, + /// User namespace to use ('userns_mode'). #[serde(skip_serializing_if = "Option::is_none")] pub userns_mode: Option, + /// Working directory inside the container ('working_dir'). #[serde(skip_serializing_if = "Option::is_none")] pub working_dir: Option, + /// Ports to expose from the container ('expose'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub expose: Vec, + /// Volumes to inherit from the container ('volumes_from'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub volumes_from: Vec, #[cfg(feature = "indexmap")] @@ -104,14 +143,19 @@ pub struct Service { skip_serializing_if = "HashMap::is_empty" )] pub extends: HashMap, + /// Logging configuration for the service ('logging'). #[serde(skip_serializing_if = "Option::is_none")] pub logging: Option, + /// Number of replicas for the service ('scale'). #[serde(default, skip_serializing_if = "is_zero")] pub scale: i64, + /// Enable init system inside the container ('init'). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub init: bool, + /// Keep STDIN open even if not attached ('stdin_open'). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub stdin_open: bool, + /// Size of /dev/shm ('shm_size'). #[serde(skip_serializing_if = "Option::is_none")] pub shm_size: Option, #[cfg(feature = "indexmap")] @@ -120,28 +164,40 @@ pub struct Service { #[cfg(not(feature = "indexmap"))] #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] pub extensions: HashMap, + /// Additional hosts to add to the container's /etc/hosts ('extra_hosts'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub extra_hosts: Vec, + /// Groups to add the user to ('group_add'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub group_add: Vec, + /// Allocate a pseudo-TTY ('tty'). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub tty: bool, + /// Sysctl options to set in the container ('sysctls'). #[serde(default, skip_serializing_if = "SysCtls::is_empty")] pub sysctls: SysCtls, + /// Security options to apply to the container ('security_opt'). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub security_opt: Vec, + /// Secrets to expose to the service ('secrets'). #[serde(skip_serializing_if = "Option::is_none")] pub secrets: Option, + /// Image pull policy ('pull_policy'). #[serde(default, skip_serializing_if = "Option::is_none")] pub pull_policy: Option, + /// Parent cgroup for the container ('cgroup_parent'). #[serde(default, skip_serializing_if = "Option::is_none")] pub cgroup_parent: Option, + /// Memory limit for the container ('mem_limit'). #[serde(default, skip_serializing_if = "Option::is_none")] pub mem_limit: Option, + /// Memory reservation for the container ('mem_reservation'). #[serde(default, skip_serializing_if = "Option::is_none")] pub mem_reservation: Option, + /// Memory swappiness for the container ('mem_swappiness'). #[serde(default, skip_serializing_if = "Option::is_none")] pub mem_swappiness: Option, + /// Runtime to use for the container ('runtime'). #[serde(skip_serializing_if = "Option::is_none")] pub runtime: Option, } @@ -254,7 +310,7 @@ impl DependsOnOptions { } } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct DependsCondition { pub condition: String, } @@ -425,69 +481,96 @@ impl Networks { } } +/// Represents a build configuration for a service. #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(untagged)] pub enum BuildStep { + /// Simple build configuration with just a context path. Simple(String), + /// Advanced build configuration with detailed settings. Advanced(Box), } +/// Advanced build configuration with detailed settings. #[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields)] #[builder(setter(into), default)] pub struct AdvancedBuildStep { + /// Build context path. pub context: String, + /// Path to the Dockerfile relative to the build context. #[serde(default, skip_serializing_if = "Option::is_none")] pub dockerfile: Option, + /// Build-time variables to pass to the build process. #[serde(default, skip_serializing_if = "Option::is_none")] pub args: Option, + /// Size of /dev/shm in bytes for the build container. #[serde(default, skip_serializing_if = "Option::is_none")] pub shm_size: Option, + /// Target build stage to build. #[serde(default, skip_serializing_if = "Option::is_none")] pub target: Option, + /// Network mode for the build container. #[serde(default, skip_serializing_if = "Option::is_none")] pub network: Option, + /// Images to consider as cache sources. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub cache_from: Vec, + /// Metadata labels to apply to the built image. #[serde(default, skip_serializing_if = "Labels::is_empty")] pub labels: Labels, } +/// Build arguments to pass to the build process. #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(untagged)] pub enum BuildArgs { + /// Simple string build argument. Simple(String), + /// List of build arguments. List(Vec), + /// Key-value pairs of build arguments. #[cfg(feature = "indexmap")] KvPair(IndexMap), + /// Key-value pairs of build arguments. #[cfg(not(feature = "indexmap"))] KvPair(HashMap), } +/// Advanced network configurations for a service. #[cfg(feature = "indexmap")] #[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct AdvancedNetworks(pub IndexMap>); +/// Advanced network configurations for a service. #[cfg(not(feature = "indexmap"))] #[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct AdvancedNetworks(pub HashMap>); +/// Detailed network settings for a service. #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct AdvancedNetworkSettings { + /// IPv4 address for the container on this network. #[serde(skip_serializing_if = "Option::is_none")] pub ipv4_address: Option, + /// IPv6 address for the container on this network. #[serde(skip_serializing_if = "Option::is_none")] pub ipv6_address: Option, + /// Network aliases for the container on this network. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub aliases: Vec, } +/// Sysctl options to set in the container. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum SysCtls { + /// List of sysctl options as strings. List(Vec), + /// Map of sysctl option names to values. #[cfg(feature = "indexmap")] Map(IndexMap>), + /// Map of sysctl option names to values. #[cfg(not(feature = "indexmap"))] Map(HashMap>), } @@ -507,21 +590,29 @@ impl SysCtls { } } +/// Deployment configuration options for a service. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] #[serde(deny_unknown_fields)] pub struct Deploy { + /// Deployment mode (replicated or global). #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, + /// Number of container instances for the service. #[serde(skip_serializing_if = "Option::is_none")] pub replicas: Option, + /// Metadata labels for the deployed service. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub labels: Vec, + /// Configuration for how the service should be updated. #[serde(skip_serializing_if = "Option::is_none")] pub update_config: Option, + /// Resource constraints for the service. #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, + /// Restart policy for the service. #[serde(skip_serializing_if = "Option::is_none")] pub restart_policy: Option, + /// Placement constraints and preferences for the service. #[serde(skip_serializing_if = "Option::is_none")] pub placement: Option, } @@ -530,138 +621,193 @@ fn is_zero(val: &i64) -> bool { *val == 0 } +/// Healthcheck configuration for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct Healthcheck { + /// The test to perform to check container health. #[serde(skip_serializing_if = "Option::is_none")] pub test: Option, + /// Time between running the check. #[serde(skip_serializing_if = "Option::is_none")] pub interval: Option, + /// Maximum time to wait for a check to complete. #[serde(skip_serializing_if = "Option::is_none")] pub timeout: Option, + /// Number of consecutive failures needed to report unhealthy. #[serde(default, skip_serializing_if = "is_zero")] pub retries: i64, + /// Start period for the container to initialize before counting retries. #[serde(skip_serializing_if = "Option::is_none")] pub start_period: Option, + /// Time between running the check during the start period. #[serde(skip_serializing_if = "Option::is_none")] pub start_interval: Option, + /// Disable the healthcheck. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub disable: bool, } +/// Test to perform to check container health. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum HealthcheckTest { + /// Single string command. Single(String), + /// List of strings (command and its arguments). Multiple(Vec), } +/// Resource limits for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields)] pub struct Limits { + /// CPU limit for the service. #[serde(skip_serializing_if = "Option::is_none")] pub cpus: Option, + /// Memory limit for the service. #[serde(skip_serializing_if = "Option::is_none")] pub memory: Option, + /// Device limits for the service. #[serde(skip_serializing_if = "Option::is_none")] pub devices: Option>, } +/// Device configuration for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields)] pub struct Device { + /// Device driver to use. #[serde(skip_serializing_if = "Option::is_none")] pub driver: Option, + /// Number of devices to allocate. #[serde(skip_serializing_if = "Option::is_none")] pub count: Option, + /// List of device IDs to use. #[serde(skip_serializing_if = "Option::is_none")] pub device_ids: Option>, + /// Device capabilities to enable. #[serde(skip_serializing_if = "Option::is_none")] pub capabilities: Option>, + /// Driver-specific options. #[serde(skip_serializing_if = "Option::is_none")] #[cfg(feature = "indexmap")] pub options: Option>, + /// Driver-specific options. #[cfg(not(feature = "indexmap"))] pub options: Option>, } +/// Placement constraints and preferences for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] #[serde(deny_unknown_fields)] pub struct Placement { + /// Placement constraints for the service. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub constraints: Vec, + /// Placement preferences for the service. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub preferences: Vec, } +/// Placement preference for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct Preferences { + /// Spread tasks across the given value. pub spread: String, } +/// Resource constraints for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] #[serde(deny_unknown_fields)] pub struct Resources { + /// Hard resource limits for the service. pub limits: Option, + /// Resource reservations for the service. pub reservations: Option, } +/// Restart policy for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] #[serde(deny_unknown_fields)] pub struct RestartPolicy { + /// Condition for restarting the service (none, on-failure, any). #[serde(skip_serializing_if = "Option::is_none")] pub condition: Option, + /// Delay between restart attempts. #[serde(skip_serializing_if = "Option::is_none")] pub delay: Option, + /// Maximum number of restart attempts. #[serde(skip_serializing_if = "Option::is_none")] pub max_attempts: Option, + /// Time window to evaluate restart attempts. #[serde(skip_serializing_if = "Option::is_none")] pub window: Option, } +/// Configuration for how a service should be updated. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] #[serde(deny_unknown_fields)] pub struct UpdateConfig { + /// Number of containers to update at a time. #[serde(skip_serializing_if = "Option::is_none")] pub parallelism: Option, + /// Delay between updating groups of containers. #[serde(skip_serializing_if = "Option::is_none")] pub delay: Option, + /// Action to take if an update fails (pause, continue, rollback). #[serde(skip_serializing_if = "Option::is_none")] pub failure_action: Option, + /// Duration to monitor updated tasks for failures. #[serde(skip_serializing_if = "Option::is_none")] pub monitor: Option, + /// Failure rate to tolerate during an update. #[serde(skip_serializing_if = "Option::is_none")] pub max_failure_ratio: Option, } +/// Image pull policy for a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(rename_all = "lowercase")] pub enum PullPolicy { + /// Always pull the image. Always, + /// Never pull the image. Never, + /// Pull the image if it doesn't exist locally. #[serde(alias = "if_not_present")] Missing, + /// Build the image from source. Build, } +/// Command to run in the container. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum Command { + /// Simple string command. Simple(String), + /// Command with arguments as a list. Args(Vec), } +/// Entrypoint for the container. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum Entrypoint { + /// Simple string entrypoint. Simple(String), + /// Entrypoint as a list of strings. List(Vec), } +/// Group to add the user to. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum Group { + /// Group name. Named(String), + /// Group ID. Gid(u32), } diff --git a/src/volume.rs b/src/volume.rs index 35af965..eafcce5 100644 --- a/src/volume.rs +++ b/src/volume.rs @@ -8,9 +8,13 @@ use std::collections::HashMap; use crate::{Labels, MapOrEmpty, SingleValue}; +/// Container for volume definitions in a Compose file. +/// Maps volume names to their configuration settings. #[cfg(feature = "indexmap")] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct TopLevelVolumes(pub IndexMap>); +/// Container for volume definitions in a Compose file. +/// Maps volume names to their configuration settings. #[cfg(not(feature = "indexmap"))] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct TopLevelVolumes(pub HashMap>); @@ -21,75 +25,105 @@ impl TopLevelVolumes { } } +/// Configuration for a volume in a Compose file. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ComposeVolume { + /// Volume driver to use for this volume. #[serde(skip_serializing_if = "Option::is_none")] pub driver: Option, + /// Driver-specific options for this volume. #[cfg(feature = "indexmap")] #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub driver_opts: IndexMap>, + /// Driver-specific options for this volume. #[cfg(not(feature = "indexmap"))] #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub driver_opts: HashMap>, + /// Specifies that this volume is externally created. #[serde(skip_serializing_if = "Option::is_none")] pub external: Option, + /// Metadata labels for the volume. #[serde(default, skip_serializing_if = "Labels::is_empty")] pub labels: Labels, + /// Custom name for the volume. #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, } +/// Represents an external volume configuration in a Compose file. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum ExternalVolume { + /// Simple boolean flag for external volumes. Bool(bool), + /// Named external volume with a specific name. Name { name: String }, } +/// Represents a volume configuration in a service. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(untagged)] pub enum Volumes { + /// Simple string volume specification (e.g., "./host:/container"). Simple(String), + /// Advanced volume configuration with detailed settings. Advanced(AdvancedVolumes), } +/// Advanced volume configuration with detailed settings. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(deny_unknown_fields)] pub struct AdvancedVolumes { + /// Source path or volume name. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, + /// Mount path inside the container. pub target: String, + /// Mount type (bind, volume, tmpfs, etc.). #[serde(rename = "type")] pub _type: String, + /// Whether the volume is read-only. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub read_only: bool, + /// Bind mount specific options. #[serde(default, skip_serializing_if = "Option::is_none")] pub bind: Option, + /// Named volume specific options. #[serde(default, skip_serializing_if = "Option::is_none")] pub volume: Option, + /// Tmpfs specific options. #[serde(default, skip_serializing_if = "Option::is_none")] pub tmpfs: Option, } +/// Configuration options for bind mounts. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] #[serde(deny_unknown_fields)] pub struct Bind { + /// Propagation mode for the bind mount. pub propagation: Option, + /// Whether to create the host path if it doesn't exist. pub create_host_path: Option, + /// SELinux context for the bind mount. pub selinux: Option, } +/// Configuration options for named volumes. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] #[serde(deny_unknown_fields)] pub struct Volume { + /// Disable copying data from the container when a volume is created. #[serde(skip_serializing_if = "Option::is_none")] pub nocopy: Option, + /// Subpath within the volume to mount. #[serde(skip_serializing_if = "Option::is_none")] pub subpath: Option, } +/// Configuration options for tmpfs mounts. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] #[serde(deny_unknown_fields)] pub struct TmpfsSettings { + /// Size of the tmpfs mount in bytes. pub size: u64, } From 34e41b037567e0616ed81db7d77c9f4a1d24e148 Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Fri, 6 Jun 2025 16:43:25 +0200 Subject: [PATCH 6/6] refactor: enhance documentation for Compose file structures and service configurations --- src/network.rs | 10 +++++++--- src/secret.rs | 4 +++- src/volume.rs | 16 +++++++++++----- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/network.rs b/src/network.rs index a5324c4..632e5f0 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,5 +1,6 @@ // Network related structures extracted from lib.rs +use derive_builder::*; #[cfg(feature = "indexmap")] use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -51,7 +52,8 @@ pub struct ComposeNetworkSettingDetails { pub struct ExternalNetworkSettingBool(bool); /// Configuration settings for a network in a Compose file. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[builder(setter(into), default)] #[serde(deny_unknown_fields)] pub struct NetworkSettings { /// Whether the network can be attached to by external containers. @@ -89,7 +91,8 @@ pub struct NetworkSettings { } /// IP Address Management configuration for a network. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[builder(setter(into), default)] #[serde(deny_unknown_fields)] pub struct Ipam { /// IPAM driver to use. @@ -101,7 +104,8 @@ pub struct Ipam { } /// Configuration block for IPAM settings. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[builder(setter(into))] #[serde(deny_unknown_fields)] pub struct IpamConfig { /// Subnet in CIDR format. diff --git a/src/secret.rs b/src/secret.rs index 37b70da..5130d3e 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -1,5 +1,6 @@ // Secret related structures extracted from lib.rs +use derive_builder::*; #[cfg(feature = "indexmap")] use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -68,7 +69,8 @@ impl Secrets { } /// Advanced secret configuration with detailed settings. -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive(Builder, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[builder(setter(into), default)] #[serde(deny_unknown_fields)] pub struct AdvancedSecrets { /// Name of the secret in the Compose file. diff --git a/src/volume.rs b/src/volume.rs index eafcce5..25a4d3f 100644 --- a/src/volume.rs +++ b/src/volume.rs @@ -1,5 +1,6 @@ // Volume related structures extracted from lib.rs +use derive_builder::*; #[cfg(feature = "indexmap")] use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -26,7 +27,8 @@ impl TopLevelVolumes { } /// Configuration for a volume in a Compose file. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[builder(setter(into), default)] pub struct ComposeVolume { /// Volume driver to use for this volume. #[serde(skip_serializing_if = "Option::is_none")] @@ -71,7 +73,8 @@ pub enum Volumes { } /// Advanced volume configuration with detailed settings. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[builder(setter(into))] #[serde(deny_unknown_fields)] pub struct AdvancedVolumes { /// Source path or volume name. @@ -97,7 +100,8 @@ pub struct AdvancedVolumes { } /// Configuration options for bind mounts. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[builder(setter(into), default)] #[serde(deny_unknown_fields)] pub struct Bind { /// Propagation mode for the bind mount. @@ -109,7 +113,8 @@ pub struct Bind { } /// Configuration options for named volumes. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[builder(setter(into), default)] #[serde(deny_unknown_fields)] pub struct Volume { /// Disable copying data from the container when a volume is created. @@ -121,7 +126,8 @@ pub struct Volume { } /// Configuration options for tmpfs mounts. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[derive(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[builder(setter(into), default)] #[serde(deny_unknown_fields)] pub struct TmpfsSettings { /// Size of the tmpfs mount in bytes.