1use isoloom_spec::images;
6use isoloom_spec::model::{Filesystem, Machine, Spec};
7
8use crate::requirements::Requirement;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Lifetime {
13 Environment,
15 Persistent,
17 External {
19 name: String,
21 },
22}
23
24#[must_use]
27pub fn lifetime(spec: &Spec, volume: &str) -> Lifetime {
28 let Some(v) = spec.volumes.get(volume) else {
29 return Lifetime::Environment;
30 };
31 match &v.external {
32 Some(name) => Lifetime::External { name: name.clone() },
33 None if v.persistent => Lifetime::Persistent,
34 None => Lifetime::Environment,
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct DataDisk {
41 pub name: String,
43 pub key: String,
45 pub size_gb: u32,
47 pub fs: Filesystem,
49 pub mount: Option<String>,
51}
52
53impl DataDisk {
54 #[must_use]
56 pub fn is_raw(&self) -> bool {
57 self.fs == Filesystem::Raw
58 }
59
60 #[must_use]
62 pub fn bytes(&self) -> u64 {
63 u64::from(self.size_gb) << 30
64 }
65}
66
67#[must_use]
69pub fn disks(name: &str, m: &Machine) -> Vec<DataDisk> {
70 let windows = m.vm.as_ref().is_some_and(|vm| images::is_windows(&vm.os));
71 let default = if windows { Filesystem::Ntfs } else { Filesystem::Ext4 };
72 m.disks
73 .iter()
74 .map(|(d, disk)| DataDisk {
75 name: d.clone(),
76 key: format!("{name}-{d}"),
77 size_gb: disk.size_gb,
78 fs: disk.fs.unwrap_or(default),
79 mount: disk.mount.clone(),
80 })
81 .collect()
82}
83
84#[must_use]
87pub fn requirements(spec: &Spec) -> Vec<Requirement> {
88 let mut out = Vec::new();
89 if let Some((v, _)) = spec.volumes.iter().find(|(_, v)| v.persistent) {
90 out.push(Requirement::PersistentVolume { volume: v.clone() });
91 }
92 if let Some((v, _)) = spec.volumes.iter().find(|(_, v)| v.external.is_some()) {
93 out.push(Requirement::ExternalVolume { volume: v.clone() });
94 }
95 if let Some((m, _)) = spec.machines.iter().find(|(_, m)| !m.disks.is_empty()) {
96 out.push(Requirement::DataDisks { machine: m.clone() });
97 }
98 let raw = |m: &Machine| m.disks.values().any(|d| d.fs == Some(Filesystem::Raw));
99 if let Some((m, _)) = spec.machines.iter().find(|(_, m)| raw(m)) {
100 out.push(Requirement::RawDisks { machine: m.clone() });
101 }
102 if let Some((m, _)) = spec.machines.iter().find(|(_, m)| m.tmpfs.iter().any(|t| t.mode().is_some())) {
103 out.push(Requirement::TmpfsModes { machine: m.clone() });
104 }
105 out
106}
107
108pub(crate) fn no_persistent_volume(f: &mut std::fmt::Formatter<'_>, volume: &str) -> std::fmt::Result {
110 write!(
111 f,
112 "volume `{volume}` is persistent: it outlives `down` on Docker and Kubernetes, while this target's storage goes with its machines"
113 )
114}
115
116pub(crate) fn no_existing_volume(f: &mut std::fmt::Formatter<'_>, volume: &str) -> std::fmt::Result {
118 write!(
119 f,
120 "volume `{volume}` is an existing one (`external`): Docker and Kubernetes mount one by name, this target has none to mount"
121 )
122}
123
124pub(crate) fn no_data_disks(f: &mut std::fmt::Formatter<'_>, machine: &str) -> std::fmt::Result {
126 write!(f, "machine `{machine}` has data disks (`disks`), which this target can't attach")
127}
128
129pub(crate) fn no_raw_disks(f: &mut std::fmt::Formatter<'_>, machine: &str) -> std::fmt::Result {
131 write!(
132 f,
133 "machine `{machine}` has a raw disk (`fs: raw`): a block device, which this target's containers don't get"
134 )
135}
136
137pub(crate) fn no_tmpfs_modes(f: &mut std::fmt::Formatter<'_>, machine: &str) -> std::fmt::Result {
139 write!(f, "machine `{machine}` sets a tmpfs `mode`: Kubernetes' memory volumes have none")
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn a_volume_lives_with_the_environment_past_it_or_already_exists() {
148 let spec = crate::testing::lab(
149 "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",
150 )
151 .for_target(isoloom_spec::model::Target::Docker);
152 assert_eq!(lifetime(&spec, "keep"), Lifetime::Persistent);
153 assert_eq!(lifetime(&spec, "old"), Lifetime::External { name: "data-1".into() });
154 assert_eq!(lifetime(&spec, "tmp"), Lifetime::Environment);
155 assert_eq!(lifetime(&spec, "own"), Lifetime::Environment);
156 assert_eq!(
157 requirements(&spec),
158 [
159 Requirement::PersistentVolume { volume: "keep".into() },
160 Requirement::ExternalVolume { volume: "old".into() },
161 Requirement::TmpfsModes { machine: "m".into() }
162 ]
163 );
164 }
165
166 #[test]
167 fn a_disk_takes_its_os_default_filesystem_and_a_key_of_its_machine() {
168 let spec = crate::testing::spec(
169 "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",
170 );
171 let db = disks("db", &spec.machines["db"]);
172 assert_eq!((db[0].key.as_str(), db[0].fs, db[0].bytes()), ("db-data", Filesystem::Ext4, 5 << 30));
173 assert!(db[1].is_raw() && !db[0].is_raw());
174 assert_eq!(disks("win", &spec.machines["win"])[0].fs, Filesystem::Ntfs);
175 let kinds: Vec<_> = requirements(&spec).iter().map(Requirement::feature).collect();
176 assert_eq!(kinds, [crate::Feature::DataDisks, crate::Feature::RawDisks]);
177 let formatted =
178 crate::testing::spec("version: 1\nname: t\nmachines:\n db: { os: debian@12, disks: { data: { size_gb: 5, mount: /data, fs: xfs } } }\n");
179 assert_eq!(requirements(&formatted), [Requirement::DataDisks { machine: "db".into() }]);
180 }
181}