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