diff --git a/crates/configuration/src/shared/errors.rs b/crates/configuration/src/shared/errors.rs index 32007d6cb530a07b5fd583650e596db1150fe864..0ff10fdaa6a9674d58ea9fccb8f5abd8ed0d9392 100644 --- a/crates/configuration/src/shared/errors.rs +++ b/crates/configuration/src/shared/errors.rs @@ -1,4 +1,4 @@ -use super::types::ParaId; +use super::types::{ParaId, Port}; /// An error at the configuration level. #[derive(thiserror::Error, Debug)] @@ -66,6 +66,18 @@ pub enum FieldError { #[error("limit_cpu: {0}")] LimitCpu(anyhow::Error), + + #[error("ws_port: {0}")] + WsPort(anyhow::Error), + + #[error("rpc_port: {0}")] + RpcPort(anyhow::Error), + + #[error("prometheus_port: {0}")] + PrometheusPort(anyhow::Error), + + #[error("p2p_port: {0}")] + P2pPort(anyhow::Error), } /// A conversion error for shared types across fields. @@ -80,3 +92,10 @@ pub enum ConversionError { #[error("can't be empty")] CantBeEmpty, } + +/// A validation error for shared types across fields. +#[derive(thiserror::Error, Debug, Clone)] +pub enum ValidationError { + #[error("'{0}' is already used")] + PortAlreadyUsed(Port), +} diff --git a/crates/configuration/src/shared/helpers.rs b/crates/configuration/src/shared/helpers.rs index 93a3dffb62e06209e45ac0564e24fc3a485dfe88..ae742289ff1d22dd9f5e38b2532227a2957d81b3 100644 --- a/crates/configuration/src/shared/helpers.rs +++ b/crates/configuration/src/shared/helpers.rs @@ -1,3 +1,10 @@ +use std::{cell::RefCell, rc::Rc}; + +use super::{ + errors::ValidationError, + types::{Port, ValidationContext}, +}; + pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> { let mut errors = errors; errors.push(new_error); @@ -17,3 +24,19 @@ pub fn merge_errors_vecs( errors } + +pub fn ensure_port_unique( + port: Port, + validation_context: Rc<RefCell<ValidationContext>>, +) -> Result<(), anyhow::Error> { + let mut context = validation_context + .try_borrow_mut() + .expect("must be borrowable as mutable, this is a bug please report it: https://github.com/paritytech/zombienet-sdk/issues"); + + if !context.used_ports.contains(&port) { + context.used_ports.push(port); + return Ok(()); + } + + Err(ValidationError::PortAlreadyUsed(port).into()) +} diff --git a/crates/configuration/src/shared/node.rs b/crates/configuration/src/shared/node.rs index c1b752032fa87bf6eab643ab36b2af027fab9028..70fa330294a19e14c921e19a5c8c74cb52fd80c3 100644 --- a/crates/configuration/src/shared/node.rs +++ b/crates/configuration/src/shared/node.rs @@ -4,7 +4,7 @@ use multiaddr::Multiaddr; use super::{ errors::FieldError, - helpers::{merge_errors, merge_errors_vecs}, + helpers::{ensure_port_unique, merge_errors, merge_errors_vecs}, macros::states, resources::ResourcesBuilder, types::{AssetLocation, ChainDefaultContext, Command, Image, ValidationContext}, @@ -422,50 +422,78 @@ impl NodeConfigBuilder<Buildable> { /// Set the websocket port that will be exposed. Uniqueness across config will be checked. pub fn with_ws_port(self, ws_port: Port) -> Self { - Self::transition( - NodeConfig { - ws_port: Some(ws_port), - ..self.config - }, - self.validation_context, - self.errors, - ) + match ensure_port_unique(ws_port, self.validation_context.clone()) { + Ok(_) => Self::transition( + NodeConfig { + ws_port: Some(ws_port), + ..self.config + }, + self.validation_context, + self.errors, + ), + Err(error) => Self::transition( + self.config, + self.validation_context, + merge_errors(self.errors, FieldError::WsPort(error).into()), + ), + } } /// Set the RPC port that will be exposed. Uniqueness across config will be checked. pub fn with_rpc_port(self, rpc_port: Port) -> Self { - Self::transition( - NodeConfig { - rpc_port: Some(rpc_port), - ..self.config - }, - self.validation_context, - self.errors, - ) + match ensure_port_unique(rpc_port, self.validation_context.clone()) { + Ok(_) => Self::transition( + NodeConfig { + rpc_port: Some(rpc_port), + ..self.config + }, + self.validation_context, + self.errors, + ), + Err(error) => Self::transition( + self.config, + self.validation_context, + merge_errors(self.errors, FieldError::RpcPort(error).into()), + ), + } } /// Set the prometheus port that will be exposed for metrics. Uniqueness across config will be checked. pub fn with_prometheus_port(self, prometheus_port: Port) -> Self { - Self::transition( - NodeConfig { - prometheus_port: Some(prometheus_port), - ..self.config - }, - self.validation_context, - self.errors, - ) + match ensure_port_unique(prometheus_port, self.validation_context.clone()) { + Ok(_) => Self::transition( + NodeConfig { + prometheus_port: Some(prometheus_port), + ..self.config + }, + self.validation_context, + self.errors, + ), + Err(error) => Self::transition( + self.config, + self.validation_context, + merge_errors(self.errors, FieldError::PrometheusPort(error).into()), + ), + } } - /// Set the P2P port that will be exposed. Uniqueness across will be checked. + /// Set the P2P port that will be exposed. Uniqueness across config will be checked. pub fn with_p2p_port(self, p2p_port: Port) -> Self { - Self::transition( - NodeConfig { - p2p_port: Some(p2p_port), - ..self.config - }, - self.validation_context, - self.errors, - ) + match ensure_port_unique(p2p_port, self.validation_context.clone()) { + Ok(_) => Self::transition( + NodeConfig { + p2p_port: Some(p2p_port), + ..self.config + }, + self.validation_context, + self.errors, + ), + Err(error) => Self::transition( + self.config, + self.validation_context, + merge_errors(self.errors, FieldError::P2pPort(error).into()), + ), + } } /// Set the P2P cert hash that will be used as part of the multiaddress