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