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/.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. diff --git a/src/lib.rs b/src/lib.rs index 13f6e62..845ae6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ -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,39 +11,64 @@ use std::str::FromStr; use serde_yaml::Value; +mod network; +mod secret; +mod service; +mod volume; + +pub use network::*; +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, @@ -59,335 +83,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 +122,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 +144,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 { diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..632e5f0 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,116 @@ +// Network related structures extracted from lib.rs + +use derive_builder::*; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +#[cfg(not(feature = "indexmap"))] +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>); + +impl ComposeNetworks { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// 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(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. + #[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(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[builder(setter(into), default)] +#[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(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[builder(setter(into))] +#[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 new file mode 100644 index 0000000..5130d3e --- /dev/null +++ b/src/secret.rs @@ -0,0 +1,90 @@ +// Secret related structures extracted from lib.rs + +use derive_builder::*; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +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( + #[serde(with = "serde_yaml::with::singleton_map_recursive")] + 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( + #[serde(with = "serde_yaml::with::singleton_map_recursive")] + 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), +} + +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(), + } + } +} + +/// Advanced secret configuration with detailed settings. +#[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. + 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 new file mode 100644 index 0000000..ad95e9a --- /dev/null +++ b/src/service.rs @@ -0,0 +1,813 @@ +// 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 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")] + #[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, + /// 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")] + #[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, + /// 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, +} + +#[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)] +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(), + } + } +} + +/// 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>), +} + +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(), + } + } +} + +/// 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, +} + +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 new file mode 100644 index 0000000..25a4d3f --- /dev/null +++ b/src/volume.rs @@ -0,0 +1,135 @@ +// Volume related structures extracted from lib.rs + +use derive_builder::*; +#[cfg(feature = "indexmap")] +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +#[cfg(not(feature = "indexmap"))] +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>); + +impl TopLevelVolumes { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Configuration for a volume in a Compose file. +#[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")] + 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(Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[builder(setter(into))] +#[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(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. + 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(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. + #[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(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. + pub size: u64, +}