Skip to main content

isoloom_plan/configure/
daemons.rs

1//! The daemon table: for each daemon, per family, its unit, the package that ships it, and the
2//! foreground form a container runs it in. A `service: <unit>` step without `run` takes its
3//! container form from here, and a recipe that installs a daemon's package without a `service`
4//! step for it gets a warning (a daemon runs only where a step says so).
5//!
6//! The catalogue fills the table: each entry's `daemons:`, and `catalogue/daemons.yml` for the
7//! daemons no entry owns.
8
9use isoloom_catalogue::{Catalogue, LogStream};
10use isoloom_spec::syntax::{AbsPath, Argv, PackageName, UnitName, UserName};
11
12use super::family::Family;
13
14/// A daemon as a container runs it.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Daemon {
17    /// Its unit (what a VM enables).
18    pub unit: UnitName,
19    /// Its foreground form: what the container's runner `exec`s.
20    pub foreground: Argv,
21    /// The user it drops to (root when `None`).
22    pub user: Option<UserName>,
23    /// Its log files, linked to the container's output.
24    pub logs: Vec<LogLink>,
25}
26
27/// A daemon's log file, linked to one of the container's streams.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct LogLink {
30    /// The log file.
31    pub path: AbsPath,
32    /// The stream it is linked to.
33    pub stream: Stream,
34}
35
36/// A container's output stream.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum Stream {
40    /// Standard output.
41    Stdout,
42    /// Standard error.
43    Stderr,
44}
45
46/// A daemon on one family: the package that ships it, and how it runs.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct FamilyDaemon {
49    /// The family.
50    pub family: Family,
51    /// The package that ships it, when one does.
52    pub package: Option<PackageName>,
53    /// How it runs.
54    pub daemon: Daemon,
55}
56
57/// One daemon of the table: its key (what a step names) and its form on each family it has.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DaemonEntry {
60    /// What a `service` step names it by (`ssh`), besides each family's unit (`sshd`).
61    pub key: UnitName,
62    /// Its form on each family.
63    pub forms: Vec<FamilyDaemon>,
64}
65
66/// The daemons Isoloom knows the foreground form of.
67#[derive(Debug, Clone, PartialEq, Eq, Default)]
68pub struct DaemonTable {
69    entries: Vec<DaemonEntry>,
70}
71
72impl DaemonTable {
73    /// A table of `entries`.
74    #[must_use]
75    pub fn new(entries: Vec<DaemonEntry>) -> DaemonTable {
76        DaemonTable { entries }
77    }
78
79    /// The daemons of the catalogue built into Isoloom (empty only if it doesn't load, which
80    /// its own tests rule out).
81    #[must_use]
82    pub fn common() -> DaemonTable {
83        Catalogue::embedded().map(DaemonTable::from).unwrap_or_default()
84    }
85
86    /// The entries.
87    #[must_use]
88    pub fn entries(&self) -> &[DaemonEntry] {
89        &self.entries
90    }
91
92    /// The daemon a step names `name` on `family`: by the table's key, else by the family's unit.
93    #[must_use]
94    pub fn find(&self, name: &UnitName, family: Family) -> Option<(&DaemonEntry, &FamilyDaemon)> {
95        self.entries.iter().find_map(|e| {
96            let form = e.forms.iter().find(|f| f.family == family)?;
97            (e.key == *name || form.daemon.unit == *name).then_some((e, form))
98        })
99    }
100
101    /// The daemon package `package` ships on `family`, when it ships one.
102    #[must_use]
103    pub fn shipped_by(&self, package: &PackageName, family: Family) -> Option<(&DaemonEntry, &FamilyDaemon)> {
104        self.entries.iter().find_map(|e| {
105            let form = e.forms.iter().find(|f| f.family == family && f.package.as_ref() == Some(package))?;
106            Some((e, form))
107        })
108    }
109}
110
111impl From<&Catalogue> for DaemonTable {
112    /// The catalogue's daemon table: each daemon on each family its unit exists on.
113    fn from(catalogue: &Catalogue) -> DaemonTable {
114        let entries = catalogue
115            .daemons()
116            .map(|(key, d)| DaemonEntry {
117                key: key.clone(),
118                forms: d
119                    .unit
120                    .families()
121                    .into_iter()
122                    .filter_map(|family| {
123                        let daemon = Daemon {
124                            unit: d.unit.on(family)?.clone(),
125                            foreground: d.foreground.on(family)?.clone(),
126                            user: d.user.clone(),
127                            logs: d
128                                .logs
129                                .iter()
130                                .map(|(path, stream)| LogLink {
131                                    path: path.clone(),
132                                    stream: (*stream).into(),
133                                })
134                                .collect(),
135                        };
136                        let package = d.package.as_ref().and_then(|p| p.on(family)).cloned();
137                        Some(FamilyDaemon { family, package, daemon })
138                    })
139                    .collect(),
140            })
141            .collect();
142        DaemonTable::new(entries)
143    }
144}
145
146impl From<LogStream> for Stream {
147    fn from(s: LogStream) -> Stream {
148        match s {
149            LogStream::Stderr => Stream::Stderr,
150            _ => Stream::Stdout,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn unit(s: &str) -> UnitName {
160        s.parse().unwrap()
161    }
162
163    #[test]
164    fn the_catalogue_fills_the_table() {
165        let table = DaemonTable::common();
166        let keys: Vec<&str> = table.entries().iter().map(|e| e.key.as_str()).collect();
167        for key in ["nginx", "apache", "ssh", "cron", "rsyslog"] {
168            assert!(keys.contains(&key), "{key}: {keys:?}");
169        }
170    }
171
172    #[test]
173    fn a_step_finds_a_daemon_by_key_or_by_the_unit_its_family_names() {
174        let table = DaemonTable::common();
175        let (_, debian) = table.find(&unit("ssh"), Family::Debian).unwrap();
176        assert_eq!(debian.daemon.unit.as_str(), "ssh");
177        let (entry, rocky) = table.find(&unit("sshd"), Family::RedHat).unwrap();
178        assert_eq!((entry.key.as_str(), rocky.daemon.foreground.program()), ("ssh", "/usr/sbin/sshd"));
179        assert!(table.find(&unit("cron"), Family::Alpine).is_none(), "no cron form on Alpine");
180        assert!(table.find(&unit("myd"), Family::Debian).is_none());
181    }
182
183    #[test]
184    fn a_package_names_the_daemon_it_ships() {
185        let table = DaemonTable::common();
186        let (entry, form) = table.shipped_by(&"httpd".parse().unwrap(), Family::RedHat).unwrap();
187        assert_eq!((entry.key.as_str(), form.daemon.unit.as_str()), ("apache", "httpd"));
188        assert!(table.shipped_by(&"httpd".parse().unwrap(), Family::Debian).is_none());
189        let (_, nginx) = table.shipped_by(&"nginx".parse().unwrap(), Family::Alpine).unwrap();
190        assert_eq!(nginx.daemon.logs.len(), 2);
191    }
192}