use std::collections::BTreeSet;
use super::machines::Subject;
use super::{Problems, kebab};
use crate::model::{SharedVolume, Spec};
pub(super) fn shared(spec: &Spec, p: &mut Problems) {
for (v, volume) in &spec.volumes {
let at = format!("volumes.{v}");
let users = spec.volume_users(v);
if users.is_empty() {
p.add(at, "no machine mounts it: list it in a machine's `volumes`");
continue;
}
if let Some(o) = &volume.owner
&& !users.contains(&o.as_str())
{
p.add(
format!("{at}.owner"),
format!("`{o}` doesn't mount it: the owner is one of its machines ({})", users.join(", ")),
);
continue;
}
kept(volume, &at, p);
host_folder(volume, &at, p);
let Some(owner) = spec.volume_owner(v) else { continue };
let owner_networks = spec.machines.get(owner).map(|o| &o.networks);
for (u, m) in users.iter().filter_map(|u| Some((*u, spec.machines.get(*u)?))) {
if m.vm.as_ref().is_some_and(|vm| crate::images::is_windows(&vm.os)) {
p.add(format!("machines.{u}.volumes.{v}"), windows_why(volume));
}
if u != owner && !m.networks.keys().any(|n| owner_networks.is_some_and(|o| o.contains_key(n))) {
p.add(
format!("machines.{u}.volumes.{v}"),
format!("`{u}` shares no network with `{owner}`, which holds the volume (NFS on the VM targets)"),
);
}
}
}
}
fn windows_why(volume: &SharedVolume) -> &'static str {
if volume.virtiofs.is_some() {
"a shared volume is mounted over NFS, or virtiofs on Proxmox, whose Windows driver (virtio-fs and WinFsp) the set-up doesn't install: Linux machines only"
} else {
"a shared volume is mounted over NFS: Linux machines only"
}
}
fn host_folder(volume: &SharedVolume, at: &str, p: &mut Problems) {
let Some(mapping) = volume.virtiofs.as_ref().and_then(|f| f.mapping.as_deref()) else {
return;
};
if !crate::model::mapping_id(mapping) {
p.add(
format!("{at}.virtiofs.mapping"),
format!("`{mapping}` isn't a directory mapping id: a letter, then letters, digits, '-' and '_', at most 64"),
);
}
}
fn kept(vol: &SharedVolume, at: &str, p: &mut Problems) {
let Some(name) = &vol.external else { return };
if vol.persistent {
p.add(format!("{at}.persistent"), "an external volume is never removed: leave `persistent` out");
}
let valid = name.len() <= 63
&& name.starts_with(|c: char| c.is_ascii_alphanumeric())
&& name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
if !valid {
p.add(
format!("{at}.external"),
format!("`{name}` isn't a volume name: letters, digits, '-', '_' and '.', starting with a letter or digit, at most 63"),
);
}
}
pub(super) fn of_machine(s: &Subject, p: &mut Problems) {
let mut paths = BTreeSet::new();
for (v, mount) in &s.m.volumes {
let vat = format!("{}.volumes.{v}", s.at);
if let Some(why) = name_problem(s.name, v) {
p.add(&vat, why);
}
let path = mount.path();
if !safe_path(path) {
p.add(
&vat,
format!("`{path}` isn't a safe absolute path inside the machine (no spaces or ':'), like /var/lib/postgresql/data"),
);
} else if !paths.insert(path.trim_end_matches('/')) {
p.add(&vat, format!("`{path}` is already a volume of this machine"));
}
if let Some(sub) = mount.subpath() {
subpath(s, v, sub, &format!("{vat}.subpath"), p);
}
}
super::storage::of_machine(s, &paths, p);
}
pub(super) fn safe_path(path: &str) -> bool {
path.starts_with('/') && path != "/" && !path.split('/').any(|s| s == "..") && !path.contains([':', ' ', '\t'])
}
fn subpath(s: &Subject, v: &str, sub: &str, at: &str, p: &mut Problems) {
if !s.spec.volumes.contains_key(v) {
p.add(
at,
format!("only a shared volume has folders to take: declare `{v}` under the top-level `volumes`"),
);
} else if s.spec.volume_owner(v) == Some(s.name) {
p.add(
at,
format!(
"`{}` holds the whole volume `{v}`: give the subpath to another machine, or make another one the `owner`",
s.name
),
);
}
let safe = !sub.is_empty()
&& !sub.starts_with('/')
&& sub.split('/').all(|part| !part.is_empty() && part != "." && part != "..")
&& sub.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/'));
if !safe {
p.add(
at,
format!("`{sub}` isn't a folder inside the volume: a relative path like `www` or `logs/app`, no `..`"),
);
}
}
fn name_problem(machine: &str, v: &str) -> Option<&'static str> {
if !kebab(v) {
Some("volume names are kebab-case: lowercase letters, digits and dashes")
} else if machine.len() + 1 + v.len() > 63 {
Some("machine name and volume name together are at most 62 characters: the Kubernetes claim is `<machine>-<volume>`")
} else if v.len() > 58 {
Some("volume names are at most 58 characters: the Kubernetes init container seeding it is `seed-<volume>`")
} else {
None
}
}
#[cfg(test)]
mod tests;