use isoloom_spec::model::CommandLine;
use isoloom_spec::syntax::{self, UnitName};
use super::daemons::Daemon;
use super::recipe::Recipe;
use super::resources::Resource;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContainerProcess {
Image,
Started {
entrypoint: Vec<String>,
command: Vec<String>,
},
Idle,
Command(CommandLine),
One(Daemon),
Supervised {
daemons: Vec<Daemon>,
command: Option<CommandLine>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NoProcess {
NoForeground {
unit: UnitName,
},
NothingServes,
}
impl ContainerProcess {
pub fn of(recipe: &Recipe, m: &syntax::Machine) -> Result<ContainerProcess, NoProcess> {
ContainerProcess::resolve(recipe, m.command.as_ref().map(|c| &c.run), !m.ports.is_empty())
}
pub fn resolve(recipe: &Recipe, command: Option<&CommandLine>, ports: bool) -> Result<ContainerProcess, NoProcess> {
let mut daemons = Vec::new();
for r in recipe.resources() {
let Resource::Service(s) = r else { continue };
if !s.enabled {
continue;
}
match recipe.daemons.iter().find(|d| d.unit == s.unit) {
Some(d) => daemons.push(d.clone()),
None => return Err(NoProcess::NoForeground { unit: s.unit.clone() }),
}
}
match (daemons.len(), command.cloned()) {
(0, None) if ports && recipe.resources().next().is_some() => Err(NoProcess::NothingServes),
(0, None) => Ok(ContainerProcess::Idle),
(0, Some(c)) => Ok(ContainerProcess::Command(c)),
(1, None) => Ok(daemons.pop().map_or(ContainerProcess::Idle, ContainerProcess::One)),
(_, command) => Ok(ContainerProcess::Supervised { daemons, command }),
}
}
#[must_use]
pub fn is_runner(&self) -> bool {
matches!(
self,
ContainerProcess::One(_) | ContainerProcess::Supervised { .. } | ContainerProcess::Started { .. }
)
}
}
#[cfg(test)]
mod tests {
use super::super::{DaemonTable, recipe};
use super::*;
fn process(yaml: &str) -> Result<ContainerProcess, NoProcess> {
let m: syntax::Machine = serde_yaml_ng::from_str(yaml).unwrap_or_else(|e| panic!("{yaml}: {e}"));
let os = m.os.expect("the test's machine has an os");
let r = recipe(os, &m, &DaemonTable::common()).expect("a portable recipe");
ContainerProcess::of(&r, &m)
}
#[test]
fn no_service_and_no_command_is_idle() {
assert_eq!(process("{ os: debian@12, app: { steps: [{ packages: [curl] }] } }"), Ok(ContainerProcess::Idle));
}
#[test]
fn a_command_alone_is_the_containers_command() {
let p = process("{ os: debian@12, ports: { web: 80 }, command: [nginx, -g, 'daemon off;'], app: { steps: [{ packages: [nginx] }] } }");
assert!(matches!(p, Ok(ContainerProcess::Command(_))), "{p:?}");
}
#[test]
fn one_enabled_service_is_the_foreground_process() {
let Ok(ContainerProcess::One(d)) = process("{ os: debian@12, ports: { web: 80 }, app: { steps: [{ packages: [nginx] }, { service: nginx }] } }") else {
panic!()
};
assert_eq!(d.foreground.args(), ["nginx", "-g", "daemon off;"]);
}
#[test]
fn two_services_or_one_and_a_command_are_supervised() {
let two = process(
"{ os: debian@12, app: { steps: [{ service: { name: api, run: [/srv/api] } }, { service: { name: worker, run: [/srv/worker], user: api } }] } }",
);
let Ok(ContainerProcess::Supervised { daemons, command: None }) = two else {
panic!("{two:?}")
};
assert_eq!(daemons.iter().map(|d| d.unit.as_str()).collect::<Vec<_>>(), ["api", "worker"]);
let with_command = process("{ os: debian@12, command: [tail, -f, /dev/null], app: { steps: [{ service: { name: api, run: [/srv/api] } }] } }");
assert!(matches!(with_command, Ok(ContainerProcess::Supervised { command: Some(_), .. })));
}
#[test]
fn a_disabled_service_is_not_started() {
let p = process("{ os: debian@12, app: { steps: [{ packages: [nginx] }, { service: { name: nginx, enabled: false } }] } }");
assert_eq!(p, Ok(ContainerProcess::Idle));
}
#[test]
fn a_service_with_no_foreground_form_is_refused() {
let p = process("{ os: debian@12, app: { steps: [{ service: myd }] } }");
assert_eq!(p, Err(NoProcess::NoForeground { unit: "myd".parse().unwrap() }));
assert!(process("{ os: alpine, app: { steps: [{ service: cron }] } }").is_err());
}
#[test]
fn ports_nothing_serves_are_refused() {
assert_eq!(
process("{ os: debian@12, ports: { web: 80 }, app: { steps: [{ run: web.sh }] } }"),
Err(NoProcess::NothingServes)
);
assert_eq!(
process("{ os: debian@12, ports: { web: 80 } }"),
Ok(ContainerProcess::Idle),
"the OS alone stays idle, as before"
);
}
}