Skip to main content

isoloom_render/container/
jobs.rs

1//! The one-shot `setup` jobs of the containers, and how a Compose project is started around
2//! them.
3
4use isoloom_spec::model::{Machine, Spec};
5
6/// The one-shot jobs of a machine, in order: `<machine>-init-<n>`.
7#[must_use]
8pub fn init_names(name: &str, m: &Machine) -> Vec<String> {
9    let n = m.docker.as_ref().map_or(0, |d| d.init.len());
10    (1..=n).map(|i| format!("{name}-init-{i}")).collect()
11}
12
13/// The one-shot jobs no running service waits for: those of machines nothing depends on.
14/// Compose's `up --wait` fails when such a job exits, even with 0, so starters bring the rest
15/// up with `--wait` first, then run these attached, in order (see [`start_commands`]).
16#[must_use]
17pub fn leaf_jobs(spec: &Spec) -> Vec<String> {
18    let waited_for = |name: &str| spec.machines.values().any(|o| o.docker.is_some() && o.depends_on.iter().any(|d| d == name));
19    spec.machines
20        .iter()
21        .filter(|(name, m)| m.docker.is_some() && !waited_for(name))
22        .flat_map(|(name, m)| init_names(name, m))
23        .collect()
24}
25
26/// The shell command that starts a Compose project with `compose` (e.g. `docker compose -f x`):
27/// plain `up -d --build --wait` when no job is left over, else everything but the jobs up and
28/// waited for, then each job attached. `jobs` are the leaf jobs ([`leaf_jobs`]); the services
29/// to wait for are listed at run time ([`as_arguments`]).
30#[must_use]
31pub fn start_commands(compose: &str, jobs: &[String], wait_timeout: Option<u32>) -> String {
32    let timeout = wait_timeout.map(|t| format!(" --wait-timeout {t}")).unwrap_or_default();
33    if jobs.is_empty() {
34        return format!("{compose} up -d --build --wait{timeout}");
35    }
36    let exclude: String = jobs.iter().map(|j| format!(" -e {j}")).collect::<Vec<_>>().concat();
37    let attached: String = jobs
38        .iter()
39        .map(|j| format!(" && {compose} up --no-deps --exit-code-from {j} {j}"))
40        .collect::<Vec<_>>()
41        .concat();
42    let services = as_arguments(&format!("{compose} config --services | grep -vx{exclude}"));
43    format!("{{ {services}; }} && {compose} up -d --build --wait{timeout} \"$@\"{attached}")
44}
45
46/// The shell commands making the words `listing` prints the positional parameters (`"$@"`), one
47/// argument each: a list known only at run time, handed to a command without splitting an
48/// unquoted expansion.
49#[must_use]
50pub fn as_arguments(listing: &str) -> String {
51    format!("set --; for w in $({listing}); do set -- \"$@\" \"$w\"; done")
52}