zombienet_configuration/
global_settings.rs

1use std::{
2    error::Error,
3    fmt::Display,
4    net::IpAddr,
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8
9use multiaddr::Multiaddr;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    observability::{ObservabilityConfig, ObservabilityConfigBuilder},
14    shared::{
15        errors::{ConfigError, FieldError},
16        helpers::{merge_errors, merge_errors_vecs},
17        types::Duration,
18    },
19    utils::{default_as_true, default_node_spawn_timeout, default_timeout},
20};
21
22fn is_default_observability(config: &ObservabilityConfig) -> bool {
23    *config == ObservabilityConfig::default()
24}
25
26/// Global settings applied to an entire network.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct GlobalSettings {
29    /// Global bootnodes to use (we will then add more)
30    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
31    bootnodes_addresses: Vec<Multiaddr>,
32    // TODO: parse both case in zombienet node version to avoid renamed ?
33    /// Global spawn timeout
34    #[serde(rename = "timeout", default = "default_timeout")]
35    network_spawn_timeout: Duration,
36    // TODO: not used yet
37    /// Node spawn timeout
38    #[serde(default = "default_node_spawn_timeout")]
39    node_spawn_timeout: Duration,
40    // TODO: not used yet
41    /// Local ip to use for construct the direct links
42    local_ip: Option<IpAddr>,
43    /// Directory to use as base dir
44    /// Used to reuse the same files (database) from a previous run,
45    /// also note that we will override the content of some of those files.
46    base_dir: Option<PathBuf>,
47    /// Number of concurrent spawning process to launch, None means try to spawn all at the same time.
48    spawn_concurrency: Option<usize>,
49    /// If enabled, will launch a task to monitor nodes' liveness and tear down the network if there are any.
50    #[serde(default = "default_as_true")]
51    tear_down_on_failure: bool,
52    /// Observability stack configuration (Prometheus + Grafana)
53    #[serde(default, skip_serializing_if = "is_default_observability")]
54    observability: ObservabilityConfig,
55}
56
57impl GlobalSettings {
58    /// External bootnode address.
59    pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
60        self.bootnodes_addresses.iter().collect()
61    }
62
63    /// Global spawn timeout in seconds.
64    pub fn network_spawn_timeout(&self) -> Duration {
65        self.network_spawn_timeout
66    }
67
68    /// Individual node spawn timeout in seconds.
69    pub fn node_spawn_timeout(&self) -> Duration {
70        self.node_spawn_timeout
71    }
72
73    /// Local IP used to expose local services (including RPC, metrics and monitoring).
74    pub fn local_ip(&self) -> Option<&IpAddr> {
75        self.local_ip.as_ref()
76    }
77
78    /// Base directory to use (instead a random tmp one)
79    /// All the artifacts will be created in this directory.
80    pub fn base_dir(&self) -> Option<&Path> {
81        self.base_dir.as_deref()
82    }
83
84    /// Number of concurrent spawning process to launch
85    pub fn spawn_concurrency(&self) -> Option<usize> {
86        self.spawn_concurrency
87    }
88
89    /// A flag to tear down the network if there are any unresponsive nodes detected.
90    pub fn tear_down_on_failure(&self) -> bool {
91        self.tear_down_on_failure
92    }
93
94    /// Observability stack configuration
95    pub fn observability(&self) -> &ObservabilityConfig {
96        &self.observability
97    }
98}
99
100impl Default for GlobalSettings {
101    fn default() -> Self {
102        Self {
103            bootnodes_addresses: Default::default(),
104            network_spawn_timeout: default_timeout(),
105            node_spawn_timeout: default_node_spawn_timeout(),
106            local_ip: Default::default(),
107            base_dir: Default::default(),
108            spawn_concurrency: Default::default(),
109            tear_down_on_failure: true,
110            observability: ObservabilityConfig::default(),
111        }
112    }
113}
114
115/// A global settings builder, used to build [`GlobalSettings`] declaratively with fields validation.
116#[derive(Default)]
117pub struct GlobalSettingsBuilder {
118    config: GlobalSettings,
119    errors: Vec<anyhow::Error>,
120}
121
122impl GlobalSettingsBuilder {
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    // Transition to the next state of the builder.
128    fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
129        Self { config, errors }
130    }
131
132    /// Set the external bootnode address.
133    ///
134    /// Note: Bootnode address replacements are NOT supported here.
135    /// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address.
136    pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
137    where
138        T: TryInto<Multiaddr> + Display + Copy,
139        T::Error: Error + Send + Sync + 'static,
140    {
141        let mut addrs = vec![];
142        let mut errors = vec![];
143
144        for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
145            match addr.try_into() {
146                Ok(addr) => addrs.push(addr),
147                Err(error) => errors.push(
148                    FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
149                ),
150            }
151        }
152
153        Self::transition(
154            GlobalSettings {
155                bootnodes_addresses: addrs,
156                ..self.config
157            },
158            merge_errors_vecs(self.errors, errors),
159        )
160    }
161
162    /// Set global spawn timeout in seconds.
163    pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
164        Self::transition(
165            GlobalSettings {
166                network_spawn_timeout: timeout,
167                ..self.config
168            },
169            self.errors,
170        )
171    }
172
173    /// Set individual node spawn timeout in seconds.
174    pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
175        Self::transition(
176            GlobalSettings {
177                node_spawn_timeout: timeout,
178                ..self.config
179            },
180            self.errors,
181        )
182    }
183
184    /// Set local IP used to expose local services (including RPC, metrics and monitoring).
185    pub fn with_local_ip(self, local_ip: &str) -> Self {
186        match IpAddr::from_str(local_ip) {
187            Ok(local_ip) => Self::transition(
188                GlobalSettings {
189                    local_ip: Some(local_ip),
190                    ..self.config
191                },
192                self.errors,
193            ),
194            Err(error) => Self::transition(
195                self.config,
196                merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
197            ),
198        }
199    }
200
201    /// Set the directory to use as base (instead of a random tmp one).
202    pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
203        Self::transition(
204            GlobalSettings {
205                base_dir: Some(base_dir.into()),
206                ..self.config
207            },
208            self.errors,
209        )
210    }
211
212    /// Set the spawn concurrency
213    pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
214        Self::transition(
215            GlobalSettings {
216                spawn_concurrency: Some(spawn_concurrency),
217                ..self.config
218            },
219            self.errors,
220        )
221    }
222
223    /// Set the `tear_down_on_failure` flag
224    pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
225        Self::transition(
226            GlobalSettings {
227                tear_down_on_failure,
228                ..self.config
229            },
230            self.errors,
231        )
232    }
233
234    /// Configure the observability stack (Prometheus + Grafana)
235    pub fn with_observability(
236        self,
237        f: impl FnOnce(ObservabilityConfigBuilder) -> ObservabilityConfigBuilder,
238    ) -> Self {
239        let observability = f(ObservabilityConfigBuilder::new()).build();
240        Self::transition(
241            GlobalSettings {
242                observability,
243                ..self.config
244            },
245            self.errors,
246        )
247    }
248
249    /// Seals the builder and returns a [`GlobalSettings`] if there are no validation errors, else returns errors.
250    pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
251        if !self.errors.is_empty() {
252            return Err(self
253                .errors
254                .into_iter()
255                .map(|error| ConfigError::GlobalSettings(error).into())
256                .collect::<Vec<_>>());
257        }
258
259        Ok(self.config)
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
269        let global_settings_config = GlobalSettingsBuilder::new()
270            .with_raw_bootnodes_addresses(vec![
271                "/ip4/10.41.122.55/tcp/45421",
272                "/ip4/51.144.222.10/tcp/2333",
273            ])
274            .with_network_spawn_timeout(600)
275            .with_node_spawn_timeout(120)
276            .with_local_ip("10.0.0.1")
277            .with_base_dir("/home/nonroot/mynetwork")
278            .with_spawn_concurrency(5)
279            .with_tear_down_on_failure(true)
280            .build()
281            .unwrap();
282
283        let bootnodes_addresses: Vec<Multiaddr> = vec![
284            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
285            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
286        ];
287        assert_eq!(
288            global_settings_config.bootnodes_addresses(),
289            bootnodes_addresses.iter().collect::<Vec<_>>()
290        );
291        assert_eq!(global_settings_config.network_spawn_timeout(), 600);
292        assert_eq!(global_settings_config.node_spawn_timeout(), 120);
293        assert_eq!(
294            global_settings_config
295                .local_ip()
296                .unwrap()
297                .to_string()
298                .as_str(),
299            "10.0.0.1"
300        );
301        assert_eq!(
302            global_settings_config.base_dir().unwrap(),
303            Path::new("/home/nonroot/mynetwork")
304        );
305        assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
306        assert!(global_settings_config.tear_down_on_failure());
307    }
308
309    #[test]
310    fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
311        let global_settings_config = GlobalSettingsBuilder::new()
312            .with_raw_bootnodes_addresses(vec![
313                "/ip4/10.41.122.55/tcp/45421",
314                "/ip4/51.144.222.10/tcp/2333",
315            ])
316            .with_network_spawn_timeout(600)
317            .with_local_ip("10.0.0.1")
318            .build()
319            .unwrap();
320
321        let bootnodes_addresses: Vec<Multiaddr> = vec![
322            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
323            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
324        ];
325        assert_eq!(
326            global_settings_config.bootnodes_addresses(),
327            bootnodes_addresses.iter().collect::<Vec<_>>()
328        );
329        assert_eq!(global_settings_config.network_spawn_timeout(), 600);
330        assert_eq!(global_settings_config.node_spawn_timeout(), 600);
331        assert_eq!(
332            global_settings_config
333                .local_ip()
334                .unwrap()
335                .to_string()
336                .as_str(),
337            "10.0.0.1"
338        );
339    }
340
341    #[test]
342    fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
343    ) {
344        let errors = GlobalSettingsBuilder::new()
345            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
346            .build()
347            .unwrap_err();
348
349        assert_eq!(errors.len(), 1);
350        assert_eq!(
351            errors.first().unwrap().to_string(),
352            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
353        );
354    }
355
356    #[test]
357    fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
358    ) {
359        let errors = GlobalSettingsBuilder::new()
360            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
361            .build()
362            .unwrap_err();
363
364        assert_eq!(errors.len(), 2);
365        assert_eq!(
366            errors.first().unwrap().to_string(),
367            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
368        );
369        assert_eq!(
370            errors.get(1).unwrap().to_string(),
371            "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
372        );
373    }
374
375    #[test]
376    fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
377        let errors = GlobalSettingsBuilder::new()
378            .with_local_ip("invalid")
379            .build()
380            .unwrap_err();
381
382        assert_eq!(errors.len(), 1);
383        assert_eq!(
384            errors.first().unwrap().to_string(),
385            "global_settings.local_ip: invalid IP address syntax"
386        );
387    }
388
389    #[test]
390    fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
391    ) {
392        let errors = GlobalSettingsBuilder::new()
393            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
394            .with_local_ip("invalid")
395            .build()
396            .unwrap_err();
397
398        assert_eq!(errors.len(), 3);
399        assert_eq!(
400            errors.first().unwrap().to_string(),
401            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
402        );
403        assert_eq!(
404            errors.get(1).unwrap().to_string(),
405            "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
406        );
407        assert_eq!(
408            errors.get(2).unwrap().to_string(),
409            "global_settings.local_ip: invalid IP address syntax"
410        );
411    }
412}