use isoloom_catalogue::{Catalogue, LogStream};
use isoloom_spec::syntax::{AbsPath, Argv, PackageName, UnitName, UserName};
use super::family::Family;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Daemon {
pub unit: UnitName,
pub foreground: Argv,
pub user: Option<UserName>,
pub logs: Vec<LogLink>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogLink {
pub path: AbsPath,
pub stream: Stream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Stream {
Stdout,
Stderr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FamilyDaemon {
pub family: Family,
pub package: Option<PackageName>,
pub daemon: Daemon,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DaemonEntry {
pub key: UnitName,
pub forms: Vec<FamilyDaemon>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DaemonTable {
entries: Vec<DaemonEntry>,
}
impl DaemonTable {
#[must_use]
pub fn new(entries: Vec<DaemonEntry>) -> DaemonTable {
DaemonTable { entries }
}
#[must_use]
pub fn common() -> DaemonTable {
Catalogue::embedded().map(DaemonTable::from).unwrap_or_default()
}
#[must_use]
pub fn entries(&self) -> &[DaemonEntry] {
&self.entries
}
#[must_use]
pub fn find(&self, name: &UnitName, family: Family) -> Option<(&DaemonEntry, &FamilyDaemon)> {
self.entries.iter().find_map(|e| {
let form = e.forms.iter().find(|f| f.family == family)?;
(e.key == *name || form.daemon.unit == *name).then_some((e, form))
})
}
#[must_use]
pub fn shipped_by(&self, package: &PackageName, family: Family) -> Option<(&DaemonEntry, &FamilyDaemon)> {
self.entries.iter().find_map(|e| {
let form = e.forms.iter().find(|f| f.family == family && f.package.as_ref() == Some(package))?;
Some((e, form))
})
}
}
impl From<&Catalogue> for DaemonTable {
fn from(catalogue: &Catalogue) -> DaemonTable {
let entries = catalogue
.daemons()
.map(|(key, d)| DaemonEntry {
key: key.clone(),
forms: d
.unit
.families()
.into_iter()
.filter_map(|family| {
let daemon = Daemon {
unit: d.unit.on(family)?.clone(),
foreground: d.foreground.on(family)?.clone(),
user: d.user.clone(),
logs: d
.logs
.iter()
.map(|(path, stream)| LogLink {
path: path.clone(),
stream: (*stream).into(),
})
.collect(),
};
let package = d.package.as_ref().and_then(|p| p.on(family)).cloned();
Some(FamilyDaemon { family, package, daemon })
})
.collect(),
})
.collect();
DaemonTable::new(entries)
}
}
impl From<LogStream> for Stream {
fn from(s: LogStream) -> Stream {
match s {
LogStream::Stderr => Stream::Stderr,
_ => Stream::Stdout,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unit(s: &str) -> UnitName {
s.parse().unwrap()
}
#[test]
fn the_catalogue_fills_the_table() {
let table = DaemonTable::common();
let keys: Vec<&str> = table.entries().iter().map(|e| e.key.as_str()).collect();
for key in ["nginx", "apache", "ssh", "cron", "rsyslog"] {
assert!(keys.contains(&key), "{key}: {keys:?}");
}
}
#[test]
fn a_step_finds_a_daemon_by_key_or_by_the_unit_its_family_names() {
let table = DaemonTable::common();
let (_, debian) = table.find(&unit("ssh"), Family::Debian).unwrap();
assert_eq!(debian.daemon.unit.as_str(), "ssh");
let (entry, rocky) = table.find(&unit("sshd"), Family::RedHat).unwrap();
assert_eq!((entry.key.as_str(), rocky.daemon.foreground.program()), ("ssh", "/usr/sbin/sshd"));
assert!(table.find(&unit("cron"), Family::Alpine).is_none(), "no cron form on Alpine");
assert!(table.find(&unit("myd"), Family::Debian).is_none());
}
#[test]
fn a_package_names_the_daemon_it_ships() {
let table = DaemonTable::common();
let (entry, form) = table.shipped_by(&"httpd".parse().unwrap(), Family::RedHat).unwrap();
assert_eq!((entry.key.as_str(), form.daemon.unit.as_str()), ("apache", "httpd"));
assert!(table.shipped_by(&"httpd".parse().unwrap(), Family::Debian).is_none());
let (_, nginx) = table.shipped_by(&"nginx".parse().unwrap(), Family::Alpine).unwrap();
assert_eq!(nginx.daemon.logs.len(), 2);
}
}