1use std::{cell::RefCell, error::Error, fmt::Display, marker::PhantomData, path::PathBuf, rc::Rc};
2
3use multiaddr::Multiaddr;
4use serde::{ser::SerializeStruct, Deserialize, Serialize};
5
6use super::{
7 errors::FieldError,
8 helpers::{
9 ensure_port_unique, ensure_value_is_not_empty, generate_unique_node_name,
10 generate_unique_node_name_from_names, merge_errors, merge_errors_vecs,
11 },
12 macros::states,
13 resources::ResourcesBuilder,
14 types::{AssetLocation, ChainDefaultContext, Command, Image, ValidationContext, U128},
15};
16use crate::{
17 shared::{
18 resources::Resources,
19 types::{Arg, Port},
20 },
21 utils::{default_as_true, default_initial_balance},
22};
23
24states! {
25 Buildable,
26 Initial
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct EnvVar {
49 pub name: String,
51
52 pub value: String,
54}
55
56impl From<(&str, &str)> for EnvVar {
57 fn from((name, value): (&str, &str)) -> Self {
58 Self {
59 name: name.to_owned(),
60 value: value.to_owned(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
67pub struct NodeConfig {
68 pub(crate) name: String,
69 pub(crate) image: Option<Image>,
70 pub(crate) command: Option<Command>,
71 pub(crate) subcommand: Option<Command>,
72 #[serde(default)]
73 args: Vec<Arg>,
74 #[serde(alias = "validator", default = "default_as_true")]
75 pub(crate) is_validator: bool,
76 #[serde(alias = "invulnerable", default = "default_as_true")]
77 pub(crate) is_invulnerable: bool,
78 #[serde(alias = "bootnode", default)]
79 pub(crate) is_bootnode: bool,
80 #[serde(alias = "balance")]
81 #[serde(default = "default_initial_balance")]
82 initial_balance: U128,
83 #[serde(default)]
84 env: Vec<EnvVar>,
85 #[serde(default)]
86 bootnodes_addresses: Vec<Multiaddr>,
87 pub(crate) resources: Option<Resources>,
88 ws_port: Option<Port>,
89 rpc_port: Option<Port>,
90 prometheus_port: Option<Port>,
91 p2p_port: Option<Port>,
92 p2p_cert_hash: Option<String>,
93 pub(crate) db_snapshot: Option<AssetLocation>,
94 #[serde(default)]
95 pub(crate) chain_context: ChainDefaultContext,
97 pub(crate) node_log_path: Option<PathBuf>,
98}
99
100impl Serialize for NodeConfig {
101 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102 where
103 S: serde::Serializer,
104 {
105 let mut state = serializer.serialize_struct("NodeConfig", 18)?;
106 state.serialize_field("name", &self.name)?;
107
108 if self.image == self.chain_context.default_image {
109 state.skip_field("image")?;
110 } else {
111 state.serialize_field("image", &self.image)?;
112 }
113
114 if self.command == self.chain_context.default_command {
115 state.skip_field("command")?;
116 } else {
117 state.serialize_field("command", &self.command)?;
118 }
119
120 if self.subcommand.is_none() {
121 state.skip_field("subcommand")?;
122 } else {
123 state.serialize_field("subcommand", &self.subcommand)?;
124 }
125
126 if self.args.is_empty() || self.args == self.chain_context.default_args {
127 state.skip_field("args")?;
128 } else {
129 state.serialize_field("args", &self.args)?;
130 }
131
132 state.serialize_field("validator", &self.is_validator)?;
133 state.serialize_field("invulnerable", &self.is_invulnerable)?;
134 state.serialize_field("bootnode", &self.is_bootnode)?;
135 state.serialize_field("balance", &self.initial_balance)?;
136
137 if self.env.is_empty() {
138 state.skip_field("env")?;
139 } else {
140 state.serialize_field("env", &self.env)?;
141 }
142
143 if self.bootnodes_addresses.is_empty() {
144 state.skip_field("bootnodes_addresses")?;
145 } else {
146 state.serialize_field("bootnodes_addresses", &self.bootnodes_addresses)?;
147 }
148
149 if self.resources == self.chain_context.default_resources {
150 state.skip_field("resources")?;
151 } else {
152 state.serialize_field("resources", &self.resources)?;
153 }
154
155 state.serialize_field("ws_port", &self.ws_port)?;
156 state.serialize_field("rpc_port", &self.rpc_port)?;
157 state.serialize_field("prometheus_port", &self.prometheus_port)?;
158 state.serialize_field("p2p_port", &self.p2p_port)?;
159 state.serialize_field("p2p_cert_hash", &self.p2p_cert_hash)?;
160
161 if self.db_snapshot == self.chain_context.default_db_snapshot {
162 state.skip_field("db_snapshot")?;
163 } else {
164 state.serialize_field("db_snapshot", &self.db_snapshot)?;
165 }
166
167 if self.node_log_path.is_none() {
168 state.skip_field("node_log_path")?;
169 } else {
170 state.serialize_field("node_log_path", &self.node_log_path)?;
171 }
172
173 state.skip_field("chain_context")?;
174 state.end()
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Deserialize)]
180pub struct GroupNodeConfig {
181 #[serde(flatten)]
182 pub(crate) base_config: NodeConfig,
183 pub(crate) count: usize,
184}
185
186impl GroupNodeConfig {
187 pub fn expand_group_configs(&self) -> Vec<NodeConfig> {
190 let mut used_names = std::collections::HashSet::new();
191
192 (0..self.count)
193 .map(|_index| {
194 let mut node = self.base_config.clone();
195
196 let unique_name = generate_unique_node_name_from_names(node.name, &mut used_names);
197 node.name = unique_name;
198
199 if let Some(ref base_log_path) = node.node_log_path {
201 let unique_log_path = if let Some(parent) = base_log_path.parent() {
202 parent.join(format!("{}.log", node.name))
203 } else {
204 PathBuf::from(format!("{}.log", node.name))
205 };
206 node.node_log_path = Some(unique_log_path);
207 }
208
209 node
210 })
211 .collect()
212 }
213}
214
215impl Serialize for GroupNodeConfig {
216 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
217 where
218 S: serde::Serializer,
219 {
220 let mut state = serializer.serialize_struct("GroupNodeConfig", 18)?;
221 state.serialize_field("NodeConfig", &self.base_config)?;
222 state.serialize_field("count", &self.count)?;
223 state.end()
224 }
225}
226
227impl NodeConfig {
228 pub fn name(&self) -> &str {
230 &self.name
231 }
232
233 pub fn image(&self) -> Option<&Image> {
235 self.image.as_ref()
236 }
237
238 pub fn command(&self) -> Option<&Command> {
240 self.command.as_ref()
241 }
242
243 pub fn subcommand(&self) -> Option<&Command> {
245 self.subcommand.as_ref()
246 }
247
248 pub fn args(&self) -> Vec<&Arg> {
250 self.args.iter().collect()
251 }
252
253 pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
255 self.args = args;
256 }
257
258 pub fn is_validator(&self) -> bool {
260 self.is_validator
261 }
262
263 pub fn is_invulnerable(&self) -> bool {
265 self.is_invulnerable
266 }
267
268 pub fn is_bootnode(&self) -> bool {
270 self.is_bootnode
271 }
272
273 pub fn initial_balance(&self) -> u128 {
275 self.initial_balance.0
276 }
277
278 pub fn env(&self) -> Vec<&EnvVar> {
280 self.env.iter().collect()
281 }
282
283 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
285 self.bootnodes_addresses.iter().collect()
286 }
287
288 pub fn resources(&self) -> Option<&Resources> {
290 self.resources.as_ref()
291 }
292
293 pub fn ws_port(&self) -> Option<u16> {
295 self.ws_port
296 }
297
298 pub fn rpc_port(&self) -> Option<u16> {
300 self.rpc_port
301 }
302
303 pub fn prometheus_port(&self) -> Option<u16> {
305 self.prometheus_port
306 }
307
308 pub fn p2p_port(&self) -> Option<u16> {
310 self.p2p_port
311 }
312
313 pub fn p2p_cert_hash(&self) -> Option<&str> {
315 self.p2p_cert_hash.as_deref()
316 }
317
318 pub fn db_snapshot(&self) -> Option<&AssetLocation> {
320 self.db_snapshot.as_ref()
321 }
322
323 pub fn node_log_path(&self) -> Option<&PathBuf> {
325 self.node_log_path.as_ref()
326 }
327}
328
329pub struct NodeConfigBuilder<S> {
331 config: NodeConfig,
332 validation_context: Rc<RefCell<ValidationContext>>,
333 errors: Vec<anyhow::Error>,
334 _state: PhantomData<S>,
335}
336
337impl Default for NodeConfigBuilder<Initial> {
338 fn default() -> Self {
339 Self {
340 config: NodeConfig {
341 name: "".into(),
342 image: None,
343 command: None,
344 subcommand: None,
345 args: vec![],
346 is_validator: true,
347 is_invulnerable: true,
348 is_bootnode: false,
349 initial_balance: 2_000_000_000_000.into(),
350 env: vec![],
351 bootnodes_addresses: vec![],
352 resources: None,
353 ws_port: None,
354 rpc_port: None,
355 prometheus_port: None,
356 p2p_port: None,
357 p2p_cert_hash: None,
358 db_snapshot: None,
359 chain_context: Default::default(),
360 node_log_path: None,
361 },
362 validation_context: Default::default(),
363 errors: vec![],
364 _state: PhantomData,
365 }
366 }
367}
368
369impl<A> NodeConfigBuilder<A> {
370 fn transition<B>(
371 config: NodeConfig,
372 validation_context: Rc<RefCell<ValidationContext>>,
373 errors: Vec<anyhow::Error>,
374 ) -> NodeConfigBuilder<B> {
375 NodeConfigBuilder {
376 config,
377 validation_context,
378 errors,
379 _state: PhantomData,
380 }
381 }
382}
383
384impl NodeConfigBuilder<Initial> {
385 pub fn new(
386 chain_context: ChainDefaultContext,
387 validation_context: Rc<RefCell<ValidationContext>>,
388 ) -> Self {
389 Self::transition(
390 NodeConfig {
391 command: chain_context.default_command.clone(),
392 image: chain_context.default_image.clone(),
393 resources: chain_context.default_resources.clone(),
394 db_snapshot: chain_context.default_db_snapshot.clone(),
395 args: chain_context.default_args.clone(),
396 chain_context,
397 ..Self::default().config
398 },
399 validation_context,
400 vec![],
401 )
402 }
403
404 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> NodeConfigBuilder<Buildable> {
406 let name: String = generate_unique_node_name(name, self.validation_context.clone());
407
408 match ensure_value_is_not_empty(&name) {
409 Ok(_) => Self::transition(
410 NodeConfig {
411 name,
412 ..self.config
413 },
414 self.validation_context,
415 self.errors,
416 ),
417 Err(e) => Self::transition(
418 NodeConfig {
419 name,
421 ..self.config
422 },
423 self.validation_context,
424 merge_errors(self.errors, FieldError::Name(e).into()),
425 ),
426 }
427 }
428}
429
430impl NodeConfigBuilder<Buildable> {
431 pub fn with_command<T>(self, command: T) -> Self
433 where
434 T: TryInto<Command>,
435 T::Error: Error + Send + Sync + 'static,
436 {
437 match command.try_into() {
438 Ok(command) => Self::transition(
439 NodeConfig {
440 command: Some(command),
441 ..self.config
442 },
443 self.validation_context,
444 self.errors,
445 ),
446 Err(error) => Self::transition(
447 self.config,
448 self.validation_context,
449 merge_errors(self.errors, FieldError::Command(error.into()).into()),
450 ),
451 }
452 }
453
454 pub fn with_subcommand<T>(self, subcommand: T) -> Self
456 where
457 T: TryInto<Command>,
458 T::Error: Error + Send + Sync + 'static,
459 {
460 match subcommand.try_into() {
461 Ok(subcommand) => Self::transition(
462 NodeConfig {
463 subcommand: Some(subcommand),
464 ..self.config
465 },
466 self.validation_context,
467 self.errors,
468 ),
469 Err(error) => Self::transition(
470 self.config,
471 self.validation_context,
472 merge_errors(self.errors, FieldError::Command(error.into()).into()),
473 ),
474 }
475 }
476
477 pub fn with_image<T>(self, image: T) -> Self
479 where
480 T: TryInto<Image>,
481 T::Error: Error + Send + Sync + 'static,
482 {
483 match image.try_into() {
484 Ok(image) => Self::transition(
485 NodeConfig {
486 image: Some(image),
487 ..self.config
488 },
489 self.validation_context,
490 self.errors,
491 ),
492 Err(error) => Self::transition(
493 self.config,
494 self.validation_context,
495 merge_errors(self.errors, FieldError::Image(error.into()).into()),
496 ),
497 }
498 }
499
500 pub fn with_args(self, args: Vec<Arg>) -> Self {
502 Self::transition(
503 NodeConfig {
504 args,
505 ..self.config
506 },
507 self.validation_context,
508 self.errors,
509 )
510 }
511
512 pub fn validator(self, choice: bool) -> Self {
514 Self::transition(
515 NodeConfig {
516 is_validator: choice,
517 ..self.config
518 },
519 self.validation_context,
520 self.errors,
521 )
522 }
523
524 pub fn invulnerable(self, choice: bool) -> Self {
526 Self::transition(
527 NodeConfig {
528 is_invulnerable: choice,
529 ..self.config
530 },
531 self.validation_context,
532 self.errors,
533 )
534 }
535
536 pub fn bootnode(self, choice: bool) -> Self {
538 Self::transition(
539 NodeConfig {
540 is_bootnode: choice,
541 ..self.config
542 },
543 self.validation_context,
544 self.errors,
545 )
546 }
547
548 pub fn with_initial_balance(self, initial_balance: u128) -> Self {
550 Self::transition(
551 NodeConfig {
552 initial_balance: initial_balance.into(),
553 ..self.config
554 },
555 self.validation_context,
556 self.errors,
557 )
558 }
559
560 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
562 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
563
564 Self::transition(
565 NodeConfig { env, ..self.config },
566 self.validation_context,
567 self.errors,
568 )
569 }
570
571 pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
576 where
577 T: TryInto<Multiaddr> + Display + Copy,
578 T::Error: Error + Send + Sync + 'static,
579 {
580 let mut addrs = vec![];
581 let mut errors = vec![];
582
583 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
584 match addr.try_into() {
585 Ok(addr) => addrs.push(addr),
586 Err(error) => errors.push(
587 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
588 ),
589 }
590 }
591
592 Self::transition(
593 NodeConfig {
594 bootnodes_addresses: addrs,
595 ..self.config
596 },
597 self.validation_context,
598 merge_errors_vecs(self.errors, errors),
599 )
600 }
601
602 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
604 match f(ResourcesBuilder::new()).build() {
605 Ok(resources) => Self::transition(
606 NodeConfig {
607 resources: Some(resources),
608 ..self.config
609 },
610 self.validation_context,
611 self.errors,
612 ),
613 Err(errors) => Self::transition(
614 self.config,
615 self.validation_context,
616 merge_errors_vecs(
617 self.errors,
618 errors
619 .into_iter()
620 .map(|error| FieldError::Resources(error).into())
621 .collect::<Vec<_>>(),
622 ),
623 ),
624 }
625 }
626
627 pub fn with_ws_port(self, ws_port: Port) -> Self {
629 match ensure_port_unique(ws_port, self.validation_context.clone()) {
630 Ok(_) => Self::transition(
631 NodeConfig {
632 ws_port: Some(ws_port),
633 ..self.config
634 },
635 self.validation_context,
636 self.errors,
637 ),
638 Err(error) => Self::transition(
639 self.config,
640 self.validation_context,
641 merge_errors(self.errors, FieldError::WsPort(error).into()),
642 ),
643 }
644 }
645
646 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
648 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
649 Ok(_) => Self::transition(
650 NodeConfig {
651 rpc_port: Some(rpc_port),
652 ..self.config
653 },
654 self.validation_context,
655 self.errors,
656 ),
657 Err(error) => Self::transition(
658 self.config,
659 self.validation_context,
660 merge_errors(self.errors, FieldError::RpcPort(error).into()),
661 ),
662 }
663 }
664
665 pub fn with_prometheus_port(self, prometheus_port: Port) -> Self {
667 match ensure_port_unique(prometheus_port, self.validation_context.clone()) {
668 Ok(_) => Self::transition(
669 NodeConfig {
670 prometheus_port: Some(prometheus_port),
671 ..self.config
672 },
673 self.validation_context,
674 self.errors,
675 ),
676 Err(error) => Self::transition(
677 self.config,
678 self.validation_context,
679 merge_errors(self.errors, FieldError::PrometheusPort(error).into()),
680 ),
681 }
682 }
683
684 pub fn with_p2p_port(self, p2p_port: Port) -> Self {
686 match ensure_port_unique(p2p_port, self.validation_context.clone()) {
687 Ok(_) => Self::transition(
688 NodeConfig {
689 p2p_port: Some(p2p_port),
690 ..self.config
691 },
692 self.validation_context,
693 self.errors,
694 ),
695 Err(error) => Self::transition(
696 self.config,
697 self.validation_context,
698 merge_errors(self.errors, FieldError::P2pPort(error).into()),
699 ),
700 }
701 }
702
703 pub fn with_p2p_cert_hash(self, p2p_cert_hash: impl Into<String>) -> Self {
706 Self::transition(
707 NodeConfig {
708 p2p_cert_hash: Some(p2p_cert_hash.into()),
709 ..self.config
710 },
711 self.validation_context,
712 self.errors,
713 )
714 }
715
716 pub fn with_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
718 Self::transition(
719 NodeConfig {
720 db_snapshot: Some(location.into()),
721 ..self.config
722 },
723 self.validation_context,
724 self.errors,
725 )
726 }
727
728 pub fn with_log_path(self, log_path: impl Into<PathBuf>) -> Self {
730 Self::transition(
731 NodeConfig {
732 node_log_path: Some(log_path.into()),
733 ..self.config
734 },
735 self.validation_context,
736 self.errors,
737 )
738 }
739
740 pub fn build(self) -> Result<NodeConfig, (String, Vec<anyhow::Error>)> {
742 if !self.errors.is_empty() {
743 return Err((self.config.name.clone(), self.errors));
744 }
745
746 Ok(self.config)
747 }
748}
749
750pub struct GroupNodeConfigBuilder<S> {
752 base_config: NodeConfig,
753 count: usize,
754 validation_context: Rc<RefCell<ValidationContext>>,
755 errors: Vec<anyhow::Error>,
756 _state: PhantomData<S>,
757}
758
759impl GroupNodeConfigBuilder<Initial> {
760 pub fn new(
761 chain_context: ChainDefaultContext,
762 validation_context: Rc<RefCell<ValidationContext>>,
763 ) -> Self {
764 let (errors, base_config) = match NodeConfigBuilder::new(
765 chain_context.clone(),
766 validation_context.clone(),
767 )
768 .with_name(" ") .build()
770 {
771 Ok(base_config) => (vec![], base_config),
772 Err((_name, errors)) => (errors, NodeConfig::default()),
773 };
774
775 Self {
776 base_config,
777 count: 1,
778 validation_context,
779 errors,
780 _state: PhantomData,
781 }
782 }
783
784 pub fn with_base_node(
786 mut self,
787 f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
788 ) -> GroupNodeConfigBuilder<Buildable> {
789 match f(NodeConfigBuilder::new(
790 ChainDefaultContext::default(),
791 self.validation_context.clone(),
792 ))
793 .build()
794 {
795 Ok(node) => {
796 self.base_config = node;
797 GroupNodeConfigBuilder {
798 base_config: self.base_config,
799 count: self.count,
800 validation_context: self.validation_context,
801 errors: self.errors,
802 _state: PhantomData,
803 }
804 },
805 Err((_name, errors)) => {
806 self.errors.extend(errors);
807 GroupNodeConfigBuilder {
808 base_config: self.base_config,
809 count: self.count,
810 validation_context: self.validation_context,
811 errors: self.errors,
812 _state: PhantomData,
813 }
814 },
815 }
816 }
817
818 pub fn with_count(mut self, count: usize) -> Self {
820 self.count = count;
821 self
822 }
823}
824
825impl GroupNodeConfigBuilder<Buildable> {
826 pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
827 if self.count == 0 {
828 return Err((
829 self.base_config.name().to_string(),
830 vec![anyhow::anyhow!("Count cannot be zero")],
831 ));
832 }
833
834 if !self.errors.is_empty() {
835 return Err((self.base_config.name().to_string(), self.errors));
836 }
837
838 Ok(GroupNodeConfig {
839 base_config: self.base_config,
840 count: self.count,
841 })
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use std::collections::HashSet;
848
849 use super::*;
850
851 #[test]
852 fn node_config_builder_should_succeeds_and_returns_a_node_config() {
853 let node_config =
854 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
855 .with_name("node")
856 .with_command("mycommand")
857 .with_image("myrepo:myimage")
858 .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
859 .validator(true)
860 .invulnerable(true)
861 .bootnode(true)
862 .with_initial_balance(100_000_042)
863 .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
864 .with_raw_bootnodes_addresses(vec![
865 "/ip4/10.41.122.55/tcp/45421",
866 "/ip4/51.144.222.10/tcp/2333",
867 ])
868 .with_resources(|resources| {
869 resources
870 .with_request_cpu("200M")
871 .with_request_memory("500M")
872 .with_limit_cpu("1G")
873 .with_limit_memory("2G")
874 })
875 .with_ws_port(5000)
876 .with_rpc_port(6000)
877 .with_prometheus_port(7000)
878 .with_p2p_port(8000)
879 .with_p2p_cert_hash(
880 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
881 )
882 .with_db_snapshot("/tmp/mysnapshot")
883 .build()
884 .unwrap();
885
886 assert_eq!(node_config.name(), "node");
887 assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
888 assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
889 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
890 assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
891 assert!(node_config.is_validator());
892 assert!(node_config.is_invulnerable());
893 assert!(node_config.is_bootnode());
894 assert_eq!(node_config.initial_balance(), 100_000_042);
895 let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
896 assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
897 let bootnodes_addresses: Vec<Multiaddr> = vec![
898 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
899 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
900 ];
901 assert_eq!(
902 node_config.bootnodes_addresses(),
903 bootnodes_addresses.iter().collect::<Vec<_>>()
904 );
905 let resources = node_config.resources().unwrap();
906 assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
907 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
908 assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
909 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
910 assert_eq!(node_config.ws_port().unwrap(), 5000);
911 assert_eq!(node_config.rpc_port().unwrap(), 6000);
912 assert_eq!(node_config.prometheus_port().unwrap(), 7000);
913 assert_eq!(node_config.p2p_port().unwrap(), 8000);
914 assert_eq!(
915 node_config.p2p_cert_hash().unwrap(),
916 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
917 );
918 assert!(matches!(
919 node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
920 ));
921 }
922
923 #[test]
924 fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
925 let mut used_nodes_names = HashSet::new();
926 used_nodes_names.insert("mynode".into());
927 let validation_context = Rc::new(RefCell::new(ValidationContext {
928 used_nodes_names,
929 ..Default::default()
930 }));
931 let node_config =
932 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
933 .with_name("mynode")
934 .build()
935 .unwrap();
936
937 assert_eq!(node_config.name, "mynode-1");
938 }
939
940 #[test]
941 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
942 let (node_name, errors) =
943 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
944 .with_name("node")
945 .with_command("invalid command")
946 .build()
947 .unwrap_err();
948
949 assert_eq!(node_name, "node");
950 assert_eq!(errors.len(), 1);
951 assert_eq!(
952 errors.first().unwrap().to_string(),
953 "command: 'invalid command' shouldn't contains whitespace"
954 );
955 }
956
957 #[test]
958 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
959 let (node_name, errors) =
960 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
961 .with_name("node")
962 .with_image("myinvalid.image")
963 .build()
964 .unwrap_err();
965
966 assert_eq!(node_name, "node");
967 assert_eq!(errors.len(), 1);
968 assert_eq!(
969 errors.first().unwrap().to_string(),
970 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
971 );
972 }
973
974 #[test]
975 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
976 ) {
977 let (node_name, errors) =
978 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
979 .with_name("node")
980 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
981 .build()
982 .unwrap_err();
983
984 assert_eq!(node_name, "node");
985 assert_eq!(errors.len(), 1);
986 assert_eq!(
987 errors.first().unwrap().to_string(),
988 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
989 );
990 }
991
992 #[test]
993 fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
994 ) {
995 let (node_name, errors) =
996 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
997 .with_name("node")
998 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
999 .build()
1000 .unwrap_err();
1001
1002 assert_eq!(node_name, "node");
1003 assert_eq!(errors.len(), 2);
1004 assert_eq!(
1005 errors.first().unwrap().to_string(),
1006 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1007 );
1008 assert_eq!(
1009 errors.get(1).unwrap().to_string(),
1010 "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1011 );
1012 }
1013
1014 #[test]
1015 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1016 ) {
1017 let (node_name, errors) =
1018 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1019 .with_name("node")
1020 .with_resources(|resources| resources.with_limit_cpu("invalid"))
1021 .build()
1022 .unwrap_err();
1023
1024 assert_eq!(node_name, "node");
1025 assert_eq!(errors.len(), 1);
1026 assert_eq!(
1027 errors.first().unwrap().to_string(),
1028 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1029 );
1030 }
1031
1032 #[test]
1033 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1034 ) {
1035 let (node_name, errors) =
1036 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1037 .with_name("node")
1038 .with_resources(|resources| {
1039 resources
1040 .with_limit_cpu("invalid")
1041 .with_request_memory("invalid")
1042 })
1043 .build()
1044 .unwrap_err();
1045
1046 assert_eq!(node_name, "node");
1047 assert_eq!(errors.len(), 2);
1048 assert_eq!(
1049 errors.first().unwrap().to_string(),
1050 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1051 );
1052 assert_eq!(
1053 errors.get(1).unwrap().to_string(),
1054 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1055 );
1056 }
1057
1058 #[test]
1059 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1060 ) {
1061 let (node_name, errors) =
1062 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1063 .with_name("node")
1064 .with_command("invalid command")
1065 .with_image("myinvalid.image")
1066 .with_resources(|resources| {
1067 resources
1068 .with_limit_cpu("invalid")
1069 .with_request_memory("invalid")
1070 })
1071 .build()
1072 .unwrap_err();
1073
1074 assert_eq!(node_name, "node");
1075 assert_eq!(errors.len(), 4);
1076 assert_eq!(
1077 errors.first().unwrap().to_string(),
1078 "command: 'invalid command' shouldn't contains whitespace"
1079 );
1080 assert_eq!(
1081 errors.get(1).unwrap().to_string(),
1082 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1083 );
1084 assert_eq!(
1085 errors.get(2).unwrap().to_string(),
1086 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1087 );
1088 assert_eq!(
1089 errors.get(3).unwrap().to_string(),
1090 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1091 );
1092 }
1093
1094 #[test]
1095 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1096 ) {
1097 let validation_context = Rc::new(RefCell::new(ValidationContext {
1098 used_ports: vec![30333],
1099 ..Default::default()
1100 }));
1101 let (node_name, errors) =
1102 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1103 .with_name("node")
1104 .with_ws_port(30333)
1105 .build()
1106 .unwrap_err();
1107
1108 assert_eq!(node_name, "node");
1109 assert_eq!(errors.len(), 1);
1110 assert_eq!(
1111 errors.first().unwrap().to_string(),
1112 "ws_port: '30333' is already used across config"
1113 );
1114 }
1115
1116 #[test]
1117 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1118 ) {
1119 let validation_context = Rc::new(RefCell::new(ValidationContext {
1120 used_ports: vec![4444],
1121 ..Default::default()
1122 }));
1123 let (node_name, errors) =
1124 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1125 .with_name("node")
1126 .with_rpc_port(4444)
1127 .build()
1128 .unwrap_err();
1129
1130 assert_eq!(node_name, "node");
1131 assert_eq!(errors.len(), 1);
1132 assert_eq!(
1133 errors.first().unwrap().to_string(),
1134 "rpc_port: '4444' is already used across config"
1135 );
1136 }
1137
1138 #[test]
1139 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1140 ) {
1141 let validation_context = Rc::new(RefCell::new(ValidationContext {
1142 used_ports: vec![9089],
1143 ..Default::default()
1144 }));
1145 let (node_name, errors) =
1146 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1147 .with_name("node")
1148 .with_prometheus_port(9089)
1149 .build()
1150 .unwrap_err();
1151
1152 assert_eq!(node_name, "node");
1153 assert_eq!(errors.len(), 1);
1154 assert_eq!(
1155 errors.first().unwrap().to_string(),
1156 "prometheus_port: '9089' is already used across config"
1157 );
1158 }
1159
1160 #[test]
1161 fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1162 ) {
1163 let validation_context = Rc::new(RefCell::new(ValidationContext {
1164 used_ports: vec![45093],
1165 ..Default::default()
1166 }));
1167 let (node_name, errors) =
1168 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1169 .with_name("node")
1170 .with_p2p_port(45093)
1171 .build()
1172 .unwrap_err();
1173
1174 assert_eq!(node_name, "node");
1175 assert_eq!(errors.len(), 1);
1176 assert_eq!(
1177 errors.first().unwrap().to_string(),
1178 "p2p_port: '45093' is already used across config"
1179 );
1180 }
1181
1182 #[test]
1183 fn node_config_builder_should_fails_if_node_name_is_empty() {
1184 let validation_context = Rc::new(RefCell::new(ValidationContext {
1185 ..Default::default()
1186 }));
1187
1188 let (_, errors) =
1189 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1190 .with_name("")
1191 .build()
1192 .unwrap_err();
1193
1194 assert_eq!(errors.len(), 1);
1195 assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1196 }
1197
1198 #[test]
1199 fn group_default_base_node() {
1200 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1201
1202 let group_config =
1203 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1204 .with_base_node(|node| node.with_name("validator"))
1205 .build()
1206 .unwrap();
1207
1208 assert_eq!(group_config.count, 1);
1210 assert_eq!(group_config.base_config.name(), "validator");
1211 }
1212
1213 #[test]
1214 fn group_custom_base_node() {
1215 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1216 let node_config =
1217 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1218 .with_name("node")
1219 .with_command("some_command")
1220 .with_image("repo:image")
1221 .validator(true)
1222 .invulnerable(true)
1223 .bootnode(true);
1224
1225 let group_config =
1226 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1227 .with_count(5)
1228 .with_base_node(|_node| node_config)
1229 .build()
1230 .unwrap();
1231
1232 assert_eq!(group_config.count, 5);
1234
1235 assert_eq!(group_config.base_config.name(), "node");
1236 assert_eq!(
1237 group_config.base_config.command().unwrap().as_str(),
1238 "some_command"
1239 );
1240 assert_eq!(
1241 group_config.base_config.image().unwrap().as_str(),
1242 "repo:image"
1243 );
1244 assert!(group_config.base_config.is_validator());
1245 assert!(group_config.base_config.is_invulnerable());
1246 assert!(group_config.base_config.is_bootnode());
1247 }
1248}