Skip to main content

isoloom_plan/
outside.rs

1//! Networks outside the environment: an existing network the machines join by name
2//! (`external`), or the host's own LAN through a bridged interface (`bridged`). The plan keeps
3//! them apart from the environment's own networks: the router, the controller, the checks and
4//! every target's own networks never see them, and a machine's place on them is an extra
5//! interface after its own networks. A target renders them, or refuses them through its
6//! capabilities ([`requirements`]).
7
8use std::fmt;
9use std::net::Ipv4Addr;
10
11use indexmap::IndexMap;
12use isoloom_spec::model::{Bridged, Machine, Spec};
13
14use crate::addressing::address;
15use crate::requirements::Requirement;
16
17/// A network outside the environment, as the plan keeps it.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct OutsideNetwork {
20    /// Its block, as the author wrote it (the network's real one).
21    pub cidr: String,
22    /// How the machines get onto it.
23    pub join: Join,
24}
25
26/// How the machines get onto an outside network.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Join {
29    /// An existing network, by its name on the target.
30    Existing(String),
31    /// A bridge to an interface of the host.
32    Bridged(Bridged),
33}
34
35/// A machine's address on an outside network.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum OutsideAddress {
38    /// A fixed one, in the network's block.
39    Fixed(Ipv4Addr),
40    /// One from the LAN's DHCP server.
41    Dhcp,
42}
43
44/// The spec's outside networks, in spec order.
45#[must_use]
46pub fn networks(spec: &Spec) -> IndexMap<String, OutsideNetwork> {
47    spec.networks
48        .iter()
49        .filter_map(|(name, n)| {
50            let join = match (&n.external, &n.bridged) {
51                (Some(e), _) => Join::Existing(e.clone()),
52                (None, Some(b)) => Join::Bridged(b.clone()),
53                (None, None) => return None,
54            };
55            Some((name.clone(), OutsideNetwork { cidr: n.cidr.clone(), join }))
56        })
57        .collect()
58}
59
60/// Machine `m`'s addresses on the outside networks it joins, in its order.
61#[must_use]
62pub fn addresses(spec: &Spec, m: &Machine) -> IndexMap<String, OutsideAddress> {
63    m.networks
64        .iter()
65        .filter_map(|(net, octet)| {
66            let n = spec.networks.get(net).filter(|n| n.is_outside())?;
67            let a = if n.is_dhcp() {
68                OutsideAddress::Dhcp
69            } else {
70                OutsideAddress::Fixed(address(spec, net, *octet))
71            };
72            Some((net.clone(), a))
73        })
74        .collect()
75}
76
77/// The spec without its outside networks, as the rest of the plan and the backends read it:
78/// the networks gone, and gone from each machine's list.
79#[must_use]
80pub fn without(spec: &Spec) -> Spec {
81    let mut s = spec.clone();
82    s.networks.retain(|_, n| !n.is_outside());
83    for m in s.machines.values_mut() {
84        m.networks.retain(|net, _| spec.networks.get(net).is_none_or(|n| !n.is_outside()));
85    }
86    s
87}
88
89/// What the outside networks ask of a backend: joining an existing network, bridging a host
90/// interface. At most one of each, the first network that asks.
91#[must_use]
92pub fn requirements(spec: &Spec) -> Vec<Requirement> {
93    let mut out = Vec::new();
94    if let Some((network, n)) = spec.networks.iter().find(|(_, n)| n.external.is_some()) {
95        out.push(Requirement::ExternalNetwork {
96            network: network.clone(),
97            name: n.external.clone().unwrap_or_default(),
98        });
99    }
100    if let Some((network, _)) = spec.networks.iter().find(|(_, n)| n.external.is_none() && n.bridged.is_some()) {
101        out.push(Requirement::BridgedNetwork { network: network.clone() });
102    }
103    out
104}
105
106/// Why a target that makes every network it uses refuses existing network `network` (`name`).
107pub(crate) fn no_existing_network(f: &mut fmt::Formatter<'_>, network: &str, name: &str) -> fmt::Result {
108    write!(
109        f,
110        "network `{network}` is an existing network (`external: {name}`), and this target makes every network it uses (`docker`, `hosted` and `proxmox` join existing ones)"
111    )
112}
113
114/// Why a target that bridges nothing refuses bridged network `network`.
115pub(crate) fn no_bridge(f: &mut fmt::Formatter<'_>, network: &str) -> fmt::Result {
116    write!(
117        f,
118        "network `{network}` is bridged to a host interface (`bridged`), which this target has no way to do (`vagrant` and `proxmox` bridge one)"
119    )
120}
121
122#[cfg(test)]
123mod tests {
124    use isoloom_spec::model::Target;
125
126    use super::*;
127    use crate::{Feature, testing};
128
129    const SPEC: &str = "
130version: 1
131name: t
132networks:
133  lan: { cidr: 10.20.0.0/24 }
134  corp: { cidr: 192.168.50.0/24 }
135  home: { cidr: 10.0.0.0/24, bridged: { interface: en0, dhcp: true } }
136machines:
137  a: { networks: { lan: 10, corp: 20, home: 0 }, app: { image: nginx } }
138overrides:
139  docker: { networks: { corp: { external: corp-net } } }
140";
141
142    #[test]
143    fn outside_networks_stay_apart_from_the_environment_s_own() {
144        let env = testing::planned(&testing::valid(SPEC), Target::Docker);
145        assert_eq!(env.networks.keys().collect::<Vec<_>>(), ["lan"]);
146        assert_eq!(env.spec().networks.len(), 1);
147        assert_eq!(env.outside["corp"].join, Join::Existing("corp-net".into()));
148        // The existing network keeps its own block on Docker.
149        assert_eq!(env.outside["corp"].cidr, "192.168.50.0/24");
150        let a = &env.machines["a"];
151        assert_eq!(a.addresses.keys().collect::<Vec<_>>(), ["lan"]);
152        assert_eq!(a.outside["corp"], OutsideAddress::Fixed("192.168.50.20".parse().unwrap()));
153        assert_eq!(a.outside["home"], OutsideAddress::Dhcp);
154        let features: Vec<Feature> = env.requirements.iter().map(Requirement::feature).collect();
155        assert!(features.contains(&Feature::ExternalNetworks) && features.contains(&Feature::BridgedNetworks));
156    }
157}