use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Healthcheck {
test: Vec<String>,
interval: Option<Duration>,
timeout: Option<Duration>,
retries: Option<u64>,
start_period: Option<Duration>,
start_interval: Option<Duration>,
}
impl Healthcheck {
pub fn none() -> Self {
Self {
test: vec!["NONE".into()],
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
}
}
pub fn empty() -> Self {
Self {
test: Vec::new(),
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
}
}
pub fn cmd_shell(cmd: impl Into<String>) -> Self {
Self {
test: vec!["CMD-SHELL".into(), cmd.into()],
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
}
}
pub fn cmd<I, S>(cmd: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut test = vec!["CMD".into()];
test.extend(cmd.into_iter().map(Into::into));
Self {
test,
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
}
}
pub fn with_interval(mut self, interval: Duration) -> Self {
self.interval = Some(interval);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_retries(mut self, retries: u64) -> Self {
self.retries = Some(retries);
self
}
pub fn with_start_period(mut self, start_period: Duration) -> Self {
self.start_period = Some(start_period);
self
}
pub fn with_start_interval(mut self, start_interval: Duration) -> Self {
self.start_interval = Some(start_interval);
self
}
pub fn test(&self) -> &[String] {
&self.test
}
pub fn interval(&self) -> Option<Duration> {
self.interval
}
pub fn timeout(&self) -> Option<Duration> {
self.timeout
}
pub fn retries(&self) -> Option<u64> {
self.retries
}
pub fn start_period(&self) -> Option<Duration> {
self.start_period
}
pub fn start_interval(&self) -> Option<Duration> {
self.start_interval
}
#[cfg(target_os = "linux")]
pub(crate) fn to_docker_json(&self) -> Option<String> {
use crate::core::client::docker_client::json_array;
let all_none = self.interval.is_none()
&& self.timeout.is_none()
&& self.retries.is_none()
&& self.start_period.is_none()
&& self.start_interval.is_none();
if self.test.is_empty() && all_none {
return None;
}
let mut json = String::from("{\"Test\":");
json.push_str(&json_array(&self.test));
if let Some(interval) = self.interval {
json.push_str(",\"Interval\":");
json.push_str(&duration_to_nanos_i64(interval).to_string());
}
if let Some(timeout) = self.timeout {
json.push_str(",\"Timeout\":");
json.push_str(&duration_to_nanos_i64(timeout).to_string());
}
if let Some(retries) = self.retries {
json.push_str(",\"Retries\":");
json.push_str(&retries.to_string());
}
if let Some(start_period) = self.start_period {
json.push_str(",\"StartPeriod\":");
json.push_str(&duration_to_nanos_i64(start_period).to_string());
}
if let Some(start_interval) = self.start_interval {
json.push_str(",\"StartInterval\":");
json.push_str(&duration_to_nanos_i64(start_interval).to_string());
}
json.push('}');
Some(json)
}
}
#[cfg(target_os = "linux")]
fn duration_to_nanos_i64(d: Duration) -> i64 {
let nanos = d.as_nanos();
nanos.min(i64::MAX as u128) as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_set_expected_test_arrays() {
assert_eq!(Healthcheck::none().test(), &["NONE".to_string()]);
assert!(Healthcheck::empty().test().is_empty());
assert_eq!(
Healthcheck::cmd_shell("true").test(),
&["CMD-SHELL".to_string(), "true".to_string()]
);
assert_eq!(
Healthcheck::cmd(["echo", "ok"]).test(),
&["CMD".to_string(), "echo".to_string(), "ok".to_string()]
);
}
#[test]
fn builders_are_readable_via_accessors() {
let hc = Healthcheck::empty()
.with_interval(Duration::from_secs(1))
.with_timeout(Duration::from_secs(2))
.with_retries(3)
.with_start_period(Duration::from_secs(4))
.with_start_interval(Duration::from_secs(5));
assert_eq!(hc.interval(), Some(Duration::from_secs(1)));
assert_eq!(hc.timeout(), Some(Duration::from_secs(2)));
assert_eq!(hc.retries(), Some(3));
assert_eq!(hc.start_period(), Some(Duration::from_secs(4)));
assert_eq!(hc.start_interval(), Some(Duration::from_secs(5)));
}
}
#[cfg(all(test, target_os = "linux"))]
mod linux_tests {
use super::*;
#[test]
fn to_docker_json_emits_test_variants() {
let shell = Healthcheck::cmd_shell("true").to_docker_json().unwrap();
assert_eq!(shell, r#"{"Test":["CMD-SHELL","true"]}"#);
let cmd = Healthcheck::cmd(["echo", "ok"]).to_docker_json().unwrap();
assert_eq!(cmd, r#"{"Test":["CMD","echo","ok"]}"#);
let none = Healthcheck::none().to_docker_json().unwrap();
assert_eq!(none, r#"{"Test":["NONE"]}"#);
let empty_with_interval = Healthcheck::empty()
.with_interval(Duration::from_secs(5))
.to_docker_json()
.unwrap();
assert_eq!(empty_with_interval, r#"{"Test":[],"Interval":5000000000}"#);
}
#[test]
fn to_docker_json_duration_and_omission() {
let hc = Healthcheck::cmd_shell("true")
.with_interval(Duration::from_secs(30))
.with_timeout(Duration::ZERO);
let json = hc.to_docker_json().unwrap();
assert!(
json.contains(r#""Interval":30000000000"#),
"30 秒が nanosecond で出ること: {json}"
);
assert!(
json.contains(r#""Timeout":0"#),
"Some(ZERO) は 0 を出すこと: {json}"
);
assert!(
!json.contains("Retries"),
"None の Retries はキー省略であること: {json}"
);
assert!(
!json.contains("StartPeriod"),
"None の StartPeriod はキー省略であること: {json}"
);
}
#[test]
fn to_docker_json_escapes_test_elements() {
let hc = Healthcheck::cmd_shell("a\"b\\c\nd");
let json = hc.to_docker_json().unwrap();
assert_eq!(
json, r#"{"Test":["CMD-SHELL","a\"b\\c\nd"]}"#,
"引用符・バックスラッシュ・改行が escape されること"
);
let multibyte = Healthcheck::cmd_shell("日本語").to_docker_json().unwrap();
assert_eq!(
multibyte, r#"{"Test":["CMD-SHELL","日本語"]}"#,
"マルチバイト文字はそのまま出ること"
);
}
#[test]
fn to_docker_json_returns_none_for_empty_default() {
assert!(Healthcheck::empty().to_docker_json().is_none());
}
}