Skip to main content

isoloom_plan/
process.rs

1//! What a machine runs and when it is healthy, resolved once for every target: its command,
2//! the directory it runs in, and its healthcheck with every pace in
3//! seconds (Kubernetes counts in seconds; Compose and the VM scripts are given the same).
4
5use isoloom_spec::model::{CommandLine, Healthcheck, Machine, seconds};
6
7/// The healthcheck's pace when the spec leaves it out: the port probe's own.
8pub const DEFAULT_INTERVAL: u32 = 5;
9/// How long one try may take, by default.
10pub const DEFAULT_TIMEOUT: u32 = 3;
11/// Failed tries in a row before the machine is unhealthy, by default (5 minutes at 5s).
12pub const DEFAULT_RETRIES: u32 = 60;
13/// The grace period after start, by default.
14pub const DEFAULT_START_PERIOD: u32 = 10;
15
16/// A machine's own process. Empty when the spec sets none of it: the image's or the VM's own.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct Process {
19    /// Its own command, instead of the image's.
20    pub command: Option<CommandLine>,
21    /// Where the command and the healthcheck run (`command.cwd`).
22    pub working_dir: Option<String>,
23    /// When it is healthy, beside its services' ports answering.
24    pub health: Option<Health>,
25}
26
27impl Process {
28    /// The process machine `m` declares.
29    pub fn of(m: &Machine) -> Process {
30        Process {
31            command: m.command.clone(),
32            working_dir: m.working_dir.clone(),
33            health: m.healthcheck.as_ref().and_then(Health::of),
34        }
35    }
36
37    /// Whether the spec sets any of it.
38    #[must_use]
39    pub fn is_empty(&self) -> bool {
40        self.command.is_none() && self.working_dir.is_none() && self.health.is_none()
41    }
42}
43
44/// A healthcheck, its pace in seconds.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Health {
47    /// What is tried.
48    pub test: HealthTest,
49    /// Seconds between two tries.
50    pub interval: u32,
51    /// Seconds one try may take.
52    pub timeout: u32,
53    /// Failed tries in a row before the machine is unhealthy.
54    pub retries: u32,
55    /// Seconds after start before a failed try counts.
56    pub start_period: u32,
57}
58
59/// What a healthcheck tries.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum HealthTest {
62    /// A command run inside the machine, healthy when it exits with 0.
63    Command(CommandLine),
64    /// A page on the machine's own port, healthy when it answers with a 2xx or 3xx status.
65    Http {
66        /// The port.
67        port: u16,
68        /// The page's path, from `/`.
69        path: String,
70    },
71}
72
73impl Health {
74    /// The plan of a validated healthcheck; `None` when it says nothing to try.
75    #[must_use]
76    pub fn of(h: &Healthcheck) -> Option<Health> {
77        let test = match (&h.test, &h.http) {
78            (Some(t), _) => HealthTest::Command(t.clone()),
79            (None, Some(http)) => HealthTest::Http {
80                port: http.port,
81                path: http.path.clone(),
82            },
83            (None, None) => return None,
84        };
85        let pace = |v: &Option<String>, default| v.as_deref().and_then(seconds).unwrap_or(default);
86        Some(Health {
87            test,
88            interval: pace(&h.interval, DEFAULT_INTERVAL),
89            timeout: pace(&h.timeout, DEFAULT_TIMEOUT),
90            retries: h.retries.unwrap_or(DEFAULT_RETRIES),
91            start_period: pace(&h.start_period, DEFAULT_START_PERIOD),
92        })
93    }
94
95    /// The test as one line of shell, run inside the machine: the command as written, or the
96    /// page fetched with `wget` (busybox has it, as Isoloom's probe does on the containers).
97    /// `wget`'s own failure on a 4xx or 5xx status is the unhealthy answer.
98    #[must_use]
99    pub fn shell_test(&self, wget: &str) -> String {
100        match &self.test {
101            HealthTest::Command(c) => c.shell_line(),
102            HealthTest::Http { port, path } => {
103                format!("{wget} -q -O /dev/null -T {} http://127.0.0.1:{port}{path}", self.timeout)
104            }
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use isoloom_spec::model::{CommandLine, Healthcheck, HttpCheck};
112
113    use super::{Health, HealthTest, Process};
114
115    #[test]
116    fn a_process_is_empty_until_one_of_its_parts_is_set() {
117        assert!(Process::default().is_empty());
118        let health = Health {
119            test: HealthTest::Command(CommandLine::Shell("true".into())),
120            interval: 1,
121            timeout: 1,
122            retries: 1,
123            start_period: 0,
124        };
125        let one_each = [
126            Process {
127                command: Some(CommandLine::Shell("app".into())),
128                ..Process::default()
129            },
130            Process {
131                working_dir: Some("/srv".into()),
132                ..Process::default()
133            },
134            Process {
135                health: Some(health),
136                ..Process::default()
137            },
138        ];
139        for p in one_each {
140            assert!(!p.is_empty(), "{p:?}");
141        }
142    }
143
144    #[test]
145    fn the_pace_defaults_to_the_port_probes() {
146        let h = Healthcheck {
147            test: None,
148            http: Some(HttpCheck {
149                port: 8080,
150                path: "/healthz".into(),
151            }),
152            interval: Some("1m".into()),
153            timeout: None,
154            retries: None,
155            start_period: Some("0s".into()),
156        };
157        let health = Health::of(&h).expect("a test");
158        assert_eq!((health.interval, health.timeout, health.retries, health.start_period), (60, 3, 60, 0));
159        assert_eq!(
160            health.test,
161            HealthTest::Http {
162                port: 8080,
163                path: "/healthz".into()
164            }
165        );
166        assert_eq!(health.shell_test("wget"), "wget -q -O /dev/null -T 3 http://127.0.0.1:8080/healthz");
167        let cmd = Health {
168            test: HealthTest::Command(CommandLine::Exec(vec!["pg_isready".into()])),
169            ..health
170        };
171        assert_eq!(cmd.shell_test("wget"), "'pg_isready'");
172    }
173}