Skip to main content

isoloom_spec/syntax/
machine.rs

1//! A machine: what it is (`os` plus a catalogue `service`, a custom `app`, or nothing more), where
2//! it sits on the networks, and the settings every form shares. It never says how a platform
3//! runs it; each target picks the form it uses.
4
5use std::fmt;
6
7use indexmap::IndexMap;
8use schemars::JsonSchema;
9use serde::Deserialize;
10use serde::de::{self, SeqAccess, Visitor};
11
12use super::app::App;
13use super::files::File;
14use super::hardware::Hardware;
15use super::names::{PortName, ServiceRef};
16use super::os::OsRef;
17use super::ports::Port;
18use super::process::{Command, EnvValue, Healthcheck};
19use crate::Error;
20use crate::model::{Disk, Dns, Mount, Resources, ScalarSchema, Seal, Tmpfs, Ulimit};
21
22/// What a machine is, besides its OS.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Kind {
25    /// A catalogue service (or several), with its settings.
26    Service(Services),
27    /// A custom app: an image, a build, an appliance or portable steps.
28    App(App),
29    /// The OS alone: its official image kept running idle, or a fresh VM.
30    Bare,
31}
32
33/// Catalogue services on a machine and their settings.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Services {
36    /// The services, each `name[@version]`.
37    pub services: Vec<ServiceRef>,
38    /// Their settings (`with:`), as written.
39    pub with: IndexMap<String, serde_json::Value>,
40}
41
42/// A machine's address on a network: one last octet, or several (the first the primary one).
43#[derive(Debug, Clone, PartialEq, Eq, Deserialize, JsonSchema)]
44#[serde(untagged)]
45pub enum Octets {
46    /// The last octet of its address (0: an address from the LAN's DHCP server, on a bridged
47    /// network that hands them out).
48    One(u8),
49    /// Several addresses on the network, by last octet: the first is its primary address.
50    Several(Vec<u8>),
51}
52
53/// A machine of the environment.
54#[derive(Debug, Clone, PartialEq, Deserialize)]
55#[serde(try_from = "MachineInput")]
56pub struct Machine {
57    /// Its OS (`None`: a container-only app, or an access machine the runner supplies).
58    pub os: Option<OsRef>,
59    /// What it is.
60    pub kind: Kind,
61    /// Network name → its address on it.
62    pub networks: IndexMap<String, Octets>,
63    /// More DNS names it answers to.
64    pub aliases: Vec<String>,
65    /// The machine the user lands on.
66    pub access: bool,
67    /// Its ports, by name.
68    pub ports: IndexMap<PortName, Port>,
69    /// Its own command.
70    pub command: Option<Command>,
71    /// Project scripts run once it is up.
72    pub setup: Vec<String>,
73    /// When it counts as healthy beyond its ports.
74    pub healthcheck: Option<Healthcheck>,
75    /// Machines that must be ready first.
76    pub depends_on: Vec<String>,
77    /// Its environment variables.
78    pub environment: IndexMap<String, EnvValue>,
79    /// Files placed on it.
80    pub files: Vec<File>,
81    /// Its volumes: name → where it mounts it.
82    pub volumes: IndexMap<String, Mount>,
83    /// Its data disks.
84    pub disks: IndexMap<String, Disk>,
85    /// Its memory-backed mounts.
86    pub tmpfs: Vec<Tmpfs>,
87    /// Its CPUs, memory and disk.
88    pub resources: Option<Resources>,
89    /// The hardware it asks for.
90    pub hardware: Hardware,
91    /// How it resolves names.
92    pub dns: Option<Dns>,
93    /// Kernel-level access.
94    pub privileged: bool,
95    /// How strongly its `internet: false` seal holds.
96    pub seal: Seal,
97    /// A read-only root filesystem.
98    pub read_only: bool,
99    /// The size of `/dev/shm`.
100    pub shm_size: Option<String>,
101    /// Kernel parameters.
102    pub sysctls: IndexMap<String, String>,
103    /// Process limits.
104    pub ulimits: IndexMap<String, Ulimit>,
105    /// A bandwidth cap on each of its links.
106    pub rate_limit: Option<String>,
107}
108
109/// A machine as written.
110#[derive(Deserialize, JsonSchema)]
111#[serde(deny_unknown_fields)]
112pub(crate) struct MachineInput {
113    /// The OS, as `<family>@<version>` (`debian@12`, `windows-server@2022`), or the family alone
114    /// for its pinned default. Required, except for a machine whose only recipe is `app.image`,
115    /// `app.build` or `app.appliance`, and an `access` machine the runner supplies. Windows has
116    /// no container form in 1.0.
117    #[serde(default)]
118    os: Option<OsRef>,
119    /// A catalogue service, `name[@version]` (`postgres@16`), or a list of them. Not with `app`.
120    #[serde(default, deserialize_with = "services")]
121    #[schemars(with = "Option<ServicesInput>")]
122    service: Option<Vec<ServiceRef>>,
123    /// The service's settings, from its entry's settings schema. Only with `service`.
124    #[serde(default)]
125    with: IndexMap<String, serde_json::Value>,
126    /// A custom app: its own image, its own Dockerfile, a network appliance, or portable steps.
127    /// Not with `service`.
128    #[serde(default)]
129    app: Option<App>,
130    /// Network name → the last octet of the machine's address on it, or a list of them (the
131    /// first is its primary address). Every machine has one, but a LAN's `switch`.
132    #[serde(default)]
133    networks: IndexMap<String, Octets>,
134    /// More DNS names it answers to besides its own (`api.example.com`), on every network.
135    #[serde(default)]
136    aliases: Vec<String>,
137    /// How many of this machine (2 to 99): clones `<name>-01`, `<name>-02`, ..., each one address
138    /// further along on every network. Where the spec names this machine, it means every clone.
139    #[serde(default, rename = "count")]
140    _count: Option<u8>,
141    /// The machine the user lands on (at most one). Without `os`, the runner supplies it.
142    #[serde(default)]
143    access: bool,
144    /// Its ports, by name (`site`, `dns-udp`): a number (TCP), or `{ port, protocol, publish,
145    /// dormant }`. Messages and checks reach them as `machines.<m>.ports.<name>.published`.
146    #[serde(default)]
147    ports: IndexMap<PortName, Port>,
148    /// Its own command: a text (run by the shell), a list (run as is), or `{ run, cwd }`. Default:
149    /// the image's own; an `os`-only or steps-only machine stays up idle. Not with `service`.
150    #[serde(default)]
151    command: Option<Command>,
152    /// Project scripts run once, in order, once the machine is up (as root, with its network,
153    /// volumes, files and privileges): `.sh`, or `.ps1` on Windows. A machine is ready once they
154    /// have run.
155    #[serde(default)]
156    setup: Vec<String>,
157    /// When it counts as healthy beyond its ports answering: `{ test }` or `{ http: { port,
158    /// path } }` (a port's name or number), with `interval`, `timeout`, `retries` and
159    /// `start_period`.
160    #[serde(default)]
161    healthcheck: Option<Healthcheck>,
162    /// Machines that must be ready (ports answering, healthcheck passed, setup run) first.
163    #[serde(default)]
164    depends_on: Vec<String>,
165    /// Environment variables: a text, a number or a boolean, or `{ input: NAME }` for a
166    /// non-sensitive input. Secrets and sensitive inputs reach a machine as `files`.
167    #[serde(default)]
168    environment: IndexMap<String, EnvValue>,
169    /// Files placed on the machine, each from a project `source`, inline `content`, an `input` or a
170    /// `secret`, with its owner and mode.
171    #[serde(default)]
172    files: Vec<File>,
173    /// Data kept until the environment is destroyed: a name → an absolute path, or `{ path,
174    /// subpath }` for one folder of a shared volume.
175    #[serde(default)]
176    volumes: IndexMap<String, Mount>,
177    /// Data disks beside its own, by name: `{ size_gb, fs, mount }`.
178    #[serde(default)]
179    disks: IndexMap<String, Disk>,
180    /// Memory-backed mounts: a path, or `{ path, size, mode }`.
181    #[serde(default)]
182    tmpfs: Vec<Tmpfs>,
183    /// CPUs, memory and disk (default 1, 1024 MB, 20 GB).
184    #[serde(default)]
185    resources: Option<Resources>,
186    /// What it asks of the hardware: `arch`, `tpm`, `gpus`, `devices`, `nested_virtualization`,
187    /// `boot`.
188    #[serde(default)]
189    hardware: Hardware,
190    /// How it resolves names: `servers` (addresses, or machines sharing a network), `search`,
191    /// `domain`, `options`.
192    #[serde(default)]
193    dns: Option<Dns>,
194    /// Kernel-level access (nested containers, kernel modules, raw devices).
195    #[serde(default)]
196    privileged: bool,
197    /// How strongly its `internet: false` seal holds (`guaranteed` by default; `best-effort` lets a
198    /// privileged container sit on an offline network, acknowledging it can lift the seal).
199    #[serde(default)]
200    seal: Seal,
201    /// A read-only root filesystem (containers).
202    #[serde(default)]
203    read_only: bool,
204    /// The size of `/dev/shm` (`256m`, `1g`).
205    #[serde(default)]
206    shm_size: Option<String>,
207    /// Kernel parameters (`net.core.somaxconn: 4096`).
208    #[serde(default, deserialize_with = "crate::model::scalars")]
209    #[schemars(with = "IndexMap<String, ScalarSchema>")]
210    sysctls: IndexMap<String, String>,
211    /// Process limits by name: one value, or `{ soft, hard }`.
212    #[serde(default)]
213    ulimits: IndexMap<String, Ulimit>,
214    /// A bandwidth cap on each of its links, as `tc` writes a rate (`10mbit`).
215    #[serde(default)]
216    rate_limit: Option<String>,
217}
218
219// What the schema shows and the reader returns.
220/// `service:` as written, for the schema.
221#[derive(JsonSchema)]
222#[serde(untagged)]
223enum ServicesInput {
224    /// One service.
225    One(ServiceRef),
226    /// Several services on one machine (no container form: one process per container).
227    Several(Vec<ServiceRef>),
228}
229
230impl<'de> Deserialize<'de> for ServicesInput {
231    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<ServicesInput, D::Error> {
232        struct V;
233        impl<'de> Visitor<'de> for V {
234            type Value = ServicesInput;
235            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
236                f.write_str("a catalogue service, `name[@version]`, or a list of them")
237            }
238            fn visit_str<E: de::Error>(self, v: &str) -> Result<ServicesInput, E> {
239                v.parse().map(ServicesInput::One).map_err(E::custom)
240            }
241            fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<ServicesInput, A::Error> {
242                Vec::<ServiceRef>::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(ServicesInput::Several)
243            }
244        }
245        d.deserialize_any(V)
246    }
247}
248
249fn services<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Vec<ServiceRef>>, D::Error> {
250    Ok(Some(match ServicesInput::deserialize(d)? {
251        ServicesInput::One(s) => vec![s],
252        ServicesInput::Several(s) => s,
253    }))
254}
255
256impl TryFrom<MachineInput> for Machine {
257    type Error = Error;
258
259    fn try_from(m: MachineInput) -> Result<Machine, Error> {
260        let kind = match (m.service, m.app) {
261            (Some(_), Some(_)) => return Err(Error::ServiceAndApp),
262            (Some(services), None) => Kind::Service(Services { services, with: m.with }),
263            (None, _) if !m.with.is_empty() => return Err(Error::WithWithoutService),
264            (None, Some(app)) => Kind::App(app),
265            (None, None) => Kind::Bare,
266        };
267        Ok(Machine {
268            os: m.os,
269            kind,
270            networks: m.networks,
271            aliases: m.aliases,
272            access: m.access,
273            ports: m.ports,
274            command: m.command,
275            setup: m.setup,
276            healthcheck: m.healthcheck,
277            depends_on: m.depends_on,
278            environment: m.environment,
279            files: m.files,
280            volumes: m.volumes,
281            disks: m.disks,
282            tmpfs: m.tmpfs,
283            resources: m.resources,
284            hardware: m.hardware,
285            dns: m.dns,
286            privileged: m.privileged,
287            seal: m.seal,
288            read_only: m.read_only,
289            shm_size: m.shm_size,
290            sysctls: m.sysctls,
291            ulimits: m.ulimits,
292            rate_limit: m.rate_limit,
293        })
294    }
295}
296
297impl Machine {
298    /// Its custom app, when it is one.
299    #[must_use]
300    pub fn app(&self) -> Option<&App> {
301        match &self.kind {
302            Kind::App(a) => Some(a),
303            _ => None,
304        }
305    }
306}
307
308impl JsonSchema for Machine {
309    fn schema_name() -> std::borrow::Cow<'static, str> {
310        "Machine".into()
311    }
312
313    fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
314        MachineInput::json_schema(g)
315    }
316}
317
318#[cfg(test)]
319mod tests;