use std::fmt;
use std::net::Ipv4Addr;
use indexmap::IndexMap;
use isoloom_spec::model::{Bridged, Machine, Spec};
use crate::addressing::address;
use crate::requirements::Requirement;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutsideNetwork {
pub cidr: String,
pub join: Join,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Join {
Existing(String),
Bridged(Bridged),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutsideAddress {
Fixed(Ipv4Addr),
Dhcp,
}
#[must_use]
pub fn networks(spec: &Spec) -> IndexMap<String, OutsideNetwork> {
spec.networks
.iter()
.filter_map(|(name, n)| {
let join = match (&n.external, &n.bridged) {
(Some(e), _) => Join::Existing(e.clone()),
(None, Some(b)) => Join::Bridged(b.clone()),
(None, None) => return None,
};
Some((name.clone(), OutsideNetwork { cidr: n.cidr.clone(), join }))
})
.collect()
}
#[must_use]
pub fn addresses(spec: &Spec, m: &Machine) -> IndexMap<String, OutsideAddress> {
m.networks
.iter()
.filter_map(|(net, octet)| {
let n = spec.networks.get(net).filter(|n| n.is_outside())?;
let a = if n.is_dhcp() {
OutsideAddress::Dhcp
} else {
OutsideAddress::Fixed(address(spec, net, *octet))
};
Some((net.clone(), a))
})
.collect()
}
#[must_use]
pub fn without(spec: &Spec) -> Spec {
let mut s = spec.clone();
s.networks.retain(|_, n| !n.is_outside());
for m in s.machines.values_mut() {
m.networks.retain(|net, _| spec.networks.get(net).is_none_or(|n| !n.is_outside()));
}
s
}
#[must_use]
pub fn requirements(spec: &Spec) -> Vec<Requirement> {
let mut out = Vec::new();
if let Some((network, n)) = spec.networks.iter().find(|(_, n)| n.external.is_some()) {
out.push(Requirement::ExternalNetwork {
network: network.clone(),
name: n.external.clone().unwrap_or_default(),
});
}
if let Some((network, _)) = spec.networks.iter().find(|(_, n)| n.external.is_none() && n.bridged.is_some()) {
out.push(Requirement::BridgedNetwork { network: network.clone() });
}
out
}
pub(crate) fn no_existing_network(f: &mut fmt::Formatter<'_>, network: &str, name: &str) -> fmt::Result {
write!(
f,
"network `{network}` is an existing network (`external: {name}`), and this target makes every network it uses (`docker`, `hosted` and `proxmox` join existing ones)"
)
}
pub(crate) fn no_bridge(f: &mut fmt::Formatter<'_>, network: &str) -> fmt::Result {
write!(
f,
"network `{network}` is bridged to a host interface (`bridged`), which this target has no way to do (`vagrant` and `proxmox` bridge one)"
)
}
#[cfg(test)]
mod tests {
use isoloom_spec::model::Target;
use super::*;
use crate::{Feature, testing};
const SPEC: &str = "
version: 1
name: t
networks:
lan: { cidr: 10.20.0.0/24 }
corp: { cidr: 192.168.50.0/24 }
home: { cidr: 10.0.0.0/24, bridged: { interface: en0, dhcp: true } }
machines:
a: { networks: { lan: 10, corp: 20, home: 0 }, app: { image: nginx } }
overrides:
docker: { networks: { corp: { external: corp-net } } }
";
#[test]
fn outside_networks_stay_apart_from_the_environment_s_own() {
let env = testing::planned(&testing::valid(SPEC), Target::Docker);
assert_eq!(env.networks.keys().collect::<Vec<_>>(), ["lan"]);
assert_eq!(env.spec().networks.len(), 1);
assert_eq!(env.outside["corp"].join, Join::Existing("corp-net".into()));
assert_eq!(env.outside["corp"].cidr, "192.168.50.0/24");
let a = &env.machines["a"];
assert_eq!(a.addresses.keys().collect::<Vec<_>>(), ["lan"]);
assert_eq!(a.outside["corp"], OutsideAddress::Fixed("192.168.50.20".parse().unwrap()));
assert_eq!(a.outside["home"], OutsideAddress::Dhcp);
let features: Vec<Feature> = env.requirements.iter().map(Requirement::feature).collect();
assert!(features.contains(&Feature::ExternalNetworks) && features.contains(&Feature::BridgedNetworks));
}
}