depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Compose options module.
//!
//! Options for running pipelines with Docker Compose, using the containerized
//! build as the main application service and additional services like databases
//! or caches (e.g. PostgreSQL, Redis).

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

use crate::entities::containered_opts::{ContainerExecutor, ContaineredOpts, PortBinding};
use crate::entities::info::ShortName;

/// Options for a single Compose service (e.g., `postgres`, `redis`).
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
pub struct ComposeServiceOpts {
  /// Docker image for this service (e.g. `postgres:16`, `redis:7-alpine`).
  pub image: String,

  /// Environment variables passed to the service container.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub environment: BTreeMap<String, String>,

  /// Port bindings for this service.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub ports: Vec<PortBinding>,

  /// Volumes for this service (e.g. `pgdata:/var/lib/postgresql/data`).
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub volumes: Vec<String>,

  /// Custom command override for the service.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub command: Option<String>,

  /// Health check command for this service.
  ///
  /// When specified, Docker Compose will use this to determine if the service
  /// is healthy before starting dependent services.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub healthcheck_cmd: Option<String>,

  /// Health check interval in seconds.
  #[serde(
    default = "default_healthcheck_interval",
    skip_serializing_if = "is_default_healthcheck_interval"
  )]
  pub healthcheck_interval_secs: u64,

  /// Health check retries.
  #[serde(
    default = "default_healthcheck_retries",
    skip_serializing_if = "is_default_healthcheck_retries"
  )]
  pub healthcheck_retries: u32,
}

fn default_healthcheck_interval() -> u64 {
  5
}

fn is_default_healthcheck_interval(v: &u64) -> bool {
  *v == 5
}

fn default_healthcheck_retries() -> u32 {
  3
}

fn is_default_healthcheck_retries(v: &u32) -> bool {
  *v == 3
}

impl std::fmt::Display for ComposeServiceOpts {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    let mut parts = vec![format!("image: {}", self.image)];
    if !self.environment.is_empty() {
      parts.push(format!("{} env vars", self.environment.len()));
    }
    if !self.ports.is_empty() {
      parts.push(format!("{} port(s)", self.ports.len()));
    }
    if !self.volumes.is_empty() {
      parts.push(format!("{} volume(s)", self.volumes.len()));
    }
    if self.healthcheck_cmd.is_some() {
      parts.push("healthcheck".to_string());
    }
    write!(f, "{}", parts.join(", "))
  }
}

/// Options for Docker Compose pipeline runs.
///
/// Compose runs use the containerized build as the main `app` service and
/// allow defining additional services (databases, caches, message brokers, etc.)
/// that are orchestrated together via `docker compose`.
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
pub struct ComposeOpts {
  /// Containerized options for the main application service.
  ///
  /// The main service is built using the same Dockerfile generation logic
  /// as `containered_opts`, and exposed as the `app` service in the
  /// Compose file.
  pub app: ContaineredOpts,

  /// Additional services map (e.g. `postgres`, `redis`, `rabbitmq`).
  ///
  /// Keys are service names used in the Compose file, values are their
  /// configuration options.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub services: BTreeMap<String, ComposeServiceOpts>,

  /// Variables that used in services' environment values.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub with: BTreeMap<String, ShortName>,

  /// Compose project name override.
  ///
  /// If not set, defaults to `{project_name}-{pipeline_title}`.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub project_name: Option<String>,

  /// Run compose in detached mode.
  #[serde(default, skip_serializing_if = "crate::utils::is_false")]
  pub detach: bool,

  /// Abort on container exit (pass `--abort-on-container-exit` to compose).
  ///
  /// When enabled, all services are stopped when any container exits.
  /// This is useful for CI pipelines where the app container runs once
  /// and exits.
  #[serde(default, skip_serializing_if = "crate::utils::is_false")]
  pub abort_on_container_exit: bool,

  /// Remove containers after run (pass `--rm` to compose run).
  #[serde(default = "crate::utils::yes", skip_serializing_if = "crate::utils::is_true")]
  pub remove_on_exit: bool,

  /// Container executor (runner).
  ///
  /// Overrides the executor from `app.executor` when specified.
  /// Otherwise, inherits the executor from the `app` containerized options.
  #[serde(default, skip_serializing_if = "ContainerExecutor::is_default")]
  pub executor: ContainerExecutor,
}

impl ComposeOpts {
  /// Returns the effective container executor.
  ///
  /// Uses `self.executor` if explicitly set, otherwise falls back to `self.app.executor`.
  pub fn effective_executor(&self) -> &ContainerExecutor {
    if self.executor.is_default() {
      &self.app.executor
    } else {
      &self.executor
    }
  }
}