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
//! The isoloom format, version 1: what an environment looks like from outside (machines,
//! networks, who reaches whom, the ports that answer) and, per machine, how each kind of
//! target produces it (`docker:` and/or `vm:`).
mod boot;
mod bridged;
mod checks;
mod cloud;
mod configuration;
mod constraints;
mod hardware;
mod implementation;
mod ipv6;
mod machine;
mod machine_file;
mod network;
mod private_dns;
mod process;
mod secrets;
mod shared;
mod storage;
mod target;
mod virtiofs;
pub use boot::{Boot, BootDevice, Firmware, FirmwareFormat, NetworkBoot};
pub(crate) use checks::is_yaml_file;
pub use checks::{Check, Declared, Expect};
pub use cloud::{CloudProvider, CloudServices};
pub use configuration::Configuration;
pub use constraints::{Constraints, Mirrors, Proxy};
pub use hardware::{DEFAULT_GPU_DRIVER, Device, DeviceFull, DeviceInput, GpuCount, Gpus, GpusFull, GpusInput};
pub use implementation::{
Appliance, BaseImage, Controller, Dns, DockerImpl, Generated, KNOWN_OS, OwnProcess, Provision, REBOOT_STEP, VmImage, VmImpl, Winrm, is_reboot,
};
pub use ipv6::{Ipv6, Ipv6Addresses};
pub(crate) use machine::scalars;
pub use machine::{Arch, Machine, Octets, Port, Protocol, Resources, ScalarSchema, Seal};
pub use machine_file::{DEFAULT_FILE_MODE, MachineFile};
pub use network::{Bridged, Network, NetworkDocker, Reach, Tc, Vlan};
pub use private_dns::EnvironmentDns;
pub use process::{CommandLine, Healthcheck, HttpCheck, seconds};
pub use secrets::{MachineSecret, SECRETS_DIR, SecretMount, SecretSource, after_drive, is_drive_path, secret_variable};
pub use shared::{External, Group, Shared, SharedDocker, SharedVm, SharedVolume, TOOL_RECIPES, Tool, ULIMITS, Ulimit};
pub use storage::{Disk, Filesystem, Mount, MountPart, Tmpfs, TmpfsMount};
pub use target::{Shape, Target};
pub use virtiofs::{MAX_VIRTIOFS, Virtiofs, VirtiofsCache, mapping_id};
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// An environment spec. Field order in the file is kept (machines start in the order written).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Spec {
/// The version of the `isoloom.yml` format the file is written in: `1`, the only one so far.
pub version: u32,
/// The environment's id, in kebab case (`supplier-portal`): it names its containers, VMs,
/// networks and cloud resources, and the environment in `isoloom status`.
pub name: String,
/// The environment's networks, by name: each an address block the machines on it share.
#[serde(default)]
pub networks: IndexMap<String, Network>,
/// Allowed traffic between networks; everything else between networks is blocked.
#[serde(default)]
pub reach: Vec<Reach>,
/// The environment's names in a private DNS zone: off unless `private: true`. See
/// [`EnvironmentDns`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<EnvironmentDns>,
/// Names of the values a runner provides at launch (a token, a licence key): from the
/// environment of `isoloom run`, or a profile's `inputs`. A machine receives the ones its own
/// `inputs` lists. Never baked into images.
#[serde(default)]
pub inputs: Vec<String>,
/// The inputs the authored spec marks `sensitive: false` (an URL, a mode): shown in a runner's
/// events and logs. Every other input is masked. Set by lowering only.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[schemars(skip)]
pub plain_inputs: Vec<String>,
/// Secrets, by kebab-case name: where `isoloom run` reads each value on the runner (`env`,
/// `file`, `vault`, `sops`). A machine lists the ones it gets in its own `secrets`, as
/// files. Values never reach a generated file, the resolved snapshot or a log.
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub secrets: IndexMap<String, SecretSource>,
/// The environment's machines, by name, started in the order written (after their
/// `depends_on`).
#[serde(default)]
pub machines: IndexMap<String, Machine>,
/// An environment of cloud services rather than machines (IAM users and roles, functions,
/// buckets, queues...): a Terraform module in the project, applied into the user's own cloud
/// account. A spec has either `cloud:` or machines.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cloud: Option<CloudServices>,
/// Volumes shared between machines: a machine volume with one of these names is this
/// volume, wherever it mounts it. One declared here can also outlive `isoloom down`
/// (`persistent: true`) or be one that exists already (`external: <name>`).
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub volumes: IndexMap<String, SharedVolume>,
/// What the place it runs in imposes: an HTTP proxy, registry and package mirrors, time
/// servers. Usually set by a profile, one customer's network at a time.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constraints: Option<Constraints>,
/// Machine fields shared by every machine (a machine's own value wins). `docker:` and `vm:`
/// here only complete an implementation a machine declares itself.
#[serde(default)]
pub common: Option<Shared>,
/// Named sets of machines sharing fields (over `common:`, under the machine's own; the most
/// specific group wins). They also become Ansible inventory groups, and `isoloom exec`
/// targets.
#[serde(default)]
pub groups: IndexMap<String, Group>,
/// Provisioning across machines, once every machine is up: Ansible playbooks run from a
/// controller on the environment's networks, with an inventory Isoloom writes.
#[serde(default)]
pub provision: Vec<Provision>,
/// The controller Isoloom adds for `provision:` (and checks run from it): a small VM, sized
/// and boxed like a machine. By default 1 CPU, 512 MB and a minimal Alpine box on Vagrant.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub controller: Option<Controller>,
/// Black-box checks that prove the behavior on every target: scripts, or declared probes
/// (`http`, `tcp`, `exec`, `script`) run from a machine of the environment. Isoloom adds
/// derived checks of its own from `ports` and `reach`.
#[serde(default)]
pub checks: Vec<Check>,
/// Narrows the targets derived from the implementations (e.g. not tested on Proxmox yet).
#[serde(default)]
pub targets: Option<Vec<Target>>,
/// Shown once the environment is up (`isoloom run`, `isoloom message`): how to start, in
/// Markdown. `{{ machines.web.addresses.front }}`-style placeholders take any value of the
/// resolved snapshot (`isoloom inspect` shows the paths).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
/// Observers beside the environment, on every network, that aren't machines of it (not
/// in `reach`, `resources` or the derived checks): a toolbox to look from (`shell`), or any
/// container image.
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub tools: IndexMap<String, Tool>,
/// Base machine name -> its clones, for machines that had a `count`. Filled when parsing.
#[serde(skip)]
#[schemars(skip)]
pub clones: IndexMap<String, Vec<String>>,
}
impl Spec {
/// The private DNS zone's domain, when `dns.private` is on.
#[must_use]
pub fn private_domain(&self) -> Option<String> {
self.dns.as_ref().and_then(|d| d.private_domain(&self.name))
}
/// The storage behind machine `machine`'s volume `volume`: the shared volume's own name,
/// else one of its own (`<machine>-<volume>`).
#[must_use]
pub fn volume_key(&self, machine: &str, volume: &str) -> String {
if self.volumes.contains_key(volume) {
volume.to_owned()
} else {
format!("{machine}-{volume}")
}
}
/// The machines that mount shared volume `volume`, in spec order.
#[must_use]
pub fn volume_users(&self, volume: &str) -> Vec<&str> {
self.machines
.iter()
.filter(|(_, m)| m.volumes.contains_key(volume))
.map(|(n, _)| n.as_str())
.collect()
}
/// The machine holding shared volume `volume`: its `owner`, else its first user.
#[must_use]
pub fn volume_owner(&self, volume: &str) -> Option<&str> {
match self.volumes.get(volume)?.owner.as_deref() {
Some(o) => Some(o),
None => self.volume_users(volume).first().copied(),
}
}
/// The spec with `{{ cloud.outputs.<name> }}` filled in (checks and message), from a
/// deployed cloud-services environment's outputs.
#[must_use]
pub fn with_cloud_outputs(&self, outputs: &IndexMap<String, String>) -> Spec {
let fill = |t: &str| {
outputs
.iter()
.fold(t.to_owned(), |acc, (k, v)| acc.replace(&format!("{{{{ cloud.outputs.{k} }}}}"), v))
};
let mut s = self.clone();
for c in &mut s.checks {
match c {
Check::Script(p) => *p = fill(p),
Check::Declared(d) => {
for f in [&mut d.http, &mut d.tcp, &mut d.exec, &mut d.body, &mut d.contains].into_iter().flatten() {
*f = fill(f);
}
for v in d.headers.values_mut() {
*v = fill(v);
}
}
}
}
s.message = s.message.as_deref().map(fill);
s
}
/// The message of a cloud-services environment, its outputs filled in.
#[must_use]
pub fn cloud_message(&self, outputs: &IndexMap<String, String>) -> Option<String> {
self.with_cloud_outputs(outputs).message
}
}
/// Whether `value` is its type's default: a field left out when it holds it.
pub(crate) fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
#[cfg(test)]
mod tests;