use std::collections::BTreeSet;
use std::fmt;
use indexmap::IndexMap;
use isoloom_spec::model::Spec;
use isoloom_spec::windows::Tpm;
use crate::checks::{self, Position, Probe};
use crate::environment::{MachinePlan, MachineShape};
use crate::hardware::Need;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Feature {
EnvironmentProvisioning,
PlaybookChecks,
ExecWithoutContainer,
NetworkAppliances,
Gateways,
VolumesAcrossShapes,
VmAfterContainer,
MachineAliases,
Ulimits,
TrafficControl,
Ipv6,
Ipv6Only,
Ipv6Auto,
Secrets,
SecretOwners,
ExternalNetworks,
BridgedNetworks,
OwnedProjectFiles,
ExistingHostFiles,
Tpm,
MachineProcess,
OwnBoot,
ContainerGpus,
ContainerDevices,
ContainerNestedVirtualization,
VmGpus,
VmDevices,
NestedVirtualization,
NetworkMtu,
RateLimit,
PrivateDns,
PersistentVolumes,
ExternalVolumes,
DataDisks,
RawDisks,
TmpfsModes,
VmMoreAddresses,
ContainerMoreAddresses,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Requirement {
EnvironmentProvisioning,
PlaybookChecks,
ExecWithoutContainer {
check: String,
},
NetworkAppliance {
machine: String,
},
Gateway {
network: String,
},
VolumeAcrossShapes {
volume: String,
},
VmDependsOnContainer {
vm: String,
container: String,
},
MachineAliases,
Ulimits {
machine: String,
},
TrafficControl,
Ipv6 {
network: String,
},
Ipv6Only {
network: String,
},
Ipv6Auto {
network: String,
},
Secrets {
machine: String,
},
SecretOwner {
machine: String,
},
ExternalNetwork {
network: String,
name: String,
},
BridgedNetwork {
network: String,
},
OwnedProjectFile {
machine: String,
path: String,
},
ExistingHostFiles {
machine: String,
},
Tpm {
machine: String,
},
MachineProcess {
machine: String,
},
OwnBoot {
machine: String,
},
Hardware {
machine: String,
need: Need,
},
NetworkMtu {
network: String,
},
RateLimit {
machine: String,
},
PrivateDns {
domain: String,
},
PersistentVolume {
volume: String,
},
ExternalVolume {
volume: String,
},
DataDisks {
machine: String,
},
RawDisks {
machine: String,
},
TmpfsModes {
machine: String,
},
MoreAddresses {
machine: String,
network: String,
shape: MachineShape,
},
}
impl Requirement {
#[must_use]
pub fn feature(&self) -> Feature {
match self {
Requirement::EnvironmentProvisioning => Feature::EnvironmentProvisioning,
Requirement::PlaybookChecks => Feature::PlaybookChecks,
Requirement::ExecWithoutContainer { .. } => Feature::ExecWithoutContainer,
Requirement::NetworkAppliance { .. } => Feature::NetworkAppliances,
Requirement::Gateway { .. } => Feature::Gateways,
Requirement::VolumeAcrossShapes { .. } => Feature::VolumesAcrossShapes,
Requirement::VmDependsOnContainer { .. } => Feature::VmAfterContainer,
Requirement::MachineAliases => Feature::MachineAliases,
Requirement::Ulimits { .. } => Feature::Ulimits,
Requirement::TrafficControl => Feature::TrafficControl,
Requirement::Ipv6 { .. } => Feature::Ipv6,
Requirement::Ipv6Only { .. } => Feature::Ipv6Only,
Requirement::Ipv6Auto { .. } => Feature::Ipv6Auto,
Requirement::Secrets { .. } => Feature::Secrets,
Requirement::SecretOwner { .. } => Feature::SecretOwners,
Requirement::ExternalNetwork { .. } => Feature::ExternalNetworks,
Requirement::BridgedNetwork { .. } => Feature::BridgedNetworks,
Requirement::OwnedProjectFile { .. } => Feature::OwnedProjectFiles,
Requirement::ExistingHostFiles { .. } => Feature::ExistingHostFiles,
Requirement::Tpm { .. } => Feature::Tpm,
Requirement::MachineProcess { .. } => Feature::MachineProcess,
Requirement::OwnBoot { .. } => Feature::OwnBoot,
Requirement::Hardware { need, .. } => need.feature(),
Requirement::NetworkMtu { .. } => Feature::NetworkMtu,
Requirement::RateLimit { .. } => Feature::RateLimit,
Requirement::PrivateDns { .. } => Feature::PrivateDns,
Requirement::PersistentVolume { .. } => Feature::PersistentVolumes,
Requirement::ExternalVolume { .. } => Feature::ExternalVolumes,
Requirement::DataDisks { .. } => Feature::DataDisks,
Requirement::RawDisks { .. } => Feature::RawDisks,
Requirement::TmpfsModes { .. } => Feature::TmpfsModes,
Requirement::MoreAddresses { shape: MachineShape::Vm, .. } => Feature::VmMoreAddresses,
Requirement::MoreAddresses { .. } => Feature::ContainerMoreAddresses,
}
}
}
const OWN_PROCESS: &str = "sets its own process (`command` or `healthcheck`): an existing host runs what it already runs, Isoloom starts nothing on it";
const IN_A_CONTAINER: &str = "needs `from:` a machine with its own container (it runs inside it)";
impl fmt::Display for Requirement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Requirement::EnvironmentProvisioning => f.write_str("environment-level provisioning (`provision:`) runs on VM targets for now"),
Requirement::PlaybookChecks => f.write_str("Ansible checks (.yml) run on VM targets for now"),
Requirement::ExecWithoutContainer { check } => write!(f, "`exec` check \"{check}\" {IN_A_CONTAINER}"),
Requirement::NetworkAppliance { machine } => write!(
f,
"machine `{machine}` is a network appliance (`app.appliance`), which runs on the Docker targets"
),
Requirement::Gateway { .. } => f.write_str("networks with a `gateway` machine in hybrid environments come later"),
Requirement::VolumeAcrossShapes { volume } => write!(
f,
"volume `{volume}` is shared by containers and VMs: one storage can't be both a Docker volume and an NFS export, for now"
),
Requirement::VmDependsOnContainer { vm, container } => write!(
f,
"VM `{vm}` depends on container `{container}`: VMs start first in a hybrid environment, for now"
),
Requirement::MachineAliases => f.write_str("machine `aliases` (dotted DNS names) aren't Kubernetes Service names"),
Requirement::Ulimits { machine } => write!(f, "machine `{machine}` sets `ulimits`: Kubernetes has no per-container process limits"),
Requirement::TrafficControl => f.write_str("link impairment (`tc`) needs a router in the path; Kubernetes has none"),
Requirement::Ipv6 { network } => write!(f, "network `{network}` is IPv6 (`ipv6`), which this target's networks don't carry yet"),
Requirement::Ipv6Only { network } => crate::ipv6::no_ipv6_only(f, network),
Requirement::Ipv6Auto { network } => crate::ipv6::no_auto_addresses(f, network),
Requirement::Secrets { machine } => crate::secrets::no_delivery(f, machine),
Requirement::SecretOwner { machine } => crate::secrets::no_owner(f, machine),
Requirement::ExternalNetwork { network, name } => crate::outside::no_existing_network(f, network, name),
Requirement::BridgedNetwork { network } => crate::outside::no_bridge(f, network),
Requirement::OwnedProjectFile { machine, path } => crate::machine_files::no_owned_project_file(f, machine, path),
Requirement::ExistingHostFiles { machine } => crate::machine_files::no_existing_host_files(f, machine),
Requirement::Tpm { machine } => no_tpm(f, machine),
Requirement::MachineProcess { machine } => write!(f, "machine `{machine}` {OWN_PROCESS}"),
Requirement::OwnBoot { machine } => crate::boot::no_own_boot(f, machine),
Requirement::Hardware { machine, need } => need.reason(machine, f),
Requirement::NetworkMtu { network } => crate::links::no_mtu(f, network),
Requirement::RateLimit { machine } => crate::links::no_rate_limit(f, machine),
Requirement::PrivateDns { domain } => write!(
f,
"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"
),
Requirement::MoreAddresses { machine, network, shape } => crate::more_addresses::refused(f, machine, network, *shape),
Requirement::PersistentVolume { volume } => crate::storage::no_persistent_volume(f, volume),
Requirement::ExternalVolume { volume } => crate::storage::no_existing_volume(f, volume),
Requirement::DataDisks { machine } => crate::storage::no_data_disks(f, machine),
Requirement::RawDisks { machine } => crate::storage::no_raw_disks(f, machine),
Requirement::TmpfsModes { machine } => crate::storage::no_tmpfs_modes(f, machine),
}
}
}
fn no_tpm(f: &mut fmt::Formatter<'_>, machine: &str) -> fmt::Result {
write!(
f,
"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)"
)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Capabilities {
features: BTreeSet<Feature>,
}
impl Capabilities {
pub fn of(features: impl IntoIterator<Item = Feature>) -> Capabilities {
Capabilities {
features: features.into_iter().collect(),
}
}
#[must_use]
pub fn has(&self, feature: Feature) -> bool {
self.features.contains(&feature)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
pub requirement: Requirement,
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.requirement.fmt(f)
}
}
#[must_use]
pub fn refusal(requirements: &[Requirement], caps: &Capabilities) -> Option<Refusal> {
requirements.iter().find(|r| !caps.has(r.feature())).map(|r| Refusal { requirement: r.clone() })
}
#[must_use]
pub fn of(spec: &Spec) -> Vec<Requirement> {
let mut out = Vec::new();
if !spec.provision.is_empty() {
out.push(Requirement::EnvironmentProvisioning);
}
out.extend(container_checks(spec));
out.extend(
spec.machines
.iter()
.filter(|(_, m)| m.docker.as_ref().is_some_and(|d| d.appliance.is_some()))
.map(|(name, _)| Requirement::NetworkAppliance { machine: name.clone() }),
);
if spec.machines.values().any(|m| !m.aliases.is_empty()) {
out.push(Requirement::MachineAliases);
}
out.extend(
spec.networks
.iter()
.filter(|(_, n)| n.gateway.is_some())
.map(|(name, _)| Requirement::Gateway { network: name.clone() }),
);
out.extend(limits(spec));
out.extend(crate::ipv6::requirements(spec));
out.extend(crate::links::requirements(spec));
out.extend(crate::private_dns::requirements(spec));
out.extend(crate::storage::requirements(spec));
out
}
fn limits(spec: &Spec) -> Vec<Requirement> {
let mut out = Vec::new();
if let Some((name, _)) = spec.machines.iter().find(|(_, m)| !m.ulimits.is_empty()) {
out.push(Requirement::Ulimits { machine: name.clone() });
}
if spec.networks.values().any(|n| n.tc.is_some()) {
out.push(Requirement::TrafficControl);
}
if let Some((name, _)) = spec.machines.iter().find(|(_, m)| !crate::process::Process::of(m).is_empty()) {
out.push(Requirement::MachineProcess { machine: name.clone() });
}
out.extend(secrets(spec));
out
}
fn secrets(spec: &Spec) -> Vec<Requirement> {
let mut out = Vec::new();
if let Some((name, _)) = spec.machines.iter().find(|(_, m)| !m.secrets.is_empty()) {
out.push(Requirement::Secrets { machine: name.clone() });
}
let owned = |m: &isoloom_spec::Machine| m.secrets.iter().any(|s| s.mount().uid.is_some() || s.mount().gid.is_some());
if let Some((name, _)) = spec.machines.iter().find(|(_, m)| owned(m)) {
out.push(Requirement::SecretOwner { machine: name.clone() });
}
out
}
pub(crate) fn mixed_shapes(spec: &Spec, shape: impl Fn(&str) -> Option<MachineShape>) -> Vec<Requirement> {
let is = |name: &str, s: MachineShape| shape(name) == Some(s);
let volumes = spec.volumes.keys().filter(|v| {
let users = spec.volume_users(v);
users.iter().any(|u| is(u, MachineShape::Container)) && users.iter().any(|u| is(u, MachineShape::Vm))
});
let mut out: Vec<Requirement> = volumes.map(|v| Requirement::VolumeAcrossShapes { volume: v.clone() }).collect();
for (name, m) in spec.machines.iter().filter(|(n, _)| is(n, MachineShape::Vm)) {
if let Some(dep) = m.depends_on.iter().find(|d| is(d, MachineShape::Container)) {
out.push(Requirement::VmDependsOnContainer {
vm: name.clone(),
container: dep.clone(),
});
}
}
out
}
#[must_use]
pub fn tpm(machines: &IndexMap<String, MachinePlan>) -> Option<Requirement> {
machines
.iter()
.find(|(_, m)| m.shape == MachineShape::Vm && m.tpm == Some(Tpm::Required))
.map(|(name, _)| Requirement::Tpm { machine: name.clone() })
}
pub(crate) fn container_checks(spec: &Spec) -> Option<Requirement> {
if spec.checks.iter().any(isoloom_spec::Check::is_playbook) {
return Some(Requirement::PlaybookChecks);
}
checks::plan(spec)
.into_iter()
.filter(|c| matches!(c.probe, Probe::Exec { .. }))
.find(|c| match &c.position {
Position::Machine(m) => !spec.machines.get(m).is_some_and(|m| m.docker.is_some() && !m.supplied),
Position::Networks => true,
})
.map(|c| Requirement::ExecWithoutContainer { check: c.name })
}
mod feature;
#[cfg(test)]
mod tests;