use indexmap::IndexMap;
use isoloom_plan::{Environment, MachineShape, PublishedPort};
use isoloom_render::container::{HybridContainers, docker_net};
use isoloom_render::{GeneratedFile, OUTPUT_DIR, header, router};
use isoloom_spec::model::{Protocol, Spec, Target};
use crate::error::Result;
use crate::file::{ComposeFile, Network, Service};
use crate::names::tool_of;
use crate::render::{compose_file, to_yaml};
const PUBLISH_NET: &str = "isoloom-publish";
pub fn render(env: &Environment) -> Result<HybridContainers> {
let alone = Environment::build(&containers_only(env.spec()), Target::Hybrid, env.instance);
let mut compose = compose_file(Target::Hybrid, &alone)?.file;
compose.services.shift_remove(router::NAME);
let names: Vec<String> = compose.services.keys().cloned().collect();
let vm_hosts = vm_hosts(env);
let mut published = Vec::new();
for (name, svc) in &mut compose.services {
let ports = ports_of(&alone, name);
if !ports.is_empty() {
publish(svc, &ports, &mut published);
}
svc.depends_on.retain(|k, _| names.contains(k));
if !svc.networks.is_empty() {
svc.extra_hosts.extend(vm_hosts.iter().cloned());
}
}
let networks: Vec<String> = env
.networks
.keys()
.filter(|n| {
env.machines
.values()
.any(|m| m.shape == MachineShape::Container && m.addresses.contains_key(*n))
})
.cloned()
.collect();
compose.networks = external_networks(env, &networks, !published.is_empty());
Ok(HybridContainers {
file: GeneratedFile {
path: format!("{OUTPUT_DIR}/hybrid/compose.yml"),
contents: contents(&compose)?,
},
published,
networks,
})
}
fn containers_only(spec: &Spec) -> Spec {
let mut s = spec.clone();
s.machines.retain(|_, m| m.docker.is_some());
let kept: Vec<String> = s.machines.keys().cloned().collect();
for m in s.machines.values_mut() {
m.depends_on.retain(|d| kept.contains(d));
}
s.checks.clear();
s.provision.clear();
s
}
fn vm_hosts(env: &Environment) -> Vec<String> {
env.machines
.iter()
.filter(|(_, m)| m.shape != MachineShape::Container)
.filter_map(|(n, m)| m.addresses.first().map(|(_, a)| format!("{n}:{a}")))
.collect()
}
fn ports_of(env: &Environment, name: &str) -> Vec<PublishedPort> {
if let Some(mp) = env.machines.get(name) {
return mp.ports.clone();
}
let tool = tool_of(name).and_then(|t| env.spec().tools.get(t));
match tool.map(|t| (t.port, t.publish)) {
Some((Some(port), Some(publish))) => vec![PublishedPort {
name: "web".into(),
port,
publish,
protocol: Protocol::Tcp,
fixed: true,
}],
_ => Vec::new(),
}
}
fn publish(svc: &mut Service, ports: &[PublishedPort], published: &mut Vec<(u16, Protocol)>) {
svc.ports = ports
.iter()
.map(|p| {
published.push((p.publish, p.protocol));
let proto = if p.protocol.is_tcp() { "" } else { "/udp" };
format!("{}:{}{proto}", p.publish, p.port)
})
.collect();
for a in svc.networks.values_mut().flatten() {
a.gw_priority = Some(1);
}
svc.networks.insert(PUBLISH_NET.into(), None);
}
fn external_networks(env: &Environment, nets: &[String], publishing: bool) -> IndexMap<String, Network> {
let mut out = IndexMap::new();
if publishing {
out.insert(PUBLISH_NET.to_owned(), Network::default());
}
for net in nets {
let n = Network {
external: true,
name: Some(docker_net(env, net)),
..Network::default()
};
out.insert(net.clone(), n);
}
out
}
fn contents(compose: &ComposeFile) -> Result<String> {
let yaml = to_yaml(Target::Hybrid, compose)?;
Ok(format!(
"{}# The containers of a hybrid environment: run on the isoloom-docker VM, on the networks it\n# creates there (macvlan, shared with the VMs). Started by the Vagrantfile next to it.\n\n{yaml}",
header("#")
))
}