Skip to main content

olai_stack_runtime/
ports.rs

1//! Whether a plan's host ports are free on this host.
2
3use std::net::{Ipv4Addr, TcpListener};
4
5use olai_stack_topology::Plan;
6use serde::Serialize;
7
8/// A host port a plan binds that something on this host already holds.
9#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
10pub struct BusyPort {
11    /// The port.
12    pub port: u16,
13    /// What binds it in the plan (see [`Plan::host_ports`]).
14    pub owner: String,
15}
16
17/// Every host port `plan` binds that can't be bound right now, so a start can be refused
18/// before Compose fails half-way. A check, not a reservation: the port can be taken between
19/// the probe and the start.
20pub fn probe_host_ports(plan: &Plan) -> Vec<BusyPort> {
21    plan.host_ports()
22        .into_iter()
23        .filter(|(port, _)| !is_free(*port))
24        .map(|(port, owner)| BusyPort { port, owner })
25        .collect()
26}
27
28/// Whether `port` can be bound on every interface, as Docker publishes by default.
29pub fn is_free(port: u16) -> bool {
30    TcpListener::bind((Ipv4Addr::UNSPECIFIED, port)).is_ok()
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36    use olai_stack_topology::{PlanCtx, Selection, baseline_catalog};
37
38    #[test]
39    fn a_held_port_is_busy() {
40        let held = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap();
41        let port = held.local_addr().unwrap().port();
42        assert!(!is_free(port));
43        drop(held);
44        assert!(is_free(port));
45    }
46
47    #[test]
48    fn the_probe_reports_the_plans_busy_ports() {
49        // Put the gateway on a port this test holds.
50        let held = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap();
51        let port = held.local_addr().unwrap().port();
52        let ctx = PlanCtx {
53            gateway_host_port: port,
54            ..PlanCtx::default()
55        };
56        let plan = baseline_catalog()
57            .plan(&Selection::modules(["envoy", "rustfs"]), &ctx)
58            .unwrap();
59        let busy = probe_host_ports(&plan);
60        assert!(busy.iter().any(|b| b.port == port), "{busy:?}");
61    }
62}