Skip to main content

isoloom_spec/syntax/
overrides.rs

1//! `overrides:`: per-target facts with no portable meaning (a Docker block, a Vagrant box, a
2//! Proxmox LXC container, a cloud image). Each target has a closed set of fields; anything else
3//! is an error, and an override nothing on its target applies is refused, never ignored.
4
5use indexmap::IndexMap;
6use schemars::JsonSchema;
7use serde::Deserialize;
8
9use crate::model::{External, Target, Virtiofs, Winrm};
10
11/// The overrides, by target.
12#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
13#[serde(deny_unknown_fields)]
14pub struct Overrides {
15    /// Every target that runs the Compose file: `docker`, `hosted`, `docker-vm`, `cloud-docker`.
16    #[serde(default)]
17    pub docker: Option<DockerOverrides>,
18    /// The `kubernetes` target.
19    #[serde(default)]
20    pub kubernetes: Option<KubernetesOverrides>,
21    /// The `vagrant` target, and the VMs of `hybrid`.
22    #[serde(default)]
23    pub vagrant: Option<VagrantOverrides>,
24    /// The `proxmox` target.
25    #[serde(default)]
26    pub proxmox: Option<ProxmoxOverrides>,
27    /// The `esxi` target.
28    #[serde(default)]
29    pub esxi: Option<EsxiOverrides>,
30    /// `cloud-vm` on AWS.
31    #[serde(default)]
32    pub aws: Option<CloudOverrides>,
33    /// `cloud-vm` on Azure.
34    #[serde(default)]
35    pub azure: Option<CloudOverrides>,
36    /// `cloud-vm` on Google Cloud.
37    #[serde(default)]
38    pub gcp: Option<CloudOverrides>,
39    /// `cloud-vm` on DigitalOcean.
40    #[serde(default)]
41    pub digitalocean: Option<CloudOverrides>,
42    /// `cloud-vm` on Linode.
43    #[serde(default)]
44    pub linode: Option<CloudOverrides>,
45    /// `cloud-vm` on Oracle Cloud.
46    #[serde(default)]
47    pub oci: Option<CloudOverrides>,
48    /// The `external` target: machines that already exist.
49    #[serde(default)]
50    pub external: Option<ExternalOverrides>,
51}
52
53/// On the targets that run the Compose file.
54#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
55#[serde(deny_unknown_fields)]
56pub struct DockerOverrides {
57    /// Networks by name: their block on Docker, or an existing Docker network.
58    #[serde(default)]
59    pub networks: IndexMap<String, DockerNetwork>,
60    /// Volumes by name: an existing Docker volume.
61    #[serde(default)]
62    pub volumes: IndexMap<String, ExternalVolume>,
63}
64
65/// A network on Docker.
66#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
67#[serde(deny_unknown_fields)]
68pub struct DockerNetwork {
69    /// The block to use on Docker instead of `cidr` (same size, inside 10.0.0.0/8). Without it,
70    /// a network outside 10.0.0.0/8 moves into it (Docker Desktop and home networks use
71    /// 172.16.0.0/12 and 192.168.0.0/16).
72    #[serde(default)]
73    pub cidr: Option<String>,
74    /// An existing Docker network, by name: the machines join it, and Isoloom never creates or
75    /// removes it.
76    #[serde(default)]
77    pub external: Option<String>,
78}
79
80/// An existing volume on the platform, by its name there.
81#[derive(Debug, Clone, PartialEq, Deserialize, JsonSchema)]
82#[serde(deny_unknown_fields)]
83pub struct ExternalVolume {
84    /// Its name on the platform (a Docker volume, a Kubernetes claim in the environment's
85    /// namespace): mounted, never created or removed.
86    pub external: String,
87}
88
89/// An existing network on the target, by its name there.
90#[derive(Debug, Clone, PartialEq, Deserialize, JsonSchema)]
91#[serde(deny_unknown_fields)]
92pub struct ExternalNetwork {
93    /// Its name on the target (a Proxmox bridge, an ESXi port group): the machines join it, and
94    /// Isoloom never creates or removes it.
95    pub external: String,
96}
97
98/// On Kubernetes.
99#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
100#[serde(deny_unknown_fields)]
101pub struct KubernetesOverrides {
102    /// Volumes by name: an existing claim.
103    #[serde(default)]
104    pub volumes: IndexMap<String, ExternalVolume>,
105}
106
107/// On Vagrant.
108#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
109#[serde(deny_unknown_fields)]
110pub struct VagrantOverrides {
111    /// Machines (or groups) by name: the box each boots.
112    #[serde(default)]
113    pub machines: IndexMap<String, VagrantMachine>,
114    /// The controller's box.
115    #[serde(default)]
116    pub controller: Option<VagrantBox>,
117}
118
119/// A machine's box on Vagrant.
120#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
121#[serde(deny_unknown_fields)]
122pub struct VagrantMachine {
123    /// The box (`StefanScherer/windows_2019`), instead of the built-in one for its OS.
124    #[serde(default, rename = "box")]
125    pub box_name: Option<String>,
126    /// The box version to pin.
127    #[serde(default)]
128    pub box_version: Option<String>,
129    /// Where Vagrant downloads the box from when it isn't on Vagrant Cloud: a URL, or a `.box`
130    /// file in the project.
131    #[serde(default)]
132    pub box_url: Option<String>,
133    /// The box server to look the box up on, instead of Vagrant Cloud.
134    #[serde(default)]
135    pub box_server_url: Option<String>,
136    /// Other boxes for the providers the box has no build for.
137    #[serde(default)]
138    pub boxes: Option<ProviderBoxes>,
139    /// How this Windows box answers WinRM: `plaintext` (HTTP on 5985, the default) or `ssl`
140    /// (HTTPS on 5986).
141    #[serde(default)]
142    pub winrm: Option<Winrm>,
143    /// This Windows box answers OpenSSH instead of WinRM. Not with `winrm`.
144    #[serde(default)]
145    pub winssh: bool,
146}
147
148/// Boxes for the providers the machine's box has no build for.
149#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
150#[serde(deny_unknown_fields)]
151pub struct ProviderBoxes {
152    /// The box the QEMU provider boots (libvirt format).
153    #[serde(default)]
154    pub qemu: Option<String>,
155    /// The box the Hyper-V provider boots.
156    #[serde(default)]
157    pub hyperv: Option<String>,
158}
159
160/// The controller's box on Vagrant.
161#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
162#[serde(deny_unknown_fields)]
163pub struct VagrantBox {
164    /// The box, instead of Isoloom's (a specific Ansible version, a mirror).
165    #[serde(default, rename = "box")]
166    pub box_name: Option<String>,
167    /// The box version to pin.
168    #[serde(default)]
169    pub box_version: Option<String>,
170}
171
172/// On Proxmox.
173#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
174#[serde(deny_unknown_fields)]
175pub struct ProxmoxOverrides {
176    /// Machines (or groups) by name: their image, or an LXC container.
177    #[serde(default)]
178    pub machines: IndexMap<String, ProxmoxMachine>,
179    /// Volumes by name: a folder of the server shared into each VM (virtiofs).
180    #[serde(default)]
181    pub volumes: IndexMap<String, ProxmoxVolume>,
182    /// Networks by name: an existing bridge.
183    #[serde(default)]
184    pub networks: IndexMap<String, ExternalNetwork>,
185}
186
187/// A machine on Proxmox.
188#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
189#[serde(deny_unknown_fields)]
190pub struct ProxmoxMachine {
191    /// The image: a cloud image's URL, or a template VM's id on the node.
192    #[serde(default)]
193    pub image: Option<String>,
194    /// Run its container image as an LXC container (Proxmox VE 9.1 or later) instead of a VM:
195    /// lighter, but no Docker runtime, healthcheck or named volumes.
196    #[serde(default)]
197    pub lxc: bool,
198}
199
200/// A volume on Proxmox.
201#[derive(Debug, Clone, PartialEq, Deserialize, JsonSchema)]
202#[serde(deny_unknown_fields)]
203pub struct ProxmoxVolume {
204    /// A folder of the server, shared into each VM that mounts the volume, instead of NFS from
205    /// its owner: `virtiofs: {}` takes the defaults.
206    pub virtiofs: Virtiofs,
207}
208
209/// On ESXi.
210#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
211#[serde(deny_unknown_fields)]
212pub struct EsxiOverrides {
213    /// Networks by name: an existing port group.
214    #[serde(default)]
215    pub networks: IndexMap<String, ExternalNetwork>,
216}
217
218/// On one cloud of `cloud-vm`.
219#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
220#[serde(deny_unknown_fields)]
221pub struct CloudOverrides {
222    /// Machines (or groups) by name: their image on this cloud.
223    #[serde(default)]
224    pub machines: IndexMap<String, CloudMachine>,
225}
226
227/// A machine's image on a cloud.
228#[derive(Debug, Clone, PartialEq, Deserialize, JsonSchema)]
229#[serde(deny_unknown_fields)]
230pub struct CloudMachine {
231    /// The image: an AMI id on AWS, a resource id on Azure, `projects/…/images/…` on Google
232    /// Cloud, a slug or id on DigitalOcean, `linode/…` on Linode, an OCID on Oracle Cloud.
233    pub image: String,
234}
235
236/// On the `external` target.
237#[derive(Debug, Clone, PartialEq, Default, Deserialize, JsonSchema)]
238#[serde(deny_unknown_fields)]
239pub struct ExternalOverrides {
240    /// Machines by name: where each already exists.
241    #[serde(default)]
242    pub machines: IndexMap<String, ExternalMachine>,
243}
244
245/// An existing machine.
246#[derive(Debug, Clone, PartialEq, Deserialize, JsonSchema)]
247#[serde(deny_unknown_fields)]
248pub struct ExternalMachine {
249    /// Its SSH endpoint: `{ address, user, port, key }`.
250    pub ssh: External,
251}
252
253impl Overrides {
254    /// The cloud overrides, each with its key.
255    #[must_use]
256    pub fn clouds(&self) -> [(&'static str, Option<&CloudOverrides>); 6] {
257        [
258            ("aws", self.aws.as_ref()),
259            ("azure", self.azure.as_ref()),
260            ("gcp", self.gcp.as_ref()),
261            ("digitalocean", self.digitalocean.as_ref()),
262            ("linode", self.linode.as_ref()),
263            ("oci", self.oci.as_ref()),
264        ]
265    }
266
267    /// The targets an override key applies to.
268    #[must_use]
269    pub fn targets_of(key: &str) -> &'static [Target] {
270        match key {
271            "docker" => &[Target::Docker, Target::Hosted, Target::DockerVm, Target::CloudDocker],
272            "kubernetes" => &[Target::Kubernetes],
273            "vagrant" => &[Target::Vagrant, Target::Hybrid],
274            "proxmox" => &[Target::Proxmox],
275            "esxi" => &[Target::Esxi],
276            "external" => &[Target::External],
277            "aws" | "azure" | "gcp" | "digitalocean" | "linode" | "oci" => &[Target::CloudVm],
278            _ => &[],
279        }
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn each_override_key_applies_to_its_targets() {
289        assert_eq!(
290            Overrides::targets_of("docker"),
291            [Target::Docker, Target::Hosted, Target::DockerVm, Target::CloudDocker]
292        );
293        assert_eq!(Overrides::targets_of("kubernetes"), [Target::Kubernetes]);
294        assert_eq!(Overrides::targets_of("vagrant"), [Target::Vagrant, Target::Hybrid]);
295        assert_eq!(Overrides::targets_of("proxmox"), [Target::Proxmox]);
296        assert_eq!(Overrides::targets_of("esxi"), [Target::Esxi]);
297        assert_eq!(Overrides::targets_of("external"), [Target::External]);
298        for cloud in ["aws", "azure", "gcp", "digitalocean", "linode", "oci"] {
299            assert_eq!(Overrides::targets_of(cloud), [Target::CloudVm], "{cloud}");
300        }
301        assert_eq!(Overrides::targets_of("podman"), []);
302    }
303
304    #[test]
305    fn each_target_has_its_own_fields() {
306        let o: Overrides = serde_yaml_ng::from_str(
307            "{ docker: { networks: { office: { cidr: 10.70.0.0/24 } } }, vagrant: { machines: { portal: { box: example/debian, box_version: '1' } } }, proxmox: { machines: { status: { lxc: true } }, volumes: { uploads: { virtiofs: {} } } }, aws: { machines: { portal: { image: ami-1 } } } }",
308        )
309        .unwrap();
310        assert_eq!(o.vagrant.as_ref().unwrap().machines["portal"].box_name.as_deref(), Some("example/debian"));
311        assert!(o.proxmox.as_ref().unwrap().machines["status"].lxc);
312        assert_eq!(o.clouds()[0].1.unwrap().machines["portal"].image, "ami-1");
313    }
314
315    #[test]
316    fn a_field_another_target_has_is_refused() {
317        let e = serde_yaml_ng::from_str::<Overrides>("{ docker: { machines: { web: { box: x } } } }").unwrap_err();
318        assert!(e.to_string().contains("unknown field `machines`"));
319        assert!(serde_yaml_ng::from_str::<Overrides>("{ podman: {} }").is_err());
320    }
321}