Skip to main content

isoloom_spec/
os.rs

1//! A machine's OS, written `<family>@<version>`:
2//!
3//! ```yaml
4//! machines:
5//!   web: { os: debian@12, ... }        # Debian 12, as a VM or its container image
6//!   dc01:
7//!     os: windows-server               # the family alone: its pinned default, 2022
8//!     app: { steps: [{ run: dc.ps1 }] }
9//! ```
10//!
11//! Those are the only spellings. Once the spec is lowered, each `vm.os` holds the image table's
12//! name for its OS (`debian@12` is `debian-12`, [`Os::name`]), the name every target reads. A
13//! version left out resolves to the family's pinned default ([`Family::default`]), recorded in
14//! the resolved snapshot.
15
16use crate::{Error, Result};
17
18/// An OS family: the versions Isoloom has images for, each with its image-table name, and the
19/// version an omitted one resolves to.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Family {
22    /// The family's name, the part before `@` (`debian`, `windows-server`).
23    pub name: &'static str,
24    /// Its versions, oldest first, each with the image-table name it maps to.
25    pub versions: &'static [(&'static str, &'static str)],
26    /// The version `os: <family>` resolves to: pinned, so an environment never changes under
27    /// its author when a newer version is added.
28    pub default: &'static str,
29}
30
31/// The OS families, with their versions and pinned defaults.
32pub const FAMILIES: &[Family] = &[
33    Family {
34        name: "debian",
35        versions: &[("11", "debian-11"), ("12", "debian-12"), ("13", "debian-13")],
36        default: "12",
37    },
38    Family {
39        name: "ubuntu",
40        versions: &[
41            ("14.04", "ubuntu-14.04"),
42            ("16.04", "ubuntu-16.04"),
43            ("18.04", "ubuntu-18.04"),
44            ("20.04", "ubuntu-20.04"),
45            ("22.04", "ubuntu-22.04"),
46            ("24.04", "ubuntu-24.04"),
47        ],
48        default: "24.04",
49    },
50    Family {
51        name: "rocky",
52        versions: &[("9", "rocky-9")],
53        default: "9",
54    },
55    Family {
56        name: "alma",
57        versions: &[("9", "almalinux-9")],
58        default: "9",
59    },
60    Family {
61        name: "alpine",
62        versions: &[("3.19", "alpine-3.19"), ("3.24", "alpine-3.24")],
63        default: "3.19",
64    },
65    Family {
66        name: "kali",
67        versions: &[("rolling", "kali")],
68        default: "rolling",
69    },
70    Family {
71        name: "fedora",
72        versions: &[("42", "fedora-42")],
73        default: "42",
74    },
75    Family {
76        name: "centos",
77        versions: &[("7", "centos-7")],
78        default: "7",
79    },
80    Family {
81        name: "windows",
82        versions: &[("10", "windows-10"), ("11", "windows-11")],
83        default: "11",
84    },
85    Family {
86        name: "windows-server",
87        versions: &[
88            ("2008r2", "windows-server-2008r2"),
89            ("2012r2", "windows-server-2012r2"),
90            ("2016", "windows-server-2016"),
91            ("2019", "windows-server-2019"),
92            ("2022", "windows-server-2022"),
93            ("2025", "windows-server-2025"),
94        ],
95        default: "2022",
96    },
97];
98
99/// An OS resolved: its family, its version (the default filled in) and its image-table name.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub struct Os {
102    /// The family (`debian`).
103    pub family: &'static str,
104    /// The version (`12`).
105    pub version: &'static str,
106    /// The image table's name for it (`debian-12`), what the targets read.
107    pub name: &'static str,
108}
109
110impl Os {
111    /// How the spec writes it in full: `debian@12`.
112    #[must_use]
113    pub fn id(&self) -> String {
114        format!("{}@{}", self.family, self.version)
115    }
116}
117
118impl Family {
119    /// The family's version `version`, resolved.
120    fn get(&self, version: &str) -> Option<Os> {
121        self.versions.iter().find(|(v, _)| *v == version).map(|(v, n)| Os {
122            family: self.name,
123            version: v,
124            name: n,
125        })
126    }
127
128    /// Its versions, as an error lists them (`11, 12, 13`).
129    fn known(&self) -> String {
130        self.versions.iter().map(|(v, _)| *v).collect::<Vec<_>>().join(", ")
131    }
132}
133
134/// Resolves an OS as a spec writes it: `<family>@<version>`, or `<family>` for its default
135/// version.
136///
137/// # Errors
138///
139/// [`Error::UnknownOs`] listing the known families, or [`Error::UnknownOsVersion`] listing the
140/// family's versions.
141pub fn resolve(text: &str) -> Result<Os> {
142    let (family, version) = text.split_once('@').map_or((text, None), |(f, v)| (f, Some(v)));
143    let Some(f) = FAMILIES.iter().find(|f| f.name == family) else {
144        return Err(Error::UnknownOs {
145            text: text.to_owned(),
146            known: FAMILIES.iter().map(|f| format!("{} ({})", f.name, f.known())).collect::<Vec<_>>().join(", "),
147        });
148    };
149    let version = version.unwrap_or(f.default);
150    f.get(version).ok_or_else(|| Error::UnknownOsVersion {
151        family: f.name,
152        version: version.to_owned(),
153        known: f.known(),
154        default: f.default,
155    })
156}
157
158/// The OS an image-table name (`debian-12`) stands for: how a parsed spec's `vm.os` names it.
159#[must_use]
160pub fn by_name(name: &str) -> Option<Os> {
161    FAMILIES
162        .iter()
163        .find_map(|f| f.versions.iter().find(|(_, n)| *n == name).and_then(|(v, _)| f.get(v)))
164}
165
166/// Every spelling an `os` field takes: each family alone and each `<family>@<version>`. The
167/// schema offers them for completion.
168#[must_use]
169pub fn spellings() -> Vec<String> {
170    let mut out: Vec<String> = Vec::new();
171    for f in FAMILIES {
172        out.push(f.name.to_owned());
173        out.extend(f.versions.iter().map(|(v, _)| format!("{}@{v}", f.name)));
174    }
175    out
176}
177
178mod containers;
179
180pub use containers::container_image;
181
182#[cfg(test)]
183mod tests;