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();
140 let c = InitConfig::compile_from_sexp(&forms[0]).unwrap();
141 assert_eq!(c.name, "plex-boot");
142 assert!(c.services.is_empty());
143 }
144
145 #[test]
146 fn services_round_trip_through_lisp() {
147 let forms = read(
148 r#"(definit
149 :name "plex-boot"
150 :services ((:name "sshd" :exec "/bin/sshd -D")
151 (:name "fumi" :exec "/bin/fumi" :enable #f)))"#,
152 )
153 .unwrap();
154 let c = InitConfig::compile_from_sexp(&forms[0]).unwrap();
155 assert_eq!(c.services.len(), 2);
156 assert_eq!(c.services[0].name, "sshd");
157 assert!(c.services[0].enable);
158 assert_eq!(c.services[1].name, "fumi");
159 assert!(!c.services[1].enable);
160 }
161
162 #[test]
163 fn restart_policy_defaults_to_on_failure() {
164 let svc = Service {
165 name: "x".into(),
166 exec: "/x".into(),
167 body: None,
168 restart: Default::default(),
169 env: vec![],
170 workdir: None,
171 enable: true,
172 };
173 assert!(matches!(svc.restart, RestartPolicy::OnFailure));
174 }
175
176 #[test]
177 fn resolved_exec_uses_body_when_present() {
178 let svc = Service {
179 name: "greet".into(),
180 exec: String::new(),
181 body: Some("(println 42)".into()),
182 restart: Default::default(),
183 env: vec![],
184 workdir: None,
185 enable: true,
186 };
187 assert_eq!(
188 svc.resolved_exec(),
189 "/bin/tatara-init --eval '(println 42)'"
190 );
191 }
192
193 #[test]
194 fn resolved_exec_falls_back_to_exec_when_body_absent() {
195 let svc = Service {
196 name: "x".into(),
197 exec: "/bin/x arg1 arg2".into(),
198 body: None,
199 restart: Default::default(),
200 env: vec![],
201 workdir: None,
202 enable: true,
203 };
204 assert_eq!(svc.resolved_exec(), "/bin/x arg1 arg2");
205 }
206
207 #[test]
208 fn resolved_exec_escapes_embedded_single_quotes() {
209 let svc = Service {
210 name: "quoted".into(),
211 exec: String::new(),
212 body: Some("(a 'b c)".into()),
213 restart: Default::default(),
214 env: vec![],
215 workdir: None,
216 enable: true,
217 };
218 assert!(svc.resolved_exec().contains("'\\''"));
221 }
222}