Skip to main content

isoloom_spec/model/
shared.rs

1//! What every machine or several machines share: `common:`, groups, tools, external
2//! resources, shared volumes and ulimits.
3
4use indexmap::IndexMap;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use super::{Arch, Dns, Mount, Resources, ScalarSchema, Tmpfs, Virtiofs, VmImage};
9#[cfg(doc)]
10use super::{Machine, Spec};
11
12/// Fields several machines share, through `common:` or a group: everything about how a machine
13/// is built, nothing about what it is (its networks, ports and `access` stay its own).
14#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
15#[serde(deny_unknown_fields)]
16pub struct Shared {
17    /// The machine's CPU architecture, as `arch` on a machine.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub arch: Option<Arch>,
20    /// Kernel-level access, as `privileged` on a machine.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub privileged: Option<bool>,
23    /// A read-only root filesystem, as `read_only` on a machine.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub read_only: Option<bool>,
26    /// Memory-backed mount paths, as `tmpfs` on a machine.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub tmpfs: Option<Vec<Tmpfs>>,
29    /// The size of `/dev/shm`, as `shm_size` on a machine.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub shm_size: Option<String>,
32    /// Name resolution, as `dns` on a machine.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub dns: Option<Dns>,
35    /// Environment variables, as `environment` on a machine (merged key by key).
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    #[schemars(with = "Option<IndexMap<String, ScalarSchema>>")]
38    pub environment: Option<IndexMap<String, serde_json::Value>>,
39    /// Kernel parameters, as `sysctls` on a machine (merged key by key).
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    #[schemars(with = "Option<IndexMap<String, ScalarSchema>>")]
42    pub sysctls: Option<IndexMap<String, serde_json::Value>>,
43    /// Process limits, as `ulimits` on a machine (merged key by key).
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub ulimits: Option<IndexMap<String, Ulimit>>,
46    /// The spec inputs the machines receive, as `inputs` on a machine.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub inputs: Option<Vec<String>>,
49    /// CPUs, memory and disk, as `resources` on a machine.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub resources: Option<Resources>,
52    /// Machines that must be ready first, as `depends_on` on a machine.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub depends_on: Option<Vec<String>>,
55    /// Data that survives restarts, as `volumes` on a machine (merged key by key).
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub volumes: Option<IndexMap<String, Mount>>,
58    /// Completes the `docker:` of machines that have one (image, build, init, idle).
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub docker: Option<SharedDocker>,
61    /// Completes the `vm:` of machines that have one (os, provision, image).
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub vm: Option<SharedVm>,
64}
65
66/// A shared `docker:` part: each field optional.
67#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
68#[serde(deny_unknown_fields)]
69pub struct SharedDocker {
70    /// A published image, as `docker.image` on a machine.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub image: Option<String>,
73    /// A build context in the project folder, as `docker.build` on a machine.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub build: Option<String>,
76    /// One-shot jobs run before the machine counts as ready, as `docker.init` on a machine.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub init: Option<Vec<String>>,
79    /// Keep a container without a service of its own running, as `docker.idle` on a machine.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub idle: Option<bool>,
82}
83
84/// A shared `vm:` part: each field optional.
85#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
86#[serde(deny_unknown_fields)]
87pub struct SharedVm {
88    /// An OS name, as `vm.os` on a machine.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub os: Option<String>,
91    /// Steps run inside the VM, as `vm.provision` on a machine.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub provision: Option<Vec<String>>,
94    /// Images per target instead of the OS name's, as `vm.image` on a machine.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub image: Option<VmImage>,
97    /// A virtual TPM 2.0, as `vm.tpm` on a machine.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub tpm: Option<bool>,
100}
101
102/// A group: the machines in it, and the fields they share.
103#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
104#[serde(deny_unknown_fields)]
105pub struct Group {
106    /// Machine names, globs (`ws*`), or other groups.
107    pub members: Vec<String>,
108    /// The fields the members share (a member's own value wins).
109    #[serde(flatten)]
110    pub shared: Shared,
111}
112
113/// A tool attached beside the environment. The built-in recipe `shell` (an empty block) is a
114/// toolbox on every network: tcpdump, nmap, curl, dig, netcat (a netshoot container on Docker, a
115/// Debian VM on local VMs). Any other name takes a container image (container targets).
116#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
117#[serde(deny_unknown_fields)]
118pub struct Tool {
119    /// A container image (needed unless the name is a built-in recipe).
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub image: Option<String>,
122    /// The command to run, when the image's own isn't right.
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub command: Vec<String>,
125    /// A port the tool serves (its web UI), reachable from every network.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub port: Option<u16>,
128    /// The port on the host's loopback (`127.0.0.1`) that forwards to the tool's `port`, so a
129    /// browser on the host can open its web UI.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub publish: Option<u16>,
132}
133
134/// Built-in tool recipes.
135pub const TOOL_RECIPES: &[&str] = &["shell"];
136
137/// An existing machine's SSH endpoint, for the `external` target.
138#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
139#[serde(deny_unknown_fields)]
140pub struct External {
141    /// An address or hostname reachable from where Isoloom runs.
142    pub address: String,
143    /// The SSH user (default `root`).
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub user: Option<String>,
146    /// The SSH port (default 22).
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub port: Option<u16>,
149    /// A private key file (default: the SSH agent and config).
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub key: Option<String>,
152}
153
154/// A volume of the environment, mounted by one machine or several (see [`Spec::volumes`]): one
155/// named volume on Docker, one claim on Kubernetes (its users on its owner's node), and on VMs
156/// the owner's directory served over NFS to the others, mounted at their own paths. Declared
157/// here, a volume can also outlive the environment (`persistent`) or be one that exists already
158/// (`external`).
159#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
160#[serde(deny_unknown_fields)]
161pub struct SharedVolume {
162    /// The machine that holds the data (serves it over NFS on the VM targets). Default: the
163    /// first machine that mounts it.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub owner: Option<String>,
166    /// Keep its data when the environment is torn down: `isothrum down` leaves it, and the next
167    /// `isothrum up` finds it again (remove it by hand). Docker and Kubernetes; a VM's storage
168    /// goes with the VM.
169    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
170    pub persistent: bool,
171    /// An existing volume, by its name on the platform (a Docker volume, a Kubernetes claim in
172    /// the environment's namespace): Isoloom mounts it and never creates or removes it.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    #[schemars(skip)] // lowering fills it from `overrides:`; a spec never writes it here.
175    pub external: Option<String>,
176    /// On Proxmox, the volume as a folder of the server, shared into each VM that mounts it
177    /// (virtiofs) instead of over NFS from its owner: `virtiofs: {}` takes the defaults. Every
178    /// other target ignores it.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    #[schemars(skip)] // lowering fills it from `overrides:`; a spec never writes it here.
181    pub virtiofs: Option<Virtiofs>,
182}
183
184/// A process limit (see [`Machine::ulimits`]): one value for the soft and hard limits, or both.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
186#[serde(untagged)]
187pub enum Ulimit {
188    /// One value for both the soft and the hard limit.
189    Both(u64),
190    /// The soft limit (what a process gets) and the hard one (how far it may raise it).
191    Pair {
192        /// The limit a process starts with.
193        soft: u64,
194        /// The ceiling a process may raise its soft limit to.
195        hard: u64,
196    },
197}
198
199impl Ulimit {
200    /// The limit a process starts with.
201    #[must_use]
202    pub fn soft(self) -> u64 {
203        match self {
204            Ulimit::Both(n) | Ulimit::Pair { soft: n, .. } => n,
205        }
206    }
207
208    /// The ceiling a process may raise its soft limit to.
209    #[must_use]
210    pub fn hard(self) -> u64 {
211        match self {
212            Ulimit::Both(n) | Ulimit::Pair { hard: n, .. } => n,
213        }
214    }
215}
216
217/// The process limits Linux knows (`ulimit`, `limits.conf`, systemd's `Limit*`).
218pub const ULIMITS: &[&str] = &[
219    "core",
220    "cpu",
221    "data",
222    "fsize",
223    "locks",
224    "memlock",
225    "msgqueue",
226    "nice",
227    "nofile",
228    "nproc",
229    "rss",
230    "rtprio",
231    "rttime",
232    "sigpending",
233    "stack",
234];
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn a_ulimit_is_one_value_for_both_or_a_soft_and_a_hard_one() -> Result<(), serde_yaml_ng::Error> {
242        let limits = |yaml| serde_yaml_ng::from_str::<Ulimit>(yaml).map(|u| (u.soft(), u.hard()));
243        assert_eq!(limits("65536")?, (65_536, 65_536));
244        assert_eq!(limits("{ soft: 1024, hard: 4096 }")?, (1024, 4096));
245        Ok(())
246    }
247}