use std::fmt;
use std::ops::Not;
use indexmap::IndexMap;
use serde::{Serialize, Serializer};
use super::{Attachment, ConfigMount, Mount, Reservations, ServiceDevice, ServiceSecret};
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct Service {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) build: Option<Build>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) platform: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) profiles: Vec<String>,
#[serde(skip_serializing_if = "Not::not")]
pub(crate) privileged: bool,
#[serde(skip_serializing_if = "Not::not")]
pub(crate) read_only: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) user: Option<String>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) sysctls: IndexMap<String, String>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) ulimits: IndexMap<String, Ulimit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) shm_size: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) tmpfs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) dns: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) dns_opt: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) dns_search: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) domainname: Option<String>,
#[serde(skip_serializing_if = "Not::not")]
pub(crate) init: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) entrypoint: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) command: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) working_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) hostname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) network_mode: Option<NetworkMode>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) networks: IndexMap<String, Option<Attachment>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) cap_add: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) devices: Vec<ServiceDevice>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) extra_hosts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) environment: Option<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) ports: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) volumes: Vec<Mount>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) secrets: Vec<ServiceSecret>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) configs: Vec<ConfigMount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) healthcheck: Option<Healthcheck>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) depends_on: IndexMap<String, Dependency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) deploy: Option<Deploy>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) labels: IndexMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) restart: Option<Restart>,
}
impl Service {
pub(crate) fn after(&mut self, service: impl Into<String>, condition: Condition) {
self.depends_on.insert(service.into(), Dependency { condition });
}
pub(crate) fn runs_once(&self) -> bool {
self.restart == Some(Restart::No)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct Build {
pub(crate) context: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) dockerfile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) target: Option<String>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) args: IndexMap<String, String>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub(crate) labels: IndexMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(untagged)]
pub(crate) enum Ulimit {
Both(u64),
Range {
soft: u64,
hard: u64,
},
}
impl Ulimit {
pub(crate) fn new(soft: u64, hard: u64) -> Ulimit {
if soft == hard { Ulimit::Both(soft) } else { Ulimit::Range { soft, hard } }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Seconds(pub(crate) u32);
impl fmt::Display for Seconds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}s", self.0)
}
}
impl Serialize for Seconds {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Healthcheck {
pub(crate) test: Vec<String>,
pub(crate) interval: Seconds,
pub(crate) timeout: Seconds,
pub(crate) retries: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) start_period: Option<Seconds>,
}
impl Healthcheck {
pub(crate) fn shell(test: impl Into<String>, every: Seconds, retries: u32) -> Healthcheck {
Healthcheck {
test: vec!["CMD-SHELL".into(), test.into()],
interval: every,
timeout: every,
retries,
start_period: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub(crate) struct Dependency {
pub(crate) condition: Condition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub(crate) enum Condition {
#[serde(rename = "service_started")]
Started,
#[serde(rename = "service_healthy")]
Healthy,
#[serde(rename = "service_completed_successfully")]
CompletedSuccessfully,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Restart {
No,
UnlessStopped,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NetworkMode {
None,
Service(String),
}
impl fmt::Display for NetworkMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NetworkMode::None => f.write_str("none"),
NetworkMode::Service(name) => write!(f, "service:{name}"),
}
}
}
impl Serialize for NetworkMode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct Deploy {
pub(crate) resources: Resources,
}
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct Resources {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) limits: Option<Limits>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) reservations: Option<Reservations>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Limits {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) cpus: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) memory: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn yaml(value: &impl Serialize) -> String {
serde_yaml_ng::to_string(value).expect("serializes")
}
#[test]
fn a_job_never_restarts() {
let job = Service {
restart: Some(Restart::No),
..Service::default()
};
assert!(job.runs_once());
assert_eq!(yaml(&job), "restart: no\n");
assert_eq!(yaml(&Restart::UnlessStopped), "unless-stopped\n");
}
#[test]
fn a_dependency_names_its_condition_as_compose_does() {
let mut svc = Service::default();
svc.after("db", Condition::Healthy);
svc.after("seed", Condition::CompletedSuccessfully);
assert_eq!(
yaml(&svc),
"depends_on:\n db:\n condition: service_healthy\n seed:\n condition: service_completed_successfully\n"
);
}
#[test]
fn a_network_mode_is_none_or_another_service() {
assert_eq!(yaml(&NetworkMode::None), "none\n");
assert_eq!(yaml(&NetworkMode::Service("web".into())), "service:web\n");
}
#[test]
fn a_shell_healthcheck_times_out_at_its_interval() {
let h = Healthcheck::shell("true", Seconds(3), 60);
assert_eq!(yaml(&h), "test:\n- CMD-SHELL\n- 'true'\ninterval: 3s\ntimeout: 3s\nretries: 60\n");
}
#[test]
fn a_ulimit_is_one_number_when_soft_and_hard_agree() {
assert_eq!(Ulimit::new(5, 5), Ulimit::Both(5));
assert_eq!(Ulimit::new(5, 9), Ulimit::Range { soft: 5, hard: 9 });
}
}