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