use std::collections::{BTreeSet, HashMap};
use super::Problems;
use super::machines::Subject;
use crate::model::Protocol;
pub(super) fn of_machine(s: &Subject, published: &mut HashMap<(u16, Protocol), String>, p: &mut Problems) {
let at = &s.at;
let mut ports = BTreeSet::new();
for (i, svc) in s.m.ports.iter().enumerate() {
if svc.port == 0 {
p.add(format!("{at}.ports[{i}].port"), "port 0 isn't a port");
} else if !ports.insert((svc.port, svc.protocol)) {
let udp = if svc.protocol.is_tcp() { "" } else { " (UDP)" };
p.add(format!("{at}.ports[{i}].port"), format!("port {}{udp} is listed twice", svc.port));
}
if !svc.protocol.is_tcp() && (svc.http || svc.tls) {
p.add(format!("{at}.ports[{i}]"), "`http` and `tls` run over TCP: a UDP port has neither");
}
match svc.publish {
Some(0) => p.add(format!("{at}.ports[{i}].publish"), "port 0 isn't a port"),
Some(port) => {
if let Some(other) = published.insert((port, svc.protocol), s.name.to_owned()) {
p.add(format!("{at}.ports[{i}].publish"), format!("port {port} is already published by `{other}`"));
}
}
None if svc.fixed => p.add(format!("{at}.ports[{i}].fixed"), "`fixed` keeps the `publish` port: give one"),
None => {}
}
}
}
fn waited_for(dep: &str, waited: &crate::model::Machine, at: String, p: &mut Problems) {
if waited.ports.is_empty() {
p.add(at, format!("`{dep}` has no `ports`, so there's nothing to wait for; give it a port"));
} else if waited.ready_ports().is_empty() {
let why = if waited.ports.iter().all(|port| port.dormant) {
"are all dormant (they answer later)"
} else if waited.ports.iter().all(|port| !port.protocol.is_tcp()) {
"are all UDP (no handshake to wait for)"
} else {
"are all dormant (they answer later) or UDP (no handshake to wait for)"
};
p.add(at, format!("`{dep}`'s ports {why}, so there's nothing to wait for"));
}
}
pub(super) fn depends_on(s: &Subject, p: &mut Problems) {
let spec = s.spec;
for (i, dep) in s.m.depends_on.iter().enumerate() {
let at = format!("{}.depends_on[{i}]", s.at);
if dep == s.name {
p.add(at, "a machine can't depend on itself");
} else {
match spec.machines.get(dep) {
None => p.unknown(at, format!("no machine named `{dep}`")),
Some(waited) => waited_for(dep, waited, at, p),
}
}
}
}
#[cfg(test)]
mod tests {
use crate::testing::{lab, problems};
fn ports(ports: &str) -> Vec<String> {
problems(&lab(&format!(
"machines:\n web: {{ networks: {{ lan: 10 }}, docker: {{ image: nginx }}, ports: {ports} }}\n"
)))
}
fn waiting(depends_on: &str, ports: &str) -> Vec<String> {
problems(&lab(&format!(
"machines:\n srv: {{ networks: {{ lan: 10 }}, docker: {{ image: x }}, ports: {ports} }}\n cli: {{ networks: {{ lan: 11 }}, docker: {{ image: x }}, depends_on: {depends_on} }}\n"
)))
}
#[test]
fn a_port_is_listed_once_per_protocol() {
assert_eq!(ports("[{ port: 53 }, { port: 53, protocol: udp }]"), Vec::<String>::new());
assert_eq!(ports("[{ port: 0 }]"), ["machines.web.ports[0].port: port 0 isn't a port"]);
assert_eq!(ports("[{ port: 80 }, { port: 80 }]"), ["machines.web.ports[1].port: port 80 is listed twice"]);
assert_eq!(
ports("[{ port: 53, protocol: udp }, { port: 53, protocol: udp }]"),
["machines.web.ports[1].port: port 53 (UDP) is listed twice"]
);
}
#[test]
fn http_and_tls_run_over_tcp() {
assert_eq!(ports("[{ port: 443, http: true, tls: true }]"), Vec::<String>::new());
let udp = "machines.web.ports[0]: `http` and `tls` run over TCP: a UDP port has neither";
assert_eq!(ports("[{ port: 443, protocol: udp, http: true }]"), [udp]);
assert_eq!(ports("[{ port: 443, protocol: udp, tls: true }]"), [udp]);
}
#[test]
fn a_fixed_port_keeps_its_published_port() {
assert_eq!(ports("[{ port: 80, publish: 8080, fixed: true }]"), Vec::<String>::new());
assert_eq!(
ports("[{ port: 80, fixed: true }]"),
["machines.web.ports[0].fixed: `fixed` keeps the `publish` port: give one"]
);
}
#[test]
fn a_machine_waits_for_another_machines_answering_port() {
assert_eq!(waiting("[srv]", "[{ port: 80 }, { port: 53, protocol: udp }]"), Vec::<String>::new());
assert_eq!(
waiting("[cli]", "[{ port: 80 }]"),
[
"machines.cli.depends_on[0]: a machine can't depend on itself",
"machines: depends_on forms a cycle: cli -> cli"
]
);
assert_eq!(waiting("[db]", "[{ port: 80 }]"), ["machines.cli.depends_on[0]: no machine named `db`"]);
assert_eq!(
waiting("[srv]", "[]"),
["machines.cli.depends_on[0]: `srv` has no `ports`, so there's nothing to wait for; give it a port"]
);
}
#[test]
fn a_wait_needs_a_port_that_answers_a_handshake_now() {
let nothing = |why: &str| [format!("machines.cli.depends_on[0]: `srv`'s ports {why}, so there's nothing to wait for")];
assert_eq!(
waiting("[srv]", "[{ port: 80, dormant: true }]"),
nothing("are all dormant (they answer later)")
);
assert_eq!(
waiting("[srv]", "[{ port: 53, protocol: udp }]"),
nothing("are all UDP (no handshake to wait for)")
);
assert_eq!(
waiting("[srv]", "[{ port: 80, dormant: true }, { port: 53, protocol: udp }]"),
nothing("are all dormant (they answer later) or UDP (no handshake to wait for)")
);
}
}