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