mod boot;
mod checks;
mod cidr;
mod cidr6;
mod cloud;
mod constraints;
mod dns;
mod files;
mod groups;
mod hardware;
mod ipv6;
mod links;
mod machine_files;
mod machines;
mod networks;
mod os;
mod outside;
mod ports;
mod process;
mod provision;
mod reach;
mod secrets;
mod settings;
mod storage;
mod targets;
mod tools;
mod top;
mod volumes;
mod windows;
#[cfg(test)]
mod tests;
use std::collections::BTreeSet;
use crate::model::{Machine, Spec};
use crate::syntax::Code;
pub use cidr::{Cidr, DOCKER_BLOCK, PRIVATE, docker_cidrs};
pub use cidr6::{Cidr6, PREFIX as IPV6_PREFIX};
pub use files::validate_files;
pub use settings::CONTAINER_SYSCTLS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Problem {
pub code: Code,
pub at: String,
pub message: String,
}
impl Problem {
pub fn new(code: Code, at: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code,
at: at.into(),
message: message.into(),
}
}
#[must_use]
pub fn section(&self) -> &str {
self.at.split(['.', '[']).next().unwrap_or_default()
}
}
impl std::fmt::Display for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.at, self.message)
}
}
#[derive(Debug, Default)]
struct Problems(Vec<Problem>);
impl Problems {
fn add(&mut self, at: impl Into<String>, message: impl Into<String>) {
self.0.push(Problem::new(Code::InvalidValue, at, message));
}
fn unknown(&mut self, at: impl Into<String>, message: impl Into<String>) {
self.0.push(Problem::new(Code::UnknownReference, at, message));
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
type Blocks = Vec<(String, Cidr)>;
fn block(blocks: &Blocks, name: &str) -> Option<Cidr> {
blocks.iter().find(|(n, _)| n == name).map(|(_, c)| *c)
}
#[must_use]
pub fn validate(spec: &Spec) -> Vec<Problem> {
let mut p = Problems::default();
top::version_and_name(spec, &mut p);
networks::declared(spec, &mut p);
constraints::check(spec, &mut p);
cloud::check(spec, &mut p);
let blocks = networks::blocks(spec, &mut p);
networks::gateways(spec, &blocks, &mut p);
outside::networks(spec, &mut p);
reach::check(spec, &mut p);
outside::reach(spec, &mut p);
top::inputs(spec, &mut p);
secrets::check(spec, &mut p);
machines::declared(spec, &mut p);
machines::appliances(spec, &mut p);
networks::switches(spec, &mut p);
networks::management(spec, &mut p);
volumes::shared(spec, &mut p);
machines::each(spec, &blocks, &mut p);
dns::options(spec, &mut p);
outside::first_networks(spec, &mut p);
machine_files::check(spec, &mut p);
windows::check(spec, &mut p);
process::check(spec, &mut p);
dns::private(spec, &mut p);
machines::cycle(spec, &mut p);
boot::check(spec, &mut p);
provision::controller(spec, &mut p);
provision::steps(spec, &mut p);
tools::reserved(spec, &blocks, &mut p);
tools::each(spec, &mut p);
machines::external(spec, &mut p);
networks::tc(spec, &mut p);
links::check(spec, &mut p);
ipv6::check(spec, &mut p);
top::message(spec, &mut p);
groups::check(spec, &mut p);
checks::check(spec, &mut p);
targets::check(spec, &mut p);
if p.is_empty() {
networks::docker_overlaps(spec, &mut p);
}
p.0
}
fn kebab(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 63
&& s.split('-')
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()))
}
fn dns_label(s: &str) -> bool {
kebab(s) && !s.starts_with(|c: char| c.is_ascii_digit())
}
fn variable_name(s: &str) -> bool {
s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn starts_after<'a>(spec: &'a Spec, name: &str, m: &'a Machine) -> Vec<&'a str> {
let mut after: Vec<&str> = m.depends_on.iter().map(String::as_str).collect();
for net in m.networks.keys() {
if let Some(gw) = spec.networks.get(net).and_then(|n| n.gateway.as_deref())
&& gw != name
&& !after.contains(&gw)
{
after.push(gw);
}
}
for v in m.volumes.keys() {
if let Some(owner) = spec.volume_owner(v)
&& owner != name
&& spec.machines.contains_key(owner)
&& !after.contains(&owner)
{
after.push(owner);
}
}
after
}
fn dependency_cycle(spec: &Spec) -> Option<Vec<String>> {
fn visit<'a>(spec: &'a Spec, n: &'a str, stack: &mut Vec<&'a str>, done: &mut BTreeSet<&'a str>) -> Option<Vec<String>> {
if let Some(pos) = stack.iter().position(|s| *s == n) {
let mut c: Vec<String> = stack.iter().skip(pos).map(|s| (*s).to_owned()).collect();
c.push(n.to_owned());
return Some(c);
}
if done.contains(n) {
return None;
}
stack.push(n);
for dep in spec.machines.get(n).map(|m| starts_after(spec, n, m)).unwrap_or_default() {
if spec.machines.contains_key(dep)
&& let Some(c) = visit(spec, dep, stack, done)
{
return Some(c);
}
}
stack.pop();
done.insert(n);
None
}
let mut done = BTreeSet::new();
spec.machines.keys().find_map(|n| visit(spec, n, &mut Vec::new(), &mut done))
}