use serde::{Deserialize, Serialize};
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RestartPolicy {
Never,
OnFailure,
Always,
}
impl Default for RestartPolicy {
fn default() -> Self {
Self::OnFailure
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Service {
pub name: String,
#[serde(default)]
pub exec: String,
#[serde(default)]
pub body: Option<String>,
#[serde(default)]
pub restart: RestartPolicy,
#[serde(default)]
pub env: Vec<(String, String)>,
#[serde(default)]
pub workdir: Option<String>,
#[serde(default = "default_true")]
pub enable: bool,
}
impl Service {
pub fn resolved_exec(&self) -> String {
match &self.body {
Some(form) => {
let escaped = form.replace('\'', "'\\''");
format!("/bin/tatara-init --eval '{escaped}'")
}
None => self.exec.clone(),
}
}
}
fn default_true() -> bool {
true
}
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[tatara(keyword = "defmount")]
pub struct MountSpec {
pub source: String,
pub target: String,
pub fstype: String,
#[serde(default)]
pub options: Option<String>,
}
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[tatara(keyword = "definit")]
pub struct InitConfig {
#[serde(default = "default_name")]
pub name: String,
#[serde(default)]
pub services: Vec<Service>,
#[serde(default)]
pub mounts: Vec<MountSpec>,
#[serde(default = "default_true")]
pub reap_zombies: bool,
#[serde(default = "default_true")]
pub reload_on_sighup: bool,
}
fn default_name() -> String {
"tatara-init".into()
}
impl Default for InitConfig {
fn default() -> Self {
Self {
name: default_name(),
services: vec![],
mounts: vec![],
reap_zombies: true,
reload_on_sighup: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp::{domain::TataraDomain, read};
#[test]
fn empty_definit_parses() {
let forms = read(r#"(definit :name "plex-boot")"#).unwrap();
let c = InitConfig::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(c.name, "plex-boot");
assert!(c.services.is_empty());
}
#[test]
fn services_round_trip_through_lisp() {
let forms = read(
r#"(definit
:name "plex-boot"
:services ((:name "sshd" :exec "/bin/sshd -D")
(:name "fumi" :exec "/bin/fumi" :enable #f)))"#,
)
.unwrap();
let c = InitConfig::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(c.services.len(), 2);
assert_eq!(c.services[0].name, "sshd");
assert!(c.services[0].enable);
assert_eq!(c.services[1].name, "fumi");
assert!(!c.services[1].enable);
}
#[test]
fn restart_policy_defaults_to_on_failure() {
let svc = Service {
name: "x".into(),
exec: "/x".into(),
body: None,
restart: Default::default(),
env: vec![],
workdir: None,
enable: true,
};
assert!(matches!(svc.restart, RestartPolicy::OnFailure));
}
#[test]
fn resolved_exec_uses_body_when_present() {
let svc = Service {
name: "greet".into(),
exec: String::new(),
body: Some("(println 42)".into()),
restart: Default::default(),
env: vec![],
workdir: None,
enable: true,
};
assert_eq!(
svc.resolved_exec(),
"/bin/tatara-init --eval '(println 42)'"
);
}
#[test]
fn resolved_exec_falls_back_to_exec_when_body_absent() {
let svc = Service {
name: "x".into(),
exec: "/bin/x arg1 arg2".into(),
body: None,
restart: Default::default(),
env: vec![],
workdir: None,
enable: true,
};
assert_eq!(svc.resolved_exec(), "/bin/x arg1 arg2");
}
#[test]
fn resolved_exec_escapes_embedded_single_quotes() {
let svc = Service {
name: "quoted".into(),
exec: String::new(),
body: Some("(a 'b c)".into()),
restart: Default::default(),
env: vec![],
workdir: None,
enable: true,
};
assert!(svc.resolved_exec().contains("'\\''"));
}
}