use indexmap::IndexMap;
use isoloom_spec::model::{self, BaseImage, DockerImpl, Generated, HttpCheck, MachineFile, MachineSecret, Protocol, SecretMount, VmImpl};
use isoloom_spec::syntax::{
self, Code, EnvValue, FieldPath, FileSource, HealthProbe, Kind, Octets, PortProtocol, PortRef, Problem, Publish, Recipe, Step, Tpm,
};
use isoloom_spec::model::Shape;
use super::forms::{self, Container, Service, Vm};
use super::overrides;
use super::trail::Trail;
use super::{Lowering, Scope};
pub(super) struct LoweredMachine {
pub machine: model::Machine,
pub inputs: Vec<(String, String)>,
pub problems: Vec<Problem>,
}
pub(super) fn input_secret(input: &str) -> String {
format!("input-{}", input.to_ascii_lowercase().replace('_', "-"))
}
pub(super) fn lower(l: &Lowering<'_>, name: &str, written: &syntax::Machine, trail: &mut Trail) -> LoweredMachine {
let service = l.services.get(name);
let (m, mut problems) = super::service::effective(name, written, service);
let m = m.as_ref();
let (files, secrets, inputs) = files(name, m, trail);
trail.machine(name, m);
let at = FieldPath::new("machines").key(name);
let (ports, unnumbered) = ports(&at, m);
problems.extend(unnumbered);
let (healthcheck, unknown_port) = m.healthcheck.as_ref().map(|h| healthcheck(&at, m, h)).unzip();
problems.extend(unknown_port.flatten());
let docker = docker(l, name, m, service);
let official = docker.as_ref().and_then(|d| d.generated.as_ref()).is_some_and(|g| g.from == BaseImage::Entry);
let (networks, more_addresses) = addresses(&m.networks);
let mut out = model::Machine {
networks,
more_addresses,
count: None,
aliases: m.aliases.clone(),
os: m.os.map(|o| o.to_string()),
arch: m.hardware.arch,
rate_limit: m.rate_limit.clone(),
privileged: m.privileged,
nested_virtualization: m.hardware.nested_virtualization,
gpus: m.hardware.gpus.clone(),
devices: m.hardware.devices.clone(),
read_only: m.read_only,
tmpfs: m.tmpfs.clone(),
shm_size: m.shm_size.clone(),
dns: m.dns.clone(),
environment: environment(m, image_environment(l, official, service)),
sysctls: m.sysctls.clone(),
ulimits: m.ulimits.clone(),
ports,
inputs: environment_inputs(m),
secrets,
resources: m.resources,
command: m.command.as_ref().map(|c| c.run.clone()),
working_dir: m.command.as_ref().and_then(|c| c.cwd.clone()),
healthcheck: healthcheck.flatten(),
depends_on: m.depends_on.clone(),
volumes: m.volumes.clone(),
files,
disks: m.disks.clone(),
access: m.access,
vm: vm(l.source, name, m, service),
docker,
external: overrides::external(l.source, name),
supplied: false,
configuration: configuration(l, m, service, official),
};
if out.docker.as_ref().is_some_and(|d| d.lxc) {
out.vm = None;
}
LoweredMachine {
machine: out,
inputs,
problems,
}
}
fn image_environment(l: &Lowering<'_>, official: bool, service: Service<'_>) -> IndexMap<String, String> {
match service {
Some(Ok(resolved)) if official && runs_containers(l) => super::service::image_environment(resolved),
_ => IndexMap::new(),
}
}
fn runs_containers(l: &Lowering<'_>) -> bool {
l.scope.target().is_some_and(|t| matches!(t.needs(), Shape::Docker | Shape::Either))
}
fn environment_inputs(m: &syntax::Machine) -> Vec<String> {
m.environment
.values()
.filter_map(|v| match v {
EnvValue::Input(i) => Some(i.to_string()),
_ => None,
})
.collect()
}
fn configuration(l: &Lowering<'_>, m: &syntax::Machine, service: Service<'_>, official: bool) -> Option<model::Configuration> {
if let Some(Ok(resolved)) = service {
return if official && runs_containers(l) {
super::service::image_configuration(resolved)
} else {
super::service::configuration(resolved)
};
}
let (os, steps) = (m.os?, &m.app()?.steps);
(!steps.is_empty()).then(|| model::Configuration::app(os, steps.clone()))
}
fn serves(l: &Lowering<'_>, m: &syntax::Machine, service: Service<'_>) -> bool {
let Some(c) = configuration(l, m, service, false) else { return false };
crate::configure::Recipe::of(&c, &[], &crate::configure::DaemonTable::common()).is_ok_and(|r| !r.daemons.is_empty())
}
fn addresses(networks: &IndexMap<String, Octets>) -> (IndexMap<String, u8>, IndexMap<String, Vec<u8>>) {
let mut first = IndexMap::new();
let mut more = IndexMap::new();
for (n, o) in networks {
match o {
Octets::One(a) => {
first.insert(n.clone(), *a);
}
Octets::Several(list) => {
if let Some((a, rest)) = list.split_first() {
first.insert(n.clone(), *a);
if !rest.is_empty() {
more.insert(n.clone(), rest.to_vec());
}
}
}
}
}
(first, more)
}
fn environment(m: &syntax::Machine, image: IndexMap<String, String>) -> IndexMap<String, String> {
let own = m.environment.iter().filter_map(|(k, v)| match v {
EnvValue::Text(t) => Some((k.clone(), t.clone())),
EnvValue::Input(_) | EnvValue::Secret(_) => None,
});
image.into_iter().chain(own).collect()
}
fn ports(at: &FieldPath, m: &syntax::Machine) -> (Vec<model::Port>, Vec<Problem>) {
let service = matches!(m.kind, Kind::Service(_));
let mut problems = Vec::new();
let ports = m
.ports
.iter()
.filter_map(|(name, p)| {
let Some(number) = p.port else {
if !service {
let path = at.key("ports").key(name.as_str()).key("port");
problems.push(Problem::new(Code::MissingField, path, "a port says its number"));
}
return None;
};
let port = number.get();
let (publish, fixed) = match p.publish {
None => (None, false),
Some(Publish::Free) => (Some(port), false),
Some(Publish::Preferred(h)) => (Some(h.get()), false),
Some(Publish::Fixed(h)) => (Some(h.get()), true),
};
Some(model::Port {
port,
name: Some(name.to_string()),
http: p.protocol.is_http(),
tls: p.protocol.is_tls(),
publish,
fixed,
dormant: p.dormant,
protocol: if p.protocol == PortProtocol::Udp { Protocol::Udp } else { Protocol::Tcp },
})
})
.collect();
(ports, problems)
}
fn healthcheck(at: &FieldPath, m: &syntax::Machine, h: &syntax::Healthcheck) -> (Option<model::Healthcheck>, Option<Problem>) {
let mut unknown = None;
let (test, page) = match &h.probe {
HealthProbe::Test(test) => (Some(test.clone()), None),
HealthProbe::Http(page) => (None, Some(page)),
};
let http = page.and_then(|page| {
let port = match &page.port {
PortRef::Number(n) => Some(n.get()),
PortRef::Name(n) => {
let Some(p) = m.ports.get(n) else {
let path = at.key("healthcheck").key("http").key("port");
unknown = Some(Problem::new(Code::UnknownReference, path, format!("the machine has no port named `{n}`")));
return None;
};
p.port.map(std::num::NonZeroU16::get)
}
};
port.map(|port| HttpCheck { port, path: page.path.clone() })
});
if test.is_none() && http.is_none() {
return (None, unknown);
}
let lowered = model::Healthcheck {
test,
http,
interval: h.interval.clone(),
timeout: h.timeout.clone(),
retries: h.retries,
start_period: h.start_period.clone(),
};
(Some(lowered), unknown)
}
fn files(name: &str, m: &syntax::Machine, trail: &mut Trail) -> (Vec<MachineFile>, Vec<MachineSecret>, Vec<(String, String)>) {
let windows = m.os.is_some_and(syntax::OsRef::is_windows);
let (mut files, mut secrets, mut inputs) = (Vec::new(), Vec::new(), Vec::new());
for (i, f) in m.files.iter().enumerate() {
let (source, content) = match &f.source {
FileSource::Source(p) => (Some(p.clone()), None),
FileSource::Content(c) => (None, Some(c.clone())),
FileSource::Input(input) => {
let secret = input_secret(input.as_str());
inputs.push((secret.clone(), input.to_string()));
trail.file(name, "secrets", secrets.len(), i);
secrets.push(value_file(&secret, f, windows));
continue;
}
FileSource::Secret(secret) => {
trail.file(name, "secrets", secrets.len(), i);
secrets.push(value_file(secret, f, windows));
continue;
}
};
trail.file(name, "files", files.len(), i);
files.push(MachineFile {
path: f.path.clone(),
source,
content,
uid: f.uid,
gid: f.gid,
mode: f.mode.map(|mode| mode.to_string()),
});
}
(files, secrets, inputs)
}
fn value_file(secret: &str, f: &syntax::File, windows: bool) -> MachineSecret {
MachineSecret::Mount(SecretMount {
source: secret.to_owned(),
target: Some(f.path.clone()),
uid: f.uid,
gid: f.gid,
mode: (!windows).then(|| f.mode_or_default()),
})
}
fn docker(l: &Lowering<'_>, name: &str, m: &syntax::Machine, service: Service<'_>) -> Option<DockerImpl> {
let (source, scope) = (l.source, l.scope);
let idle = m.command.is_none();
let mut d = DockerImpl {
image: None,
build: None,
dockerfile: None,
target: None,
args: IndexMap::new(),
init: m.setup.clone(),
idle: false,
appliance: None,
config: None,
firmware: None,
lxc: overrides::lxc(source, name, scope),
generated: None,
};
match forms::container(m, service).ok()? {
Container::Supplied => return None,
Container::Recipe(Recipe::Image(i)) => d.image = Some(i.clone()),
Container::Recipe(Recipe::Build(b)) => {
d.build = Some(b.context.clone());
d.dockerfile.clone_from(&b.dockerfile);
d.target.clone_from(&b.target);
d.args.clone_from(&b.args);
}
Container::Recipe(Recipe::Appliance(a)) => {
d.image.clone_from(&a.image);
d.appliance = Some(a.kind);
d.config.clone_from(&a.config);
d.firmware.clone_from(&a.firmware);
}
Container::Steps { os, base } => {
d.generated = Some(Generated {
os: os.name().to_owned(),
base: base.to_owned(),
from: BaseImage::Os,
after_start: None,
});
d.idle = idle && !serves(l, m, service);
}
Container::Os { image } => {
d.image = Some(image.to_owned());
d.idle = idle;
}
Container::Entry { os, image, start } => {
d.generated = Some(Generated {
os: os.name().to_owned(),
base: image.reference.clone(),
from: BaseImage::Entry,
after_start: (!start.is_empty()).then(|| model::OwnProcess {
entrypoint: image.entrypoint.clone(),
command: image.command_override.clone().unwrap_or_else(|| image.command.clone()),
}),
});
}
}
Some(d)
}
fn vm(source: &syntax::Spec, name: &str, m: &syntax::Machine, service: Service<'_>) -> Option<VmImpl> {
let Ok(Vm::Os(os)) = forms::vm(m, service) else { return None };
let provision = m
.app()
.map(|a| {
a.steps
.iter()
.filter_map(|s| match s {
Step::Run { script, .. } => Some(script.to_string()),
Step::Reboot => Some(model::REBOOT_STEP.to_owned()),
Step::Playbook(p) => Some(p.to_string()),
_ => None,
})
.collect()
})
.unwrap_or_default();
Some(VmImpl {
os: os.name().to_owned(),
provision,
image: overrides::vm_image(source, name),
tpm: match m.hardware.tpm {
Tpm::Auto => None,
Tpm::Required => Some(true),
Tpm::None => Some(false),
},
boot: m.hardware.boot.clone(),
setup: m.setup.clone(),
})
}
pub(super) fn resolve_dns(spec: &mut model::Spec, scope: Scope) {
let laid_out = match scope.target() {
Some(t) => crate::environment::lay_out(spec, t),
None => spec.clone(),
};
for (name, m) in &mut spec.machines {
let Some(dns) = &mut m.dns else { continue };
for server in &mut dns.servers {
if server.parse::<std::net::IpAddr>().is_ok() {
continue;
}
if let Some(a) = address_on_shared(&laid_out, name, server) {
*server = a.to_string();
}
}
}
}
fn address_on_shared(spec: &model::Spec, machine: &str, server: &str) -> Option<std::net::Ipv4Addr> {
let (m, s) = (spec.machines.get(machine)?, spec.machines.get(server)?);
m.networks.keys().find_map(|n| {
let octet = s.networks.get(n).filter(|_| spec.networks.contains_key(n))?;
Some(crate::addressing::address(spec, n, *octet))
})
}