use isoloom_spec::images;
use isoloom_spec::model::{Filesystem, Machine, Spec};
use crate::requirements::Requirement;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Lifetime {
Environment,
Persistent,
External {
name: String,
},
}
#[must_use]
pub fn lifetime(spec: &Spec, volume: &str) -> Lifetime {
let Some(v) = spec.volumes.get(volume) else {
return Lifetime::Environment;
};
match &v.external {
Some(name) => Lifetime::External { name: name.clone() },
None if v.persistent => Lifetime::Persistent,
None => Lifetime::Environment,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataDisk {
pub name: String,
pub key: String,
pub size_gb: u32,
pub fs: Filesystem,
pub mount: Option<String>,
}
impl DataDisk {
#[must_use]
pub fn is_raw(&self) -> bool {
self.fs == Filesystem::Raw
}
#[must_use]
pub fn bytes(&self) -> u64 {
u64::from(self.size_gb) << 30
}
}
#[must_use]
pub fn disks(name: &str, m: &Machine) -> Vec<DataDisk> {
let windows = m.vm.as_ref().is_some_and(|vm| images::is_windows(&vm.os));
let default = if windows { Filesystem::Ntfs } else { Filesystem::Ext4 };
m.disks
.iter()
.map(|(d, disk)| DataDisk {
name: d.clone(),
key: format!("{name}-{d}"),
size_gb: disk.size_gb,
fs: disk.fs.unwrap_or(default),
mount: disk.mount.clone(),
})
.collect()
}
#[must_use]
pub fn requirements(spec: &Spec) -> Vec<Requirement> {
let mut out = Vec::new();
if let Some((v, _)) = spec.volumes.iter().find(|(_, v)| v.persistent) {
out.push(Requirement::PersistentVolume { volume: v.clone() });
}
if let Some((v, _)) = spec.volumes.iter().find(|(_, v)| v.external.is_some()) {
out.push(Requirement::ExternalVolume { volume: v.clone() });
}
if let Some((m, _)) = spec.machines.iter().find(|(_, m)| !m.disks.is_empty()) {
out.push(Requirement::DataDisks { machine: m.clone() });
}
let raw = |m: &Machine| m.disks.values().any(|d| d.fs == Some(Filesystem::Raw));
if let Some((m, _)) = spec.machines.iter().find(|(_, m)| raw(m)) {
out.push(Requirement::RawDisks { machine: m.clone() });
}
if let Some((m, _)) = spec.machines.iter().find(|(_, m)| m.tmpfs.iter().any(|t| t.mode().is_some())) {
out.push(Requirement::TmpfsModes { machine: m.clone() });
}
out
}
pub(crate) fn no_persistent_volume(f: &mut std::fmt::Formatter<'_>, volume: &str) -> std::fmt::Result {
write!(
f,
"volume `{volume}` is persistent: it outlives `down` on Docker and Kubernetes, while this target's storage goes with its machines"
)
}
pub(crate) fn no_existing_volume(f: &mut std::fmt::Formatter<'_>, volume: &str) -> std::fmt::Result {
write!(
f,
"volume `{volume}` is an existing one (`external`): Docker and Kubernetes mount one by name, this target has none to mount"
)
}
pub(crate) fn no_data_disks(f: &mut std::fmt::Formatter<'_>, machine: &str) -> std::fmt::Result {
write!(f, "machine `{machine}` has data disks (`disks`), which this target can't attach")
}
pub(crate) fn no_raw_disks(f: &mut std::fmt::Formatter<'_>, machine: &str) -> std::fmt::Result {
write!(
f,
"machine `{machine}` has a raw disk (`fs: raw`): a block device, which this target's containers don't get"
)
}
pub(crate) fn no_tmpfs_modes(f: &mut std::fmt::Formatter<'_>, machine: &str) -> std::fmt::Result {
write!(f, "machine `{machine}` sets a tmpfs `mode`: Kubernetes' memory volumes have none")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_volume_lives_with_the_environment_past_it_or_already_exists() {
let spec = crate::testing::lab(
"version: 1\nname: t\nvolumes: { keep: { persistent: true }, old: {}, tmp: {} }\nmachines:\n m: { app: { image: x }, volumes: { keep: /k, old: /o, tmp: /t, own: /w }, tmpfs: [{ path: /run/x, mode: '1777' }] }\noverrides: { docker: { volumes: { old: { external: data-1 } } } }\n",
)
.for_target(isoloom_spec::model::Target::Docker);
assert_eq!(lifetime(&spec, "keep"), Lifetime::Persistent);
assert_eq!(lifetime(&spec, "old"), Lifetime::External { name: "data-1".into() });
assert_eq!(lifetime(&spec, "tmp"), Lifetime::Environment);
assert_eq!(lifetime(&spec, "own"), Lifetime::Environment);
assert_eq!(
requirements(&spec),
[
Requirement::PersistentVolume { volume: "keep".into() },
Requirement::ExternalVolume { volume: "old".into() },
Requirement::TmpfsModes { machine: "m".into() }
]
);
}
#[test]
fn a_disk_takes_its_os_default_filesystem_and_a_key_of_its_machine() {
let spec = crate::testing::spec(
"version: 1\nname: t\nnetworks: { lan: { cidr: 10.9.0.0/24 } }\nmachines:\n db: { networks: { lan: 10 }, os: debian@12, disks: { data: { size_gb: 5, mount: /data }, blk: { size_gb: 1, fs: raw } } }\n win: { networks: { lan: 11 }, os: windows-server@2022, disks: { logs: { size_gb: 2, mount: 'E:' } } }\n",
);
let db = disks("db", &spec.machines["db"]);
assert_eq!((db[0].key.as_str(), db[0].fs, db[0].bytes()), ("db-data", Filesystem::Ext4, 5 << 30));
assert!(db[1].is_raw() && !db[0].is_raw());
assert_eq!(disks("win", &spec.machines["win"])[0].fs, Filesystem::Ntfs);
let kinds: Vec<_> = requirements(&spec).iter().map(Requirement::feature).collect();
assert_eq!(kinds, [crate::Feature::DataDisks, crate::Feature::RawDisks]);
let formatted =
crate::testing::spec("version: 1\nname: t\nmachines:\n db: { os: debian@12, disks: { data: { size_gb: 5, mount: /data, fs: xfs } } }\n");
assert_eq!(requirements(&formatted), [Requirement::DataDisks { machine: "db".into() }]);
}
}