use isoloom_manifest::{EnvName, Machine, Name, Part, Placeholder, PublishMode, StepId, Template};
use isoloom_plan::Environment;
use isoloom_render::container::{exec_runner, host_environment};
use isoloom_render::run_manifest::guest::{GuestCommand, GuestCompose, GuestRunner};
use isoloom_render::run_manifest::{DERIVED, check, derived_switch, networks, timeout_s};
use isoloom_render::{OUTPUT_DIR, steps};
use super::Project;
use super::checks::{Planned, planned, stand_ins};
use super::lifecycle::{job_step, owner, ready};
use super::machines::{Member, classify, machines};
use super::slots::{Starts, slots};
use crate::error::Result;
use crate::file::ComposeFile;
use crate::start::StartPlan;
const WAIT_TIMEOUT: &str = "600";
struct InVm {
project: String,
}
impl InVm {
fn compose(&self, before: &str, rest: &[Part]) -> Template {
let head = format!(
"cd {} && {before}sudo -E env ISOLOOM_PUBLISH_ADDRESS=0.0.0.0 ISOLOOM_PUBLISH_FIXED=1 docker compose --project-name {} --file {OUTPUT_DIR}/docker/compose.yml ",
steps::PROJECT_DIR,
self.project
);
Template::from_parts(std::iter::once(Part::Text(head)).chain(rest.iter().cloned()))
}
fn status(&self) -> Template {
Template::text(&format!(
"if command -v docker >/dev/null; then sudo docker compose --project-name {} ps --all --format json; fi",
self.project
))
}
}
fn text(words: &str) -> Vec<Part> {
vec![Part::Text(words.to_owned())]
}
fn command(id: &str, note: Option<String>, command: Template, timeout_s: u32) -> Result<GuestCommand> {
Ok(GuestCommand {
id: StepId::parse(id)?,
note,
command,
timeout_s,
})
}
pub(crate) fn guest(env: &Environment, file: &ComposeFile) -> Result<GuestCompose> {
let members = classify(env, file)?;
let plan = StartPlan::of(file);
let project = Project::Named(file.name.clone());
let vm = InVm { project: file.name.clone() };
let (runners, derived) = runners(env, &vm)?;
let mut machines: Vec<Machine> = machines(env, file, &members, &project)?;
for p in machines.iter_mut().flat_map(|m| m.ports.iter_mut()) {
if let Some(publish) = &mut p.publish {
publish.mode = PublishMode::Fixed;
publish.address = None;
}
}
Ok(GuestCompose {
machines,
networks: networks(env, |n| project.prefixed(n))?,
slots: slots(env, &members, &plan, Starts::Guest)?,
params: derived_switch(derived)?.into_iter().collect(),
start: start(env, &vm, &plan, &members)?,
ready: ready(&plan, &members, START)?,
check_setup: check_setup(file, &vm)?,
status: command("ps", None, vm.status(), 60)?,
runners,
})
}
const START: &str = "start";
fn start(env: &Environment, vm: &InVm, plan: &StartPlan, members: &[Member]) -> Result<Vec<GuestCommand>> {
let files = host_environment(env);
let mut up = format!("up --detach --build --pull missing --wait --wait-timeout {WAIT_TIMEOUT}");
for service in &plan.wait {
up.push(' ');
up.push_str(service);
}
let starting = Some("Starting the lab's containers in the VM…".to_owned());
let mut out = vec![command(START, starting, vm.compose(&files, &text(&up)), 1800)?];
for job in &plan.jobs {
let note = Some(format!("Running {}'s set-up script…", owner(members, job)));
let attached = format!("up --no-deps --exit-code-from {job} {job}");
out.push(command(&job_step(job), note, vm.compose(&files, &text(&attached)), 600)?);
}
Ok(out)
}
fn runners(env: &Environment, vm: &InVm) -> Result<(Vec<GuestRunner>, Vec<StepId>)> {
let mut out = Vec::new();
let mut derived = Vec::new();
for p in planned(env) {
let runner = compose_runner(vm, &p)?;
derived.push(runner.command.id.clone());
out.push(runner);
if let Some((machine, execs)) = &p.execs {
let id = format!("check-exec-{}", p.pos.id());
let piped = format!("exec -T {machine} sh -s < {OUTPUT_DIR}/docker/checks/{}", exec_runner(machine));
out.push(GuestRunner {
id: Name::parse(&format!("exec-{}", p.pos.id()))?,
from: format!("inside {machine}"),
checks: execs.iter().map(|c| check(c)).collect(),
command: command(&id, None, vm.compose("", &text(&piped)), timeout_s(execs))?,
on_timeout: Vec::new(),
});
}
}
Ok((out, derived))
}
fn compose_runner(vm: &InVm, p: &Planned<'_>) -> Result<GuestRunner> {
let id = format!("check-{}", p.pos.id());
let run = [
Part::Text("--profile check run --rm --no-deps --env ISOLOOM_DERIVED=".into()),
Part::Placeholder(Placeholder::Param(EnvName::parse(DERIVED)?)),
Part::Text(format!(" {}", p.service)),
];
let leftover = format!(
"sudo docker ps -aq --filter label=com.docker.compose.project={} --filter label=com.docker.compose.service={} --filter label=com.docker.compose.oneoff=True | xargs -r sudo docker rm -f",
vm.project, p.service
);
Ok(GuestRunner {
id: Name::parse(&p.service)?,
from: p.pos.label(),
checks: p.checks.iter().map(|c| check(c)).collect(),
command: command(&id, None, vm.compose("", &run), timeout_s(&p.checks))?,
on_timeout: vec![command(&format!("rm-{id}"), None, Template::text(&leftover), 60)?],
})
}
fn check_setup(file: &ComposeFile, vm: &InVm) -> Result<Vec<GuestCommand>> {
let stand_ins = stand_ins(file);
if stand_ins.is_empty() {
return Ok(Vec::new());
}
let up = format!("--profile check up --detach --wait --no-deps {}", stand_ins.join(" "));
let note = Some("Starting the access machine's stand-in…".to_owned());
Ok(vec![command("stand-ins", note, vm.compose("", &text(&up)), 600)?])
}
#[cfg(test)]
mod tests;