zombienet_configuration/
custom_process.rs

1use std::{error::Error, marker::PhantomData};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    shared::{
7        errors::FieldError,
8        helpers::{ensure_value_is_not_empty, merge_errors},
9        macros::states,
10        node::EnvVar,
11    },
12    types::{Arg, Command, Image},
13};
14
15states! {
16    WithName,
17    WithOutName
18}
19
20states! {
21    WithCmd,
22    WithOutCmd
23}
24
25pub trait Cmd {}
26impl Cmd for WithOutCmd {}
27impl Cmd for WithCmd {}
28
29/// Represent a custom process to spawn, allowing to set:
30/// cmd: Command to execute
31/// args: Argumnets to pass
32/// env: Environment to set
33/// image: Image to use (provider specific)
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct CustomProcess {
36    // Name of the process
37    name: String,
38    // Image to use
39    #[serde(skip_serializing_if = "Option::is_none")]
40    image: Option<Image>,
41    // Command to execute
42    command: Command,
43    // Arguments to pass
44    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
45    args: Vec<Arg>,
46    // Environment to set
47    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
48    env: Vec<EnvVar>,
49}
50
51impl Default for CustomProcess {
52    fn default() -> Self {
53        Self {
54            name: "".into(),
55            image: None,
56            command: Command::default(), // should be changed.
57            args: vec![],
58            env: vec![],
59        }
60    }
61}
62
63impl CustomProcess {
64    /// Node name.
65    pub fn name(&self) -> &str {
66        &self.name
67    }
68
69    /// Image to run (only podman/k8s).
70    pub fn image(&self) -> Option<&Image> {
71        self.image.as_ref()
72    }
73
74    /// Command to run the node.
75    pub fn command(&self) -> &Command {
76        &self.command
77    }
78
79    /// Arguments to use for node.
80    pub fn args(&self) -> Vec<&Arg> {
81        self.args.iter().collect()
82    }
83
84    /// Environment variables to set (inside pod for podman/k8s, inside shell for native).
85    pub fn env(&self) -> Vec<&EnvVar> {
86        self.env.iter().collect()
87    }
88}
89
90/// A node configuration builder, used to build a [`NodeConfig`] declaratively with fields validation.
91pub struct CustomProcessBuilder<N, C> {
92    config: CustomProcess,
93    errors: Vec<anyhow::Error>,
94    _state_name: PhantomData<N>,
95    _state_cmd: PhantomData<C>,
96}
97
98impl Default for CustomProcessBuilder<WithOutName, WithOutCmd> {
99    fn default() -> Self {
100        Self {
101            config: CustomProcess::default(),
102            errors: vec![],
103            _state_name: PhantomData,
104            _state_cmd: PhantomData,
105        }
106    }
107}
108
109impl<A, B> CustomProcessBuilder<A, B> {
110    fn transition<C, D>(
111        config: CustomProcess,
112        errors: Vec<anyhow::Error>,
113    ) -> CustomProcessBuilder<C, D> {
114        CustomProcessBuilder {
115            config,
116            errors,
117            _state_name: PhantomData,
118            _state_cmd: PhantomData,
119        }
120    }
121}
122
123impl CustomProcessBuilder<WithOutName, WithOutCmd> {
124    pub fn new() -> CustomProcessBuilder<WithOutName, WithOutCmd> {
125        CustomProcessBuilder::default()
126    }
127}
128
129impl<C: Cmd> CustomProcessBuilder<WithOutName, C> {
130    /// set the name of the process.
131    pub fn with_name<T: Into<String> + Copy>(self, name: T) -> CustomProcessBuilder<WithName, C> {
132        let name: String = name.into();
133
134        match ensure_value_is_not_empty(&name) {
135            Ok(_) => Self::transition(
136                CustomProcess {
137                    name,
138                    ..self.config
139                },
140                self.errors,
141            ),
142            Err(e) => Self::transition(
143                CustomProcess {
144                    // we still set the name in error case to display error path
145                    name,
146                    ..self.config
147                },
148                merge_errors(self.errors, FieldError::Name(e).into()),
149            ),
150        }
151    }
152}
153
154impl CustomProcessBuilder<WithName, WithOutCmd> {
155    /// Set the command that will be executed to spawn the process.
156    pub fn with_command<T>(self, command: T) -> CustomProcessBuilder<WithName, WithCmd>
157    where
158        T: TryInto<Command>,
159        T::Error: Error + Send + Sync + 'static,
160    {
161        match command.try_into() {
162            Ok(command) => Self::transition(
163                CustomProcess {
164                    command,
165                    ..self.config
166                },
167                self.errors,
168            ),
169            Err(error) => Self::transition(
170                self.config,
171                merge_errors(self.errors, FieldError::Command(error.into()).into()),
172            ),
173        }
174    }
175}
176
177impl CustomProcessBuilder<WithName, WithCmd> {
178    /// Set the image that will be used for the node (only podman/k8s).
179    pub fn with_image<T>(self, image: T) -> Self
180    where
181        T: TryInto<Image>,
182        T::Error: Error + Send + Sync + 'static,
183    {
184        match image.try_into() {
185            Ok(image) => Self::transition(
186                CustomProcess {
187                    image: Some(image),
188                    ..self.config
189                },
190                self.errors,
191            ),
192            Err(error) => Self::transition(
193                self.config,
194                merge_errors(self.errors, FieldError::Image(error.into()).into()),
195            ),
196        }
197    }
198
199    /// Set the arguments that will be used when spawn the process.
200    pub fn with_args(self, args: Vec<Arg>) -> Self {
201        Self::transition(
202            CustomProcess {
203                args,
204                ..self.config
205            },
206            self.errors,
207        )
208    }
209
210    /// Set the  environment variables that will be used when spawn the process.
211    pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
212        let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
213
214        Self::transition(CustomProcess { env, ..self.config }, self.errors)
215    }
216
217    /// Seals the builder and returns a [`CustomProcess`] if there are no validation errors, else returns errors.
218    pub fn build(self) -> Result<CustomProcess, (String, Vec<anyhow::Error>)> {
219        if !self.errors.is_empty() {
220            return Err((self.config.name.clone(), self.errors));
221        }
222
223        Ok(self.config)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn custom_process_config_builder_should_succeeds_and_returns_a_custom_process_config() {
233        let cpb = CustomProcessBuilder::new()
234            .with_name("demo")
235            .with_command("some")
236            .with_args(vec![("--port", "100").into(), "--custom-flag".into()])
237            .build()
238            .unwrap();
239
240        assert_eq!(cpb.command().as_str(), "some");
241        let args: Vec<Arg> = vec![("--port", "100").into(), "--custom-flag".into()];
242        assert_eq!(cpb.args(), args.iter().collect::<Vec<&Arg>>());
243    }
244}