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