1use std::collections::BTreeSet;
6use std::fmt;
7
8use indexmap::IndexMap;
9use isoloom_spec::model::Spec;
10use isoloom_spec::windows::Tpm;
11
12use crate::checks::{self, Position, Probe};
13use crate::environment::{MachinePlan, MachineShape};
14use crate::hardware::Need;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18#[non_exhaustive]
19pub enum Feature {
20 EnvironmentProvisioning,
22 PlaybookChecks,
24 ExecWithoutContainer,
26 NetworkAppliances,
28 Gateways,
30 VolumesAcrossShapes,
32 VmAfterContainer,
34 MachineAliases,
36 Ulimits,
38 TrafficControl,
40 Ipv6,
42 Ipv6Only,
44 Ipv6Auto,
46 Secrets,
48 SecretOwners,
50 ExternalNetworks,
52 BridgedNetworks,
54 OwnedProjectFiles,
56 ExistingHostFiles,
58 Tpm,
60 MachineProcess,
63 OwnBoot,
65 ContainerGpus,
67 ContainerDevices,
69 ContainerNestedVirtualization,
71 VmGpus,
73 VmDevices,
75 NestedVirtualization,
77 NetworkMtu,
79 RateLimit,
81 PrivateDns,
83 PersistentVolumes,
85 ExternalVolumes,
87 DataDisks,
89 RawDisks,
91 TmpfsModes,
93 VmMoreAddresses,
95 ContainerMoreAddresses,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum Requirement {
104 EnvironmentProvisioning,
106 PlaybookChecks,
108 ExecWithoutContainer {
110 check: String,
112 },
113 NetworkAppliance {
115 machine: String,
117 },
118 Gateway {
120 network: String,
122 },
123 VolumeAcrossShapes {
125 volume: String,
127 },
128 VmDependsOnContainer {
130 vm: String,
132 container: String,
134 },
135 MachineAliases,
137 Ulimits {
139 machine: String,
141 },
142 TrafficControl,
144 Ipv6 {
146 network: String,
148 },
149 Ipv6Only {
151 network: String,
153 },
154 Ipv6Auto {
156 network: String,
158 },
159 Secrets {
161 machine: String,
163 },
164 SecretOwner {
166 machine: String,
168 },
169 ExternalNetwork {
171 network: String,
173 name: String,
175 },
176 BridgedNetwork {
178 network: String,
180 },
181 OwnedProjectFile {
183 machine: String,
185 path: String,
187 },
188 ExistingHostFiles {
190 machine: String,
192 },
193 Tpm {
195 machine: String,
197 },
198 MachineProcess {
200 machine: String,
202 },
203 OwnBoot {
205 machine: String,
207 },
208 Hardware {
210 machine: String,
212 need: Need,
214 },
215 NetworkMtu {
217 network: String,
219 },
220 RateLimit {
222 machine: String,
224 },
225 PrivateDns {
227 domain: String,
229 },
230 PersistentVolume {
232 volume: String,
234 },
235 ExternalVolume {
237 volume: String,
239 },
240 DataDisks {
242 machine: String,
244 },
245 RawDisks {
247 machine: String,
249 },
250 TmpfsModes {
252 machine: String,
254 },
255 MoreAddresses {
257 machine: String,
259 network: String,
261 shape: MachineShape,
263 },
264}
265
266impl Requirement {
267 #[must_use]
269 pub fn feature(&self) -> Feature {
270 match self {
271 Requirement::EnvironmentProvisioning => Feature::EnvironmentProvisioning,
272 Requirement::PlaybookChecks => Feature::PlaybookChecks,
273 Requirement::ExecWithoutContainer { .. } => Feature::ExecWithoutContainer,
274 Requirement::NetworkAppliance { .. } => Feature::NetworkAppliances,
275 Requirement::Gateway { .. } => Feature::Gateways,
276 Requirement::VolumeAcrossShapes { .. } => Feature::VolumesAcrossShapes,
277 Requirement::VmDependsOnContainer { .. } => Feature::VmAfterContainer,
278 Requirement::MachineAliases => Feature::MachineAliases,
279 Requirement::Ulimits { .. } => Feature::Ulimits,
280 Requirement::TrafficControl => Feature::TrafficControl,
281 Requirement::Ipv6 { .. } => Feature::Ipv6,
282 Requirement::Ipv6Only { .. } => Feature::Ipv6Only,
283 Requirement::Ipv6Auto { .. } => Feature::Ipv6Auto,
284 Requirement::Secrets { .. } => Feature::Secrets,
285 Requirement::SecretOwner { .. } => Feature::SecretOwners,
286 Requirement::ExternalNetwork { .. } => Feature::ExternalNetworks,
287 Requirement::BridgedNetwork { .. } => Feature::BridgedNetworks,
288 Requirement::OwnedProjectFile { .. } => Feature::OwnedProjectFiles,
289 Requirement::ExistingHostFiles { .. } => Feature::ExistingHostFiles,
290 Requirement::Tpm { .. } => Feature::Tpm,
291 Requirement::MachineProcess { .. } => Feature::MachineProcess,
292 Requirement::OwnBoot { .. } => Feature::OwnBoot,
293 Requirement::Hardware { need, .. } => need.feature(),
294 Requirement::NetworkMtu { .. } => Feature::NetworkMtu,
295 Requirement::RateLimit { .. } => Feature::RateLimit,
296 Requirement::PrivateDns { .. } => Feature::PrivateDns,
297 Requirement::PersistentVolume { .. } => Feature::PersistentVolumes,
298 Requirement::ExternalVolume { .. } => Feature::ExternalVolumes,
299 Requirement::DataDisks { .. } => Feature::DataDisks,
300 Requirement::RawDisks { .. } => Feature::RawDisks,
301 Requirement::TmpfsModes { .. } => Feature::TmpfsModes,
302 Requirement::MoreAddresses { shape: MachineShape::Vm, .. } => Feature::VmMoreAddresses,
303 Requirement::MoreAddresses { .. } => Feature::ContainerMoreAddresses,
304 }
305 }
306}
307
308const OWN_PROCESS: &str = "sets its own process (`command` or `healthcheck`): an existing host runs what it already runs, Isoloom starts nothing on it";
311
312const IN_A_CONTAINER: &str = "needs `from:` a machine with its own container (it runs inside it)";
314
315impl fmt::Display for Requirement {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 match self {
319 Requirement::EnvironmentProvisioning => f.write_str("environment-level provisioning (`provision:`) runs on VM targets for now"),
320 Requirement::PlaybookChecks => f.write_str("Ansible checks (.yml) run on VM targets for now"),
321 Requirement::ExecWithoutContainer { check } => write!(f, "`exec` check \"{check}\" {IN_A_CONTAINER}"),
322 Requirement::NetworkAppliance { machine } => write!(
323 f,
324 "machine `{machine}` is a network appliance (`app.appliance`), which runs on the Docker targets"
325 ),
326 Requirement::Gateway { .. } => f.write_str("networks with a `gateway` machine in hybrid environments come later"),
327 Requirement::VolumeAcrossShapes { volume } => write!(
328 f,
329 "volume `{volume}` is shared by containers and VMs: one storage can't be both a Docker volume and an NFS export, for now"
330 ),
331 Requirement::VmDependsOnContainer { vm, container } => write!(
332 f,
333 "VM `{vm}` depends on container `{container}`: VMs start first in a hybrid environment, for now"
334 ),
335 Requirement::MachineAliases => f.write_str("machine `aliases` (dotted DNS names) aren't Kubernetes Service names"),
337 Requirement::Ulimits { machine } => write!(f, "machine `{machine}` sets `ulimits`: Kubernetes has no per-container process limits"),
338 Requirement::TrafficControl => f.write_str("link impairment (`tc`) needs a router in the path; Kubernetes has none"),
339 Requirement::Ipv6 { network } => write!(f, "network `{network}` is IPv6 (`ipv6`), which this target's networks don't carry yet"),
340 Requirement::Ipv6Only { network } => crate::ipv6::no_ipv6_only(f, network),
341 Requirement::Ipv6Auto { network } => crate::ipv6::no_auto_addresses(f, network),
342 Requirement::Secrets { machine } => crate::secrets::no_delivery(f, machine),
343 Requirement::SecretOwner { machine } => crate::secrets::no_owner(f, machine),
344 Requirement::ExternalNetwork { network, name } => crate::outside::no_existing_network(f, network, name),
345 Requirement::BridgedNetwork { network } => crate::outside::no_bridge(f, network),
346 Requirement::OwnedProjectFile { machine, path } => crate::machine_files::no_owned_project_file(f, machine, path),
347 Requirement::ExistingHostFiles { machine } => crate::machine_files::no_existing_host_files(f, machine),
348 Requirement::Tpm { machine } => no_tpm(f, machine),
349 Requirement::MachineProcess { machine } => write!(f, "machine `{machine}` {OWN_PROCESS}"),
350 Requirement::OwnBoot { machine } => crate::boot::no_own_boot(f, machine),
351 Requirement::Hardware { machine, need } => need.reason(machine, f),
352 Requirement::NetworkMtu { network } => crate::links::no_mtu(f, network),
353 Requirement::RateLimit { machine } => crate::links::no_rate_limit(f, machine),
354 Requirement::PrivateDns { domain } => write!(
357 f,
358 "the names are served under a private zone (`dns.private`, `{domain}`), and this target has no DNS or hosts file Isoloom sets: a cluster names Services under its own domain, an existing host keeps its own resolver"
359 ),
360 Requirement::MoreAddresses { machine, network, shape } => crate::more_addresses::refused(f, machine, network, *shape),
361 Requirement::PersistentVolume { volume } => crate::storage::no_persistent_volume(f, volume),
362 Requirement::ExternalVolume { volume } => crate::storage::no_existing_volume(f, volume),
363 Requirement::DataDisks { machine } => crate::storage::no_data_disks(f, machine),
364 Requirement::RawDisks { machine } => crate::storage::no_raw_disks(f, machine),
365 Requirement::TmpfsModes { machine } => crate::storage::no_tmpfs_modes(f, machine),
366 }
367 }
368}
369
370fn no_tpm(f: &mut fmt::Formatter<'_>, machine: &str) -> fmt::Result {
372 write!(
373 f,
374 "machine `{machine}` asks for a virtual TPM (`hardware.tpm: required`), which this target doesn't give (Vagrant on VirtualBox, libvirt or Hyper-V, Proxmox and the big clouds do)"
375 )
376}
377
378#[derive(Debug, Clone, Default, PartialEq, Eq)]
380pub struct Capabilities {
381 features: BTreeSet<Feature>,
382}
383
384impl Capabilities {
385 pub fn of(features: impl IntoIterator<Item = Feature>) -> Capabilities {
387 Capabilities {
388 features: features.into_iter().collect(),
389 }
390 }
391
392 #[must_use]
394 pub fn has(&self, feature: Feature) -> bool {
395 self.features.contains(&feature)
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq)]
401pub struct Refusal {
402 pub requirement: Requirement,
404}
405
406impl fmt::Display for Refusal {
407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 self.requirement.fmt(f)
409 }
410}
411
412#[must_use]
414pub fn refusal(requirements: &[Requirement], caps: &Capabilities) -> Option<Refusal> {
415 requirements.iter().find(|r| !caps.has(r.feature())).map(|r| Refusal { requirement: r.clone() })
416}
417
418#[must_use]
420pub fn of(spec: &Spec) -> Vec<Requirement> {
421 let mut out = Vec::new();
422 if !spec.provision.is_empty() {
423 out.push(Requirement::EnvironmentProvisioning);
424 }
425 out.extend(container_checks(spec));
426 out.extend(
427 spec.machines
428 .iter()
429 .filter(|(_, m)| m.docker.as_ref().is_some_and(|d| d.appliance.is_some()))
430 .map(|(name, _)| Requirement::NetworkAppliance { machine: name.clone() }),
431 );
432 if spec.machines.values().any(|m| !m.aliases.is_empty()) {
433 out.push(Requirement::MachineAliases);
434 }
435 out.extend(
436 spec.networks
437 .iter()
438 .filter(|(_, n)| n.gateway.is_some())
439 .map(|(name, _)| Requirement::Gateway { network: name.clone() }),
440 );
441 out.extend(limits(spec));
442 out.extend(crate::ipv6::requirements(spec));
443 out.extend(crate::links::requirements(spec));
444 out.extend(crate::private_dns::requirements(spec));
445 out.extend(crate::storage::requirements(spec));
446 out
447}
448
449fn limits(spec: &Spec) -> Vec<Requirement> {
452 let mut out = Vec::new();
453 if let Some((name, _)) = spec.machines.iter().find(|(_, m)| !m.ulimits.is_empty()) {
454 out.push(Requirement::Ulimits { machine: name.clone() });
455 }
456 if spec.networks.values().any(|n| n.tc.is_some()) {
457 out.push(Requirement::TrafficControl);
458 }
459 if let Some((name, _)) = spec.machines.iter().find(|(_, m)| !crate::process::Process::of(m).is_empty()) {
460 out.push(Requirement::MachineProcess { machine: name.clone() });
461 }
462 out.extend(secrets(spec));
463 out
464}
465
466fn secrets(spec: &Spec) -> Vec<Requirement> {
468 let mut out = Vec::new();
469 if let Some((name, _)) = spec.machines.iter().find(|(_, m)| !m.secrets.is_empty()) {
470 out.push(Requirement::Secrets { machine: name.clone() });
471 }
472 let owned = |m: &isoloom_spec::Machine| m.secrets.iter().any(|s| s.mount().uid.is_some() || s.mount().gid.is_some());
473 if let Some((name, _)) = spec.machines.iter().find(|(_, m)| owned(m)) {
474 out.push(Requirement::SecretOwner { machine: name.clone() });
475 }
476 out
477}
478
479pub(crate) fn mixed_shapes(spec: &Spec, shape: impl Fn(&str) -> Option<MachineShape>) -> Vec<Requirement> {
484 let is = |name: &str, s: MachineShape| shape(name) == Some(s);
485 let volumes = spec.volumes.keys().filter(|v| {
486 let users = spec.volume_users(v);
487 users.iter().any(|u| is(u, MachineShape::Container)) && users.iter().any(|u| is(u, MachineShape::Vm))
488 });
489 let mut out: Vec<Requirement> = volumes.map(|v| Requirement::VolumeAcrossShapes { volume: v.clone() }).collect();
490 for (name, m) in spec.machines.iter().filter(|(n, _)| is(n, MachineShape::Vm)) {
491 if let Some(dep) = m.depends_on.iter().find(|d| is(d, MachineShape::Container)) {
492 out.push(Requirement::VmDependsOnContainer {
493 vm: name.clone(),
494 container: dep.clone(),
495 });
496 }
497 }
498 out
499}
500
501#[must_use]
504pub fn tpm(machines: &IndexMap<String, MachinePlan>) -> Option<Requirement> {
505 machines
506 .iter()
507 .find(|(_, m)| m.shape == MachineShape::Vm && m.tpm == Some(Tpm::Required))
508 .map(|(name, _)| Requirement::Tpm { machine: name.clone() })
509}
510
511pub(crate) fn container_checks(spec: &Spec) -> Option<Requirement> {
515 if spec.checks.iter().any(isoloom_spec::Check::is_playbook) {
516 return Some(Requirement::PlaybookChecks);
517 }
518 checks::plan(spec)
519 .into_iter()
520 .filter(|c| matches!(c.probe, Probe::Exec { .. }))
521 .find(|c| match &c.position {
522 Position::Machine(m) => !spec.machines.get(m).is_some_and(|m| m.docker.is_some() && !m.supplied),
523 Position::Networks => true,
524 })
525 .map(|c| Requirement::ExecWithoutContainer { check: c.name })
526}
527
528mod feature;
529#[cfg(test)]
530mod tests;