use std::net::{Ipv4Addr, Ipv6Addr};
use isoloom_spec::model::{Machine, MachineFile, Spec};
use crate::addressing::address_for;
use crate::configure::{Recipe, Resource, Shell};
use crate::environment::Route;
use crate::ipv6::{self, Route6};
use crate::routing;
use crate::storage::DataDisk;
pub type MachineLifecycle = Vec<Step>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Step {
Prelude,
DiskGrow,
Disks {
disks: Vec<DataDisk>,
},
Settings,
Hosts {
entries: Vec<(Ipv4Addr, Vec<String>)>,
},
Routes {
routes: Vec<Route>,
default: Option<Ipv4Addr>,
},
Ipv6 {
addresses: Vec<(Ipv4Addr, Ipv6Addr)>,
routes: Vec<Route6>,
},
Trunks,
MoreAddresses {
addresses: Vec<crate::more_addresses::MoreAddresses>,
},
Volumes {
paths: Vec<String>,
},
SharedVolume {
volume: String,
path: String,
role: SharedRole,
},
Project,
Files {
files: Vec<MachineFile>,
},
Inputs {
persist: bool,
},
Secrets,
WaitFor {
machine: String,
ports: Vec<u16>,
},
Run {
path: String,
kind: RunKind,
creates: Option<String>,
},
Configure {
resources: Vec<Resource>,
fresh: bool,
},
Reboot,
Command,
Setup {
paths: Vec<String>,
},
Egress,
Tc,
Ready,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SharedRole {
Serve {
clients: Vec<Ipv4Addr>,
folders: Vec<String>,
},
Mount {
source: String,
owner: String,
},
HostFolder {
tag_file: String,
folder: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunKind {
Shell,
Playbook,
PowerShell,
}
#[must_use]
pub fn of(spec: &Spec, name: &str, m: &Machine, recipe: Option<&Recipe>) -> MachineLifecycle {
let mut steps = vec![Step::Prelude];
if m.resources.and_then(|r| r.disk_gb).is_some() {
steps.push(Step::DiskGrow);
}
if !m.disks.is_empty() {
steps.push(Step::Disks {
disks: crate::storage::disks(name, m),
});
}
steps.push(Step::Project);
steps.push(hosts(spec, name));
steps.push(Step::Settings);
steps.extend(shared_volumes(spec, name, m));
steps.push(Step::Trunks);
steps.extend(crate::more_addresses::step(spec, m));
steps.push(Step::Routes {
routes: routing::routes(spec, name, m).into_iter().map(|(cidr, via)| Route { cidr, via }).collect(),
default: routing::default_gateway(spec, name, m),
});
steps.extend(ipv6_step(spec, m));
let own: Vec<String> = m
.volumes
.iter()
.filter(|(v, _)| !spec.volumes.contains_key(*v))
.map(|(_, p)| p.path().to_owned())
.collect();
if !own.is_empty() {
steps.push(Step::Volumes { paths: own });
}
steps.extend(crate::machine_files::step(m));
if !m.secrets.is_empty() {
steps.push(Step::Secrets);
}
steps.extend(m.depends_on.iter().filter_map(|dep| {
let ports = spec.machines.get(dep)?.ready_ports();
(!ports.is_empty()).then(|| Step::WaitFor { machine: dep.clone(), ports })
}));
steps.extend(runs(m, recipe));
if m.command.is_some() {
steps.push(Step::Command);
}
if let Some(vm) = m.vm.as_ref().filter(|vm| !vm.setup.is_empty()) {
steps.push(Step::Setup { paths: vm.setup.clone() });
}
if routing::offline(spec, name, m) {
steps.push(Step::Egress);
}
if m.networks.keys().any(|n| spec.networks.get(n).is_some_and(|net| net.tc.is_some())) {
steps.push(Step::Tc);
}
steps.push(Step::Ready);
steps
}
fn ipv6_step(spec: &Spec, m: &Machine) -> Option<Step> {
let addresses: Vec<(Ipv4Addr, Ipv6Addr)> = m
.networks
.iter()
.filter_map(|(n, o)| Some((crate::addressing::address(spec, n, *o), ipv6::address(spec, n, *o)?)))
.collect();
let routes = ipv6::routes(spec, m);
(!addresses.is_empty() || !routes.is_empty()).then_some(Step::Ipv6 { addresses, routes })
}
fn hosts(spec: &Spec, name: &str) -> Step {
let linux = !spec.machines.get(name).is_some_and(windows);
let entries = spec
.machines
.iter()
.filter(|(o, om)| o.as_str() != name && !om.networks.is_empty() && !(linux && windows(om)))
.map(|(o, om)| {
let names = std::iter::once(o.clone()).chain(om.aliases.iter().cloned()).collect();
(address_for(spec, name, o), crate::private_dns::with_domain(spec, names))
})
.collect();
Step::Hosts {
entries: [entries, more_addresses(spec, name, linux)].concat(),
}
}
fn more_addresses(spec: &Spec, name: &str, linux: bool) -> Vec<(Ipv4Addr, Vec<String>)> {
spec.machines
.iter()
.filter(|(o, om)| o.as_str() != name && !(linux && windows(om)))
.flat_map(|(o, om)| {
let names = crate::private_dns::with_domain(spec, vec![o.clone()]);
crate::more_addresses::of(spec, om).into_values().flatten().map(move |a| (a, names.clone()))
})
.collect()
}
fn shared_volumes(spec: &Spec, name: &str, m: &Machine) -> Vec<Step> {
let mount_of = |machine: &str, v: &str| spec.machines.get(machine).and_then(|om| om.volumes.get(v));
m.volumes
.iter()
.filter(|(v, _)| spec.volumes.contains_key(*v))
.filter_map(|(v, mount)| {
let owner = spec.volume_owner(v)?;
let role = if owner == name {
let others: Vec<&str> = spec.volume_users(v).into_iter().filter(|u| *u != name).collect();
let mut folders: Vec<String> = others.iter().filter_map(|u| mount_of(u, v)?.subpath().map(String::from)).collect();
folders.dedup();
SharedRole::Serve {
clients: others.iter().map(|u| address_for(spec, name, u)).collect(),
folders,
}
} else {
let at = mount_of(owner, v)?.path().trim_end_matches('/');
let folder = mount.subpath().map(|f| format!("/{f}")).unwrap_or_default();
SharedRole::Mount {
source: format!("{}:{at}{folder}", address_for(spec, name, owner)),
owner: owner.to_owned(),
}
};
Some(Step::SharedVolume {
volume: v.clone(),
path: mount.path().to_owned(),
role,
})
})
.collect()
}
fn runs(m: &Machine, recipe: Option<&Recipe>) -> Vec<Step> {
if m.vm.is_none() {
return Vec::new();
}
let mut steps = Vec::new();
if !m.inputs.is_empty() {
let reboots = recipe.is_some_and(|r| r.resources().any(|x| matches!(x, Resource::Reboot)));
steps.push(Step::Inputs {
persist: reboots || m.command.is_some(),
});
}
let Some(recipe) = recipe else { return steps };
let mut run: Vec<Resource> = Vec::new();
let mut fresh = true;
for r in recipe.resources() {
let own = match r {
Resource::Run(script) => Step::Run {
path: script.script.to_string(),
kind: if script.shell == Shell::PowerShell {
RunKind::PowerShell
} else {
RunKind::Shell
},
creates: script.creates.as_ref().map(ToString::to_string),
},
Resource::Playbook(p) => Step::Run {
path: p.to_string(),
kind: RunKind::Playbook,
creates: None,
},
Resource::Reboot => Step::Reboot,
other => {
run.push(other.clone());
continue;
}
};
if !run.is_empty() {
steps.push(Step::Configure {
resources: std::mem::take(&mut run),
fresh,
});
}
steps.push(own);
fresh = false;
}
if !run.is_empty() {
steps.push(Step::Configure { resources: run, fresh });
}
steps
}
#[must_use]
pub fn has_extension(path: &str, extension: &str) -> bool {
std::path::Path::new(path).extension().is_some_and(|e| e.eq_ignore_ascii_case(extension))
}
fn windows(m: &Machine) -> bool {
m.vm.as_ref().is_some_and(|vm| isoloom_spec::images::is_windows(&vm.os))
}
#[cfg(test)]
mod tests;