use crate::db::Db;
use shared::ServiceSpec;
use shared::config::RustployConfig;
pub const AUTO_PORT: u16 = 0;
pub async fn resolve_host_port(
db: &Db,
spec: &mut ServiceSpec,
exclude_id: Option<&str>,
) -> Result<(), String> {
let Some(requested) = spec.host_port else {
return Ok(());
};
let used = used_ports(db, exclude_id).await?;
if requested == AUTO_PORT {
spec.host_port = Some(allocate(&used)?);
return Ok(());
}
if used.contains(&requested) {
return Err(format!(
"porta externa {requested} já está reservada por outro serviço ou pelo próprio rustploy"
));
}
Ok(())
}
pub async fn port_in_use_by_other(db: &Db, port: u16, exclude_id: Option<&str>) -> bool {
match used_ports(db, exclude_id).await {
Ok(used) => used.contains(&port),
Err(_) => true,
}
}
async fn used_ports(db: &Db, exclude_id: Option<&str>) -> Result<Vec<u16>, String> {
let cfg = RustployConfig::global();
let mut used = vec![
cfg.ingress.http_port,
cfg.ingress.https_port,
cfg.api.port,
];
let services = crate::db::services::list_all(db)
.await
.map_err(|e| format!("falha ao listar serviços para alocação de porta: {e}"))?;
for s in services {
if Some(s.id.as_str()) == exclude_id {
continue;
}
if let Some(p) = s.spec.host_port {
if p != AUTO_PORT {
used.push(p);
}
}
}
Ok(used)
}
fn allocate(used: &[u16]) -> Result<u16, String> {
let range = &RustployConfig::global().external_ports;
for port in range.range_start..=range.range_end {
if used.contains(&port) {
continue;
}
if std::net::TcpListener::bind(("0.0.0.0", port)).is_err() {
continue;
}
return Ok(port);
}
Err(format!(
"faixa de portas externas esgotada ({}-{}); ajuste [external_ports] na config",
range.range_start, range.range_end
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocate_pula_portas_reservadas() {
let range = &RustployConfig::global().external_ports;
let first = range.range_start;
let port = allocate(&[first, first + 1]).expect("deve alocar");
assert!(port > first + 1);
assert!(range.contains(port));
}
#[test]
fn allocate_pula_porta_ocupada_no_so() {
let range = &RustployConfig::global().external_ports;
let _guard = std::net::TcpListener::bind(("0.0.0.0", range.range_start))
.expect("primeira porta da faixa livre no ambiente de teste");
let port = allocate(&[]).expect("deve alocar");
assert_ne!(port, range.range_start);
}
}