isoloom_spec/model/
process.rs1use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::shell;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11#[serde(untagged)]
12pub enum CommandLine {
13 Shell(String),
16 Exec(Vec<String>),
18}
19
20impl CommandLine {
21 #[must_use]
23 pub fn argv(&self) -> Vec<String> {
24 match self {
25 CommandLine::Shell(text) => vec!["/bin/sh".into(), "-c".into(), text.clone()],
26 CommandLine::Exec(args) => args.clone(),
27 }
28 }
29
30 #[must_use]
32 pub fn shell_line(&self) -> String {
33 match self {
34 CommandLine::Shell(text) => text.clone(),
35 CommandLine::Exec(args) => args.iter().map(|a| shell::quote(a)).collect::<Vec<_>>().join(" "),
36 }
37 }
38
39 #[must_use]
41 pub fn is_empty(&self) -> bool {
42 match self {
43 CommandLine::Shell(text) => text.trim().is_empty(),
44 CommandLine::Exec(args) => args.first().is_none_or(|a| a.trim().is_empty()),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
53#[serde(deny_unknown_fields)]
54pub struct Healthcheck {
55 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub test: Option<CommandLine>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub http: Option<HttpCheck>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub interval: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub timeout: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub retries: Option<u32>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub start_period: Option<String>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
79#[serde(deny_unknown_fields)]
80pub struct HttpCheck {
81 pub port: u16,
83 #[serde(default = "root")]
85 pub path: String,
86}
87
88fn root() -> String {
89 "/".into()
90}
91
92#[must_use]
95pub fn seconds(duration: &str) -> Option<u32> {
96 let mut total: u32 = 0;
97 let mut number = String::new();
98 for c in duration.trim().chars() {
99 if c.is_ascii_digit() {
100 number.push(c);
101 continue;
102 }
103 let unit = match c {
104 'h' => 3600,
105 'm' => 60,
106 's' => 1,
107 _ => return None,
108 };
109 let n: u32 = number.parse().ok()?;
110 total = total.checked_add(n.checked_mul(unit)?)?;
111 number.clear();
112 }
113 (number.is_empty() && !duration.trim().is_empty()).then_some(total)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::{CommandLine, seconds};
119
120 #[test]
121 fn durations_read_in_seconds() {
122 assert_eq!(seconds("5s"), Some(5));
123 assert_eq!(seconds("1m30s"), Some(90));
124 assert_eq!(seconds("2h"), Some(7200));
125 assert_eq!(seconds("0s"), Some(0));
126 for bad in ["", "5", "s", "5x", "1.5s", "-1s"] {
127 assert_eq!(seconds(bad), None, "{bad}");
128 }
129 }
130
131 #[test]
132 fn a_command_as_arguments_or_a_line() {
133 let text = CommandLine::Shell("echo $HOME && true".into());
134 assert_eq!(text.argv(), ["/bin/sh", "-c", "echo $HOME && true"]);
135 assert_eq!(text.shell_line(), "echo $HOME && true");
136 let args = CommandLine::Exec(vec!["nginx".into(), "-g".into(), "daemon off;".into()]);
137 assert_eq!(args.argv(), ["nginx", "-g", "daemon off;"]);
138 assert_eq!(args.shell_line(), "'nginx' '-g' 'daemon off;'");
139 assert!(CommandLine::Exec(Vec::new()).is_empty() && CommandLine::Shell(" ".into()).is_empty());
140 }
141}