1use 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#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct OutsideNetwork {
20 pub cidr: String,
22 pub join: Join,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Join {
29 Existing(String),
31 Bridged(Bridged),
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum OutsideAddress {
38 Fixed(Ipv4Addr),
40 Dhcp,
42}
43
44#[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#[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#[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#[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
106pub(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
114pub(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 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}