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;