use crate::model::{NetworkDocker, Spec};
use crate::validate::docker_cidrs;
use crate::validate::{Cidr, Cidr6};
use crate::{Error, Result};
pub const MAX: u8 = 99;
#[must_use]
pub fn output_dir(instance: Option<u8>) -> String {
match instance {
Some(n) => format!(".isoloom-{n}"),
None => ".isoloom".to_owned(),
}
}
pub fn apply(spec: &Spec, n: u8) -> Result<Spec> {
if n == 0 || n > MAX {
return Err(Error::InstanceNumber(n));
}
let mut copy = spec.clone();
copy.name = format!("{}-{n}", spec.name);
if copy.name.len() > 55 {
return Err(Error::InstanceName(copy.name));
}
for (name, block) in docker_cidrs(spec) {
let [_, second, third, fourth] = std::net::Ipv4Addr::from(block.base).octets();
let shifted = Cidr {
base: u32::from_be_bytes([10, second.wrapping_add(n), third, fourth]),
len: block.len,
};
if let Some(net) = copy.networks.get_mut(&name) {
net.docker = Some(NetworkDocker { cidr: shifted.to_string() });
}
}
shift_ipv6(&mut copy, n)?;
for (machine, m) in &mut copy.machines {
for port in &mut m.ports {
if let Some(published) = port.publish {
let shifted = u32::from(published) + 100 * u32::from(n);
let moved = u16::try_from(shifted).map_err(|_| Error::InstancePort {
machine: machine.clone(),
port: published,
instance: n,
})?;
port.publish = Some(moved);
}
}
}
Ok(copy)
}
fn shift_ipv6(copy: &mut Spec, n: u8) -> Result<()> {
for (name, net) in &mut copy.networks {
let Some(v6) = net.ipv6.as_mut() else { continue };
let Some(block) = Cidr6::parse(&v6.cidr) else { continue };
let moved = Cidr6 {
base: block.base.wrapping_add(u128::from(n) << 80),
};
if Cidr6::parse(&moved.to_string()).is_none() {
return Err(Error::InstanceIpv6 {
network: name.clone(),
cidr: v6.cidr.clone(),
instance: n,
});
}
v6.cidr = moved.to_string();
}
Ok(())
}
#[must_use]
pub fn relocate(path: &str, contents: &str, n: u8) -> (String, String) {
let dir = output_dir(Some(n));
let path = match path.strip_prefix(".isoloom/") {
Some(rest) => format!("{dir}/{rest}"),
None => path.to_owned(),
};
(path, contents.replace(".isoloom/", &format!("{dir}/")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{lab, model};
fn web(publish: u16) -> Result<Spec, String> {
model(&lab(&format!(
"machines:\n web:\n docker: {{ image: nginx }}\n networks: {{ lan: 10 }}\n ports: [{{ port: 80, publish: {publish} }}]\n"
)))
}
#[test]
fn an_instance_writes_to_its_own_folder() {
assert_eq!(output_dir(None), ".isoloom");
assert_eq!(output_dir(Some(3)), ".isoloom-3");
}
#[test]
fn instances_are_numbered_1_to_99() -> Result<(), String> {
let spec = web(8080)?;
for n in [0, 100] {
assert!(matches!(apply(&spec, n), Err(Error::InstanceNumber(m)) if m == n), "instance {n}");
}
for n in [1, 99] {
assert_eq!(apply(&spec, n).map(|s| s.name).ok(), Some(format!("t-{n}")), "instance {n}");
}
Ok(())
}
#[test]
fn a_name_with_its_suffix_holds_55_characters() -> Result<(), String> {
let mut spec = web(8080)?;
spec.name = "a".repeat(53);
assert_eq!(apply(&spec, 1).map(|s| s.name.len()).ok(), Some(55));
spec.name = "a".repeat(54);
assert!(matches!(apply(&spec, 1), Err(Error::InstanceName(name)) if name.len() == 56));
Ok(())
}
#[test]
fn docker_blocks_move_by_the_instance_in_the_second_octet() -> Result<(), String> {
let copy = apply(&web(8080)?, 2).map_err(|e| e.to_string())?;
assert_eq!(copy.networks["lan"].docker, Some(NetworkDocker { cidr: "10.2.0.0/24".into() }));
assert_eq!(copy.networks["lan"].cidr, "10.0.0.0/24", "the machines keep the addresses the spec writes");
Ok(())
}
#[test]
fn published_ports_move_100_per_instance() -> Result<(), String> {
let copy = apply(&web(8080)?, 3).map_err(|e| e.to_string())?;
assert_eq!(copy.machines["web"].ports[0].publish, Some(8380));
assert_eq!(copy.machines["web"].ports[0].port, 80, "the machine's own port stays");
assert!(matches!(
apply(&web(65_500)?, 1),
Err(Error::InstancePort { machine, port: 65_500, instance: 1 }) if machine == "web"
));
Ok(())
}
#[test]
fn ipv6_blocks_move_by_the_instance_in_the_third_group() -> Result<(), String> {
let spec = |cidr: &str| {
model(&format!(
"version: 1\nname: t\nnetworks:\n lan: {{ cidr: 10.0.0.0/24, ipv6: {{ cidr: \"{cidr}\" }} }}\nmachines: {{}}\n"
))
};
let copy = apply(&spec("fd00:20::/64")?, 2).map_err(|e| e.to_string())?;
assert_eq!(copy.networks["lan"].ipv6.as_ref().map(|v6| v6.cidr.as_str()), Some("fd00:20:2::/64"));
assert!(matches!(
apply(&spec("fdff:ffff:ffff::/64")?, 1),
Err(Error::InstanceIpv6 { network, instance: 1, .. }) if network == "lan"
));
Ok(())
}
#[test]
fn generated_files_and_the_paths_they_hold_move_to_the_instances_folder() {
assert_eq!(
relocate(".isoloom/compose.yml", "env_file: .isoloom/web.env", 2),
(".isoloom-2/compose.yml".to_owned(), "env_file: .isoloom-2/web.env".to_owned())
);
assert_eq!(relocate("README.md", "plain", 2), ("README.md".to_owned(), "plain".to_owned()));
}
}