1use serde::{Deserialize, Serialize};
4use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub enum RestartPolicy {
8 Never,
10 OnFailure,
12 Always,
14}
15
16impl Default for RestartPolicy {
17 fn default() -> Self {
18 Self::OnFailure
19 }
20}
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Service {
25 pub name: String,
26 #[serde(default)]
30 pub exec: String,
31 #[serde(default)]
36 pub body: Option<String>,
37 #[serde(default)]
39 pub restart: RestartPolicy,
40 #[serde(default)]
42 pub env: Vec<(String, String)>,
43 #[serde(default)]
45 pub workdir: Option<String>,
46 #[serde(default = "default_true")]
48 pub enable: bool,
49}
50
51impl Service {
52 pub fn resolved_exec(&self) -> String {
56 match &self.body {
57 Some(form) => {
58 let escaped = form.replace('\'', "'\\''");
61 format!("/bin/tatara-init --eval '{escaped}'")
62 }
63 None => self.exec.clone(),
64 }
65 }
66}
67
68fn default_true() -> bool {
69 true
70}
71
72#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
77#[tatara(keyword = "defmount")]
78pub struct MountSpec {
79 pub source: String,
81 pub target: String,
83 pub fstype: String,
85 #[serde(default)]
87 pub options: Option<String>,
88}
89
90#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92#[tatara(keyword = "definit")]
93pub struct InitConfig {
94 #[serde(default = "default_name")]
96 pub name: String,
97 #[serde(default)]
99 pub services: Vec<Service>,
100 #[serde(default)]
103 pub mounts: Vec<MountSpec>,
104 #[serde(default = "default_true")]
106 pub reap_zombies: bool,
107 #[serde(default = "default_true")]
109 pub reload_on_sighup: bool,
110}
111
112fn default_name() -> String {
113 "tatara-init".into()
114}
115
116impl Default for InitConfig {
117 fn default() -> Self {
118 Self {
119 name: default_name(),
120 services: vec![],
121 mounts: vec![],
122 reap_zombies: true,
123 reload_on_sighup: true,
124 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use tatara_lisp::{domain::TataraDomain, read};
132
133 #[test]
134 fn empty_definit_parses() {
135 let forms = read(r#"(definit :name "plex-boot")"#).unwrap();
143 let c = InitConfig::compile_from_sexp(&forms[0]).unwrap();
144 assert_eq!(c.name, "plex-boot");
145 assert!(c.services.is_empty());
146 assert!(
147 c.reap_zombies,
148 "reap_zombies must default to true via `default_true`"
149 );
150 assert!(
151 c.reload_on_sighup,
152 "reload_on_sighup must default to true via `default_true`",
153 );
154 }
155
156 #[test]
157 fn services_round_trip_through_lisp() {
158 let forms = read(
159 r#"(definit
160 :name "plex-boot"
161 :services ((:name "sshd" :exec "/bin/sshd -D")
162 (:name "fumi" :exec "/bin/fumi" :enable #f)))"#,
163 )
164 .unwrap();
165 let c = InitConfig::compile_from_sexp(&forms[0]).unwrap();
166 assert_eq!(c.services.len(), 2);
167 assert_eq!(c.services[0].name, "sshd");
168 assert!(c.services[0].enable);
169 assert_eq!(c.services[1].name, "fumi");
170 assert!(!c.services[1].enable);
171 }
172
173 #[test]
174 fn restart_policy_defaults_to_on_failure() {
175 let svc = Service {
176 name: "x".into(),
177 exec: "/x".into(),
178 body: None,
179 restart: Default::default(),
180 env: vec![],
181 workdir: None,
182 enable: true,
183 };
184 assert!(matches!(svc.restart, RestartPolicy::OnFailure));
185 }
186
187 #[test]
188 fn resolved_exec_uses_body_when_present() {
189 let svc = Service {
190 name: "greet".into(),
191 exec: String::new(),
192 body: Some("(println 42)".into()),
193 restart: Default::default(),
194 env: vec![],
195 workdir: None,
196 enable: true,
197 };
198 assert_eq!(
199 svc.resolved_exec(),
200 "/bin/tatara-init --eval '(println 42)'"
201 );
202 }
203
204 #[test]
205 fn resolved_exec_falls_back_to_exec_when_body_absent() {
206 let svc = Service {
207 name: "x".into(),
208 exec: "/bin/x arg1 arg2".into(),
209 body: None,
210 restart: Default::default(),
211 env: vec![],
212 workdir: None,
213 enable: true,
214 };
215 assert_eq!(svc.resolved_exec(), "/bin/x arg1 arg2");
216 }
217
218 #[test]
219 fn resolved_exec_escapes_embedded_single_quotes() {
220 let svc = Service {
221 name: "quoted".into(),
222 exec: String::new(),
223 body: Some("(a 'b c)".into()),
224 restart: Default::default(),
225 env: vec![],
226 workdir: None,
227 enable: true,
228 };
229 assert!(svc.resolved_exec().contains("'\\''"));
232 }
233}