Skip to main content

isoloom_plan/configure/
unstarted.rs

1//! The daemons a recipe installs and no step starts. A package that ships a daemon starts it on
2//! Debian's VMs only (its maintainer scripts do), never in a container or on the other families'
3//! VMs, so the same steps would behave differently by form and by family. A `service` step for
4//! the daemon (enabled, or `enabled: false`) says where it runs; until one does, the author gets
5//! a warning with the fix.
6
7use isoloom_spec::syntax::{self, OsRef, PackageName, Step, UnitName};
8
9use super::daemons::DaemonTable;
10use super::family::Family;
11
12/// A daemon a `packages` step installs that no `service` step names.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Unstarted {
15    /// The `packages` step, by its index in `app.steps`.
16    pub step: usize,
17    /// The package that ships the daemon.
18    pub package: PackageName,
19    /// The daemon's unit on the machine's family (what a `service` step names).
20    pub unit: UnitName,
21}
22
23/// The daemons machine `m` (on `os`) installs without a `service` step for them, in step order.
24/// Windows features aren't packages that start daemons: none there.
25#[must_use]
26pub fn unstarted(os: OsRef, m: &syntax::Machine, daemons: &DaemonTable) -> Vec<Unstarted> {
27    let family = Family::from(os);
28    let Some(app) = m.app().filter(|_| family != Family::Windows) else {
29        return Vec::new();
30    };
31    let named: Vec<&UnitName> = app
32        .steps
33        .iter()
34        .filter_map(|s| match s {
35            Step::Service(service) => daemons.find(service.name(), family).map(|(entry, _)| &entry.key),
36            _ => None,
37        })
38        .collect();
39    let mut out = Vec::new();
40    for (i, step) in app.steps.iter().enumerate() {
41        let Step::Packages(packages) = step else { continue };
42        for p in packages {
43            if let Some((entry, form)) = daemons.shipped_by(&p.name, family)
44                && !named.contains(&&entry.key)
45            {
46                out.push(Unstarted {
47                    step: i,
48                    package: p.name.clone(),
49                    unit: form.daemon.unit.clone(),
50                });
51            }
52        }
53    }
54    out
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    fn found(yaml: &str) -> Vec<(usize, String)> {
62        let m: syntax::Machine = serde_yaml_ng::from_str(yaml).unwrap_or_else(|e| panic!("{yaml}: {e}"));
63        let os = m.os.expect("an os");
64        unstarted(os, &m, &DaemonTable::common())
65            .into_iter()
66            .map(|u| (u.step, u.unit.to_string()))
67            .collect()
68    }
69
70    #[test]
71    fn a_daemon_package_without_its_service_step_is_found() {
72        assert_eq!(
73            found("{ os: debian@12, app: { steps: [{ run: a.sh }, { packages: [curl, nginx] }] } }"),
74            [(1, "nginx".to_owned())]
75        );
76        assert_eq!(
77            found("{ os: rocky@9, app: { steps: [{ packages: [httpd, openssh-server] }] } }"),
78            [(0, "httpd".to_owned()), (0, "sshd".to_owned())]
79        );
80    }
81
82    #[test]
83    fn a_service_step_for_the_daemon_says_where_it_runs() {
84        assert_eq!(found("{ os: debian@12, app: { steps: [{ packages: [nginx] }, { service: nginx }] } }"), []);
85        assert_eq!(
86            found("{ os: debian@12, app: { steps: [{ packages: [nginx] }, { service: { name: nginx, enabled: false } }] } }"),
87            []
88        );
89        // The table's key and the family's unit both name it.
90        assert_eq!(
91            found("{ os: debian@12, app: { steps: [{ packages: [openssh-server] }, { service: ssh }] } }"),
92            []
93        );
94        assert_eq!(
95            found("{ os: rocky@9, app: { steps: [{ packages: [openssh-server] }, { service: sshd }] } }"),
96            []
97        );
98    }
99
100    #[test]
101    fn windows_features_start_nothing_to_warn_about() {
102        assert_eq!(found("{ os: windows-server@2022, app: { steps: [{ packages: [Web-Server] }] } }"), []);
103    }
104}