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 types::JamNodeMode,
22 utils::{default_as_true, default_initial_balance},
23};
24
25states! {
26 Buildable,
27 Initial
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct EnvVar {
50 pub name: String,
52
53 pub value: String,
55}
56
57impl From<(&str, &str)> for EnvVar {
58 fn from((name, value): (&str, &str)) -> Self {
59 Self {
60 name: name.to_owned(),
61 value: value.to_owned(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
68pub struct BaseNodeConfig {
69 pub(crate) name: String,
70 pub(crate) image: Option<Image>,
71 pub(crate) command: Option<Command>,
72 pub(crate) subcommand: Option<Command>,
73 #[serde(default)]
74 pub(crate) args: Vec<Arg>,
75 #[serde(default)]
76 pub(crate) env: Vec<EnvVar>,
77 pub(crate) resources: Option<Resources>,
78 #[serde(default)]
79 pub(crate) chain_context: ChainDefaultContext,
81}
82
83impl Serialize for BaseNodeConfig {
84 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85 where
86 S: serde::Serializer,
87 {
88 let mut state = serializer.serialize_struct("BaseNodeConfig", 1)?;
89 state.serialize_field("name", &self.name)?;
90
91 if self.image == self.chain_context.default_image {
92 state.skip_field("image")?;
93 } else {
94 state.serialize_field("image", &self.image)?;
95 }
96
97 if self.command == self.chain_context.default_command {
98 state.skip_field("command")?;
99 } else {
100 state.serialize_field("command", &self.command)?;
101 }
102
103 if self.subcommand.is_none() {
104 state.skip_field("subcommand")?;
105 } else {
106 state.serialize_field("subcommand", &self.subcommand)?;
107 }
108
109 if self.args.is_empty() || self.args == self.chain_context.default_args {
110 state.skip_field("args")?;
111 } else {
112 state.serialize_field("args", &self.args)?;
113 }
114
115 if self.env.is_empty() {
116 state.skip_field("env")?;
117 } else {
118 state.serialize_field("env", &self.env)?;
119 }
120
121 if self.resources == self.chain_context.default_resources {
122 state.skip_field("resources")?;
123 } else {
124 state.serialize_field("resources", &self.resources)?;
125 }
126
127 state.skip_field("chain_context")?;
128 state.end()
129 }
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
134pub struct JamNodeConfig {
135 #[serde(flatten)]
136 pub(crate) base_config: BaseNodeConfig,
137 pub(crate) mode: JamNodeMode,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub(crate) rpc_port: Option<Port>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub(crate) telemetry_endpoint: Option<String>,
145}
146
147impl JamNodeConfig {
148 pub fn name(&self) -> &str {
150 &self.base_config.name
151 }
152
153 pub fn image(&self) -> Option<&Image> {
155 self.base_config.image.as_ref()
156 }
157
158 pub fn command(&self) -> Option<&Command> {
160 self.base_config.command.as_ref()
161 }
162
163 pub fn subcommand(&self) -> Option<&Command> {
165 self.base_config.subcommand.as_ref()
166 }
167
168 pub fn args(&self) -> Vec<&Arg> {
170 self.base_config.args.iter().collect()
171 }
172
173 pub fn resources(&self) -> Option<&Resources> {
180 self.base_config.resources.as_ref()
181 }
182
183 pub fn env(&self) -> Vec<&EnvVar> {
185 self.base_config.env.iter().collect()
186 }
187
188 pub fn mode(&self) -> &JamNodeMode {
190 &self.mode
191 }
192
193 pub fn rpc_port(&self) -> Option<u16> {
195 self.rpc_port
196 }
197
198 pub fn telemetry_endpoint(&self) -> Option<&str> {
200 self.telemetry_endpoint.as_deref()
201 }
202}
203#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
205pub struct NodeConfig {
206 pub(crate) name: String,
207 pub(crate) image: Option<Image>,
208 pub(crate) command: Option<Command>,
209 pub(crate) subcommand: Option<Command>,
210 #[serde(default)]
211 args: Vec<Arg>,
212 #[serde(alias = "validator", default = "default_as_true")]
213 pub(crate) is_validator: bool,
214 #[serde(alias = "invulnerable", default = "default_as_true")]
215 pub(crate) is_invulnerable: bool,
216 #[serde(alias = "bootnode", default)]
217 pub(crate) is_bootnode: bool,
218 #[serde(alias = "balance")]
219 #[serde(default = "default_initial_balance")]
220 initial_balance: U128,
221 #[serde(default)]
222 env: Vec<EnvVar>,
223 #[serde(default)]
224 bootnodes_addresses: Vec<Multiaddr>,
225 pub(crate) resources: Option<Resources>,
226 ws_port: Option<Port>,
227 rpc_port: Option<Port>,
228 prometheus_port: Option<Port>,
229 p2p_port: Option<Port>,
230 p2p_cert_hash: Option<String>,
231 pub(crate) db_snapshot: Option<AssetLocation>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
235 override_eth_key: Option<String>,
236 #[serde(default)]
237 pub(crate) chain_context: ChainDefaultContext,
239 pub(crate) node_log_path: Option<PathBuf>,
240 keystore_path: Option<PathBuf>,
242 #[serde(default)]
246 keystore_key_types: Vec<String>,
247 #[serde(default)]
252 chain_spec_key_types: Vec<String>,
253}
254
255impl Serialize for NodeConfig {
256 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
257 where
258 S: serde::Serializer,
259 {
260 let mut state = serializer.serialize_struct("NodeConfig", 19)?;
261 state.serialize_field("name", &self.name)?;
262
263 if self.image == self.chain_context.default_image {
264 state.skip_field("image")?;
265 } else {
266 state.serialize_field("image", &self.image)?;
267 }
268
269 if self.command == self.chain_context.default_command {
270 state.skip_field("command")?;
271 } else {
272 state.serialize_field("command", &self.command)?;
273 }
274
275 if self.subcommand.is_none() {
276 state.skip_field("subcommand")?;
277 } else {
278 state.serialize_field("subcommand", &self.subcommand)?;
279 }
280
281 if self.args.is_empty() || self.args == self.chain_context.default_args {
282 state.skip_field("args")?;
283 } else {
284 state.serialize_field("args", &self.args)?;
285 }
286
287 state.serialize_field("validator", &self.is_validator)?;
288 state.serialize_field("invulnerable", &self.is_invulnerable)?;
289 state.serialize_field("bootnode", &self.is_bootnode)?;
290 state.serialize_field("balance", &self.initial_balance)?;
291
292 if self.env.is_empty() {
293 state.skip_field("env")?;
294 } else {
295 state.serialize_field("env", &self.env)?;
296 }
297
298 if self.bootnodes_addresses.is_empty() {
299 state.skip_field("bootnodes_addresses")?;
300 } else {
301 state.serialize_field("bootnodes_addresses", &self.bootnodes_addresses)?;
302 }
303
304 if self.resources == self.chain_context.default_resources {
305 state.skip_field("resources")?;
306 } else {
307 state.serialize_field("resources", &self.resources)?;
308 }
309
310 state.serialize_field("ws_port", &self.ws_port)?;
311 state.serialize_field("rpc_port", &self.rpc_port)?;
312 state.serialize_field("prometheus_port", &self.prometheus_port)?;
313 state.serialize_field("p2p_port", &self.p2p_port)?;
314 state.serialize_field("p2p_cert_hash", &self.p2p_cert_hash)?;
315 state.serialize_field("override_eth_key", &self.override_eth_key)?;
316
317 if self.db_snapshot == self.chain_context.default_db_snapshot {
318 state.skip_field("db_snapshot")?;
319 } else {
320 state.serialize_field("db_snapshot", &self.db_snapshot)?;
321 }
322
323 if self.node_log_path.is_none() {
324 state.skip_field("node_log_path")?;
325 } else {
326 state.serialize_field("node_log_path", &self.node_log_path)?;
327 }
328
329 if self.keystore_path.is_none() {
330 state.skip_field("keystore_path")?;
331 } else {
332 state.serialize_field("keystore_path", &self.keystore_path)?;
333 }
334
335 if self.keystore_key_types.is_empty() {
336 state.skip_field("keystore_key_types")?;
337 } else {
338 state.serialize_field("keystore_key_types", &self.keystore_key_types)?;
339 }
340
341 if self.chain_spec_key_types.is_empty() {
342 state.skip_field("chain_spec_key_typese")?;
343 } else {
344 state.serialize_field("chain_spec_key_types", &self.chain_spec_key_types)?;
345 }
346
347 state.skip_field("chain_context")?;
348 state.end()
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Deserialize)]
354pub struct GroupNodeConfig {
355 #[serde(flatten)]
356 pub(crate) base_config: NodeConfig,
357 pub(crate) count: usize,
358}
359
360impl GroupNodeConfig {
361 pub fn expand_group_configs(&self) -> Vec<NodeConfig> {
364 let mut used_names = std::collections::HashSet::new();
365
366 (0..self.count)
367 .map(|i| {
368 let mut node = self.base_config.clone();
369 let node_name = format!("{}-{i}", node.name);
371
372 let unique_name = generate_unique_node_name_from_names(node_name, &mut used_names);
373 node.name = unique_name;
374
375 if let Some(ref base_log_path) = node.node_log_path {
377 let unique_log_path = if let Some(parent) = base_log_path.parent() {
378 parent.join(format!("{}.log", node.name))
379 } else {
380 PathBuf::from(format!("{}.log", node.name))
381 };
382 node.node_log_path = Some(unique_log_path);
383 }
384
385 node
386 })
387 .collect()
388 }
389}
390
391impl Serialize for GroupNodeConfig {
392 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
393 where
394 S: serde::Serializer,
395 {
396 let mut state = serializer.serialize_struct("GroupNodeConfig", 18)?;
397 state.serialize_field("NodeConfig", &self.base_config)?;
398 state.serialize_field("count", &self.count)?;
399 state.end()
400 }
401}
402
403impl NodeConfig {
404 pub fn name(&self) -> &str {
406 &self.name
407 }
408
409 pub fn image(&self) -> Option<&Image> {
411 self.image.as_ref()
412 }
413
414 pub fn command(&self) -> Option<&Command> {
416 self.command.as_ref()
417 }
418
419 pub fn subcommand(&self) -> Option<&Command> {
421 self.subcommand.as_ref()
422 }
423
424 pub fn args(&self) -> Vec<&Arg> {
426 self.args.iter().collect()
427 }
428
429 pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
431 self.args = args;
432 }
433
434 pub fn is_validator(&self) -> bool {
436 self.is_validator
437 }
438
439 pub fn is_invulnerable(&self) -> bool {
441 self.is_invulnerable
442 }
443
444 pub fn is_bootnode(&self) -> bool {
446 self.is_bootnode
447 }
448
449 pub fn initial_balance(&self) -> u128 {
451 self.initial_balance.0
452 }
453
454 pub fn env(&self) -> Vec<&EnvVar> {
456 self.env.iter().collect()
457 }
458
459 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
461 self.bootnodes_addresses.iter().collect()
462 }
463
464 pub fn resources(&self) -> Option<&Resources> {
466 self.resources.as_ref()
467 }
468
469 pub fn ws_port(&self) -> Option<u16> {
471 self.ws_port
472 }
473
474 pub fn rpc_port(&self) -> Option<u16> {
476 self.rpc_port
477 }
478
479 pub fn prometheus_port(&self) -> Option<u16> {
481 self.prometheus_port
482 }
483
484 pub fn p2p_port(&self) -> Option<u16> {
486 self.p2p_port
487 }
488
489 pub fn p2p_cert_hash(&self) -> Option<&str> {
491 self.p2p_cert_hash.as_deref()
492 }
493
494 pub fn db_snapshot(&self) -> Option<&AssetLocation> {
496 self.db_snapshot.as_ref()
497 }
498
499 pub fn node_log_path(&self) -> Option<&PathBuf> {
501 self.node_log_path.as_ref()
502 }
503
504 pub fn keystore_path(&self) -> Option<&PathBuf> {
506 self.keystore_path.as_ref()
507 }
508
509 pub fn override_eth_key(&self) -> Option<&str> {
511 self.override_eth_key.as_deref()
512 }
513
514 pub fn keystore_key_types(&self) -> Vec<&str> {
517 self.keystore_key_types.iter().map(String::as_str).collect()
518 }
519
520 pub fn chain_spec_key_types(&self) -> Vec<&str> {
523 self.chain_spec_key_types
524 .iter()
525 .map(String::as_str)
526 .collect()
527 }
528}
529
530pub struct NodeConfigBuilder<S> {
532 config: NodeConfig,
533 validation_context: Rc<RefCell<ValidationContext>>,
534 errors: Vec<anyhow::Error>,
535 _state: PhantomData<S>,
536}
537
538impl Default for NodeConfigBuilder<Initial> {
539 fn default() -> Self {
540 Self {
541 config: NodeConfig {
542 name: "".into(),
543 image: None,
544 command: None,
545 subcommand: None,
546 args: vec![],
547 is_validator: true,
548 is_invulnerable: true,
549 is_bootnode: false,
550 initial_balance: 2_000_000_000_000.into(),
551 env: vec![],
552 bootnodes_addresses: vec![],
553 resources: None,
554 ws_port: None,
555 rpc_port: None,
556 prometheus_port: None,
557 p2p_port: None,
558 p2p_cert_hash: None,
559 db_snapshot: None,
560 override_eth_key: None,
561 chain_context: Default::default(),
562 node_log_path: None,
563 keystore_path: None,
564 keystore_key_types: vec![],
565 chain_spec_key_types: vec![],
566 },
567 validation_context: Default::default(),
568 errors: vec![],
569 _state: PhantomData,
570 }
571 }
572}
573
574impl<A> NodeConfigBuilder<A> {
575 fn transition<B>(
576 config: NodeConfig,
577 validation_context: Rc<RefCell<ValidationContext>>,
578 errors: Vec<anyhow::Error>,
579 ) -> NodeConfigBuilder<B> {
580 NodeConfigBuilder {
581 config,
582 validation_context,
583 errors,
584 _state: PhantomData,
585 }
586 }
587}
588
589impl NodeConfigBuilder<Initial> {
590 pub fn new(
591 chain_context: ChainDefaultContext,
592 validation_context: Rc<RefCell<ValidationContext>>,
593 ) -> Self {
594 Self::transition(
595 NodeConfig {
596 command: chain_context.default_command.clone(),
597 image: chain_context.default_image.clone(),
598 resources: chain_context.default_resources.clone(),
599 db_snapshot: chain_context.default_db_snapshot.clone(),
600 args: chain_context.default_args.clone(),
601 chain_context,
602 ..Self::default().config
603 },
604 validation_context,
605 vec![],
606 )
607 }
608
609 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> NodeConfigBuilder<Buildable> {
611 let name: String = generate_unique_node_name(name, self.validation_context.clone());
612
613 match ensure_value_is_not_empty(&name) {
614 Ok(_) => Self::transition(
615 NodeConfig {
616 name,
617 ..self.config
618 },
619 self.validation_context,
620 self.errors,
621 ),
622 Err(e) => Self::transition(
623 NodeConfig {
624 name,
626 ..self.config
627 },
628 self.validation_context,
629 merge_errors(self.errors, FieldError::Name(e).into()),
630 ),
631 }
632 }
633}
634
635impl NodeConfigBuilder<Buildable> {
636 pub fn with_command<T>(self, command: T) -> Self
638 where
639 T: TryInto<Command>,
640 T::Error: Error + Send + Sync + 'static,
641 {
642 match command.try_into() {
643 Ok(command) => Self::transition(
644 NodeConfig {
645 command: Some(command),
646 ..self.config
647 },
648 self.validation_context,
649 self.errors,
650 ),
651 Err(error) => Self::transition(
652 self.config,
653 self.validation_context,
654 merge_errors(self.errors, FieldError::Command(error.into()).into()),
655 ),
656 }
657 }
658
659 pub fn with_subcommand<T>(self, subcommand: T) -> Self
661 where
662 T: TryInto<Command>,
663 T::Error: Error + Send + Sync + 'static,
664 {
665 match subcommand.try_into() {
666 Ok(subcommand) => Self::transition(
667 NodeConfig {
668 subcommand: Some(subcommand),
669 ..self.config
670 },
671 self.validation_context,
672 self.errors,
673 ),
674 Err(error) => Self::transition(
675 self.config,
676 self.validation_context,
677 merge_errors(self.errors, FieldError::Command(error.into()).into()),
678 ),
679 }
680 }
681
682 pub fn with_image<T>(self, image: T) -> Self
684 where
685 T: TryInto<Image>,
686 T::Error: Error + Send + Sync + 'static,
687 {
688 match image.try_into() {
689 Ok(image) => Self::transition(
690 NodeConfig {
691 image: Some(image),
692 ..self.config
693 },
694 self.validation_context,
695 self.errors,
696 ),
697 Err(error) => Self::transition(
698 self.config,
699 self.validation_context,
700 merge_errors(self.errors, FieldError::Image(error.into()).into()),
701 ),
702 }
703 }
704
705 pub fn with_args(self, args: Vec<Arg>) -> Self {
707 Self::transition(
708 NodeConfig {
709 args,
710 ..self.config
711 },
712 self.validation_context,
713 self.errors,
714 )
715 }
716
717 pub fn validator(self, choice: bool) -> Self {
719 Self::transition(
720 NodeConfig {
721 is_validator: choice,
722 ..self.config
723 },
724 self.validation_context,
725 self.errors,
726 )
727 }
728
729 pub fn invulnerable(self, choice: bool) -> Self {
731 Self::transition(
732 NodeConfig {
733 is_invulnerable: choice,
734 ..self.config
735 },
736 self.validation_context,
737 self.errors,
738 )
739 }
740
741 pub fn bootnode(self, choice: bool) -> Self {
743 Self::transition(
744 NodeConfig {
745 is_bootnode: choice,
746 ..self.config
747 },
748 self.validation_context,
749 self.errors,
750 )
751 }
752
753 pub fn with_override_eth_key(self, session_key: impl Into<String>) -> Self {
755 Self::transition(
756 NodeConfig {
757 override_eth_key: Some(session_key.into()),
758 ..self.config
759 },
760 self.validation_context,
761 self.errors,
762 )
763 }
764
765 pub fn with_initial_balance(self, initial_balance: u128) -> Self {
767 Self::transition(
768 NodeConfig {
769 initial_balance: initial_balance.into(),
770 ..self.config
771 },
772 self.validation_context,
773 self.errors,
774 )
775 }
776
777 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
779 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
780
781 Self::transition(
782 NodeConfig { env, ..self.config },
783 self.validation_context,
784 self.errors,
785 )
786 }
787
788 pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
793 where
794 T: TryInto<Multiaddr> + Display + Copy,
795 T::Error: Error + Send + Sync + 'static,
796 {
797 let mut addrs = vec![];
798 let mut errors = vec![];
799
800 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
801 match addr.try_into() {
802 Ok(addr) => addrs.push(addr),
803 Err(error) => errors.push(
804 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
805 ),
806 }
807 }
808
809 Self::transition(
810 NodeConfig {
811 bootnodes_addresses: addrs,
812 ..self.config
813 },
814 self.validation_context,
815 merge_errors_vecs(self.errors, errors),
816 )
817 }
818
819 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
821 match f(ResourcesBuilder::new()).build() {
822 Ok(resources) => Self::transition(
823 NodeConfig {
824 resources: Some(resources),
825 ..self.config
826 },
827 self.validation_context,
828 self.errors,
829 ),
830 Err(errors) => Self::transition(
831 self.config,
832 self.validation_context,
833 merge_errors_vecs(
834 self.errors,
835 errors
836 .into_iter()
837 .map(|error| FieldError::Resources(error).into())
838 .collect::<Vec<_>>(),
839 ),
840 ),
841 }
842 }
843
844 pub fn with_ws_port(self, ws_port: Port) -> Self {
846 match ensure_port_unique(ws_port, self.validation_context.clone()) {
847 Ok(_) => Self::transition(
848 NodeConfig {
849 ws_port: Some(ws_port),
850 ..self.config
851 },
852 self.validation_context,
853 self.errors,
854 ),
855 Err(error) => Self::transition(
856 self.config,
857 self.validation_context,
858 merge_errors(self.errors, FieldError::WsPort(error).into()),
859 ),
860 }
861 }
862
863 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
865 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
866 Ok(_) => Self::transition(
867 NodeConfig {
868 rpc_port: Some(rpc_port),
869 ..self.config
870 },
871 self.validation_context,
872 self.errors,
873 ),
874 Err(error) => Self::transition(
875 self.config,
876 self.validation_context,
877 merge_errors(self.errors, FieldError::RpcPort(error).into()),
878 ),
879 }
880 }
881
882 pub fn with_prometheus_port(self, prometheus_port: Port) -> Self {
884 match ensure_port_unique(prometheus_port, self.validation_context.clone()) {
885 Ok(_) => Self::transition(
886 NodeConfig {
887 prometheus_port: Some(prometheus_port),
888 ..self.config
889 },
890 self.validation_context,
891 self.errors,
892 ),
893 Err(error) => Self::transition(
894 self.config,
895 self.validation_context,
896 merge_errors(self.errors, FieldError::PrometheusPort(error).into()),
897 ),
898 }
899 }
900
901 pub fn with_p2p_port(self, p2p_port: Port) -> Self {
903 match ensure_port_unique(p2p_port, self.validation_context.clone()) {
904 Ok(_) => Self::transition(
905 NodeConfig {
906 p2p_port: Some(p2p_port),
907 ..self.config
908 },
909 self.validation_context,
910 self.errors,
911 ),
912 Err(error) => Self::transition(
913 self.config,
914 self.validation_context,
915 merge_errors(self.errors, FieldError::P2pPort(error).into()),
916 ),
917 }
918 }
919
920 pub fn with_p2p_cert_hash(self, p2p_cert_hash: impl Into<String>) -> Self {
923 Self::transition(
924 NodeConfig {
925 p2p_cert_hash: Some(p2p_cert_hash.into()),
926 ..self.config
927 },
928 self.validation_context,
929 self.errors,
930 )
931 }
932
933 pub fn with_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
935 Self::transition(
936 NodeConfig {
937 db_snapshot: Some(location.into()),
938 ..self.config
939 },
940 self.validation_context,
941 self.errors,
942 )
943 }
944
945 pub fn with_optional_db_snapshot(self, location: Option<impl Into<AssetLocation>>) -> Self {
949 match location {
950 Some(location) => self.with_db_snapshot(location),
951 None => self,
952 }
953 }
954
955 pub fn with_log_path(self, log_path: impl Into<PathBuf>) -> Self {
957 Self::transition(
958 NodeConfig {
959 node_log_path: Some(log_path.into()),
960 ..self.config
961 },
962 self.validation_context,
963 self.errors,
964 )
965 }
966
967 pub fn with_keystore_path(self, keystore_path: impl Into<PathBuf>) -> Self {
969 Self::transition(
970 NodeConfig {
971 keystore_path: Some(keystore_path.into()),
972 ..self.config
973 },
974 self.validation_context,
975 self.errors,
976 )
977 }
978
979 pub fn with_keystore_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
999 Self::transition(
1000 NodeConfig {
1001 keystore_key_types: key_types.into_iter().map(|k| k.into()).collect(),
1002 ..self.config
1003 },
1004 self.validation_context,
1005 self.errors,
1006 )
1007 }
1008
1009 pub fn with_chain_spec_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
1035 Self::transition(
1036 NodeConfig {
1037 chain_spec_key_types: key_types.into_iter().map(|k| k.into()).collect(),
1038 ..self.config
1039 },
1040 self.validation_context,
1041 self.errors,
1042 )
1043 }
1044
1045 pub fn build(self) -> Result<NodeConfig, (String, Vec<anyhow::Error>)> {
1047 if !self.errors.is_empty() {
1048 return Err((self.config.name.clone(), self.errors));
1049 }
1050
1051 Ok(self.config)
1052 }
1053}
1054
1055pub struct GroupNodeConfigBuilder<S> {
1057 base_config: NodeConfig,
1058 count: usize,
1059 validation_context: Rc<RefCell<ValidationContext>>,
1060 errors: Vec<anyhow::Error>,
1061 _state: PhantomData<S>,
1062}
1063
1064impl GroupNodeConfigBuilder<Initial> {
1065 pub fn new(
1066 chain_context: ChainDefaultContext,
1067 validation_context: Rc<RefCell<ValidationContext>>,
1068 ) -> Self {
1069 let (errors, base_config) =
1070 match NodeConfigBuilder::new(chain_context.clone(), validation_context.clone())
1071 .with_name(" ") .build()
1073 {
1074 Ok(base_config) => (vec![], base_config),
1075 Err((_name, errors)) => (errors, NodeConfig::default()),
1076 };
1077
1078 Self {
1079 base_config,
1080 count: 1,
1081 validation_context,
1082 errors,
1083 _state: PhantomData,
1084 }
1085 }
1086
1087 pub fn with_base_node(
1089 mut self,
1090 f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
1091 ) -> GroupNodeConfigBuilder<Buildable> {
1092 match f(NodeConfigBuilder::new(
1093 ChainDefaultContext::default(),
1094 self.validation_context.clone(),
1095 ))
1096 .build()
1097 {
1098 Ok(node) => {
1099 self.base_config = node;
1100 GroupNodeConfigBuilder {
1101 base_config: self.base_config,
1102 count: self.count,
1103 validation_context: self.validation_context,
1104 errors: self.errors,
1105 _state: PhantomData,
1106 }
1107 },
1108 Err((_name, errors)) => {
1109 self.errors.extend(errors);
1110 GroupNodeConfigBuilder {
1111 base_config: self.base_config,
1112 count: self.count,
1113 validation_context: self.validation_context,
1114 errors: self.errors,
1115 _state: PhantomData,
1116 }
1117 },
1118 }
1119 }
1120
1121 pub fn with_count(mut self, count: usize) -> Self {
1123 self.count = count;
1124 self
1125 }
1126}
1127
1128impl GroupNodeConfigBuilder<Buildable> {
1129 pub fn with_count(mut self, count: usize) -> Self {
1131 self.count = count;
1132 self
1133 }
1134
1135 pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
1136 if self.count == 0 {
1137 return Err((
1138 self.base_config.name().to_string(),
1139 vec![anyhow::anyhow!("Count cannot be zero")],
1140 ));
1141 }
1142
1143 if !self.errors.is_empty() {
1144 return Err((self.base_config.name().to_string(), self.errors));
1145 }
1146
1147 Ok(GroupNodeConfig {
1148 base_config: self.base_config,
1149 count: self.count,
1150 })
1151 }
1152}
1153
1154pub struct JamNodeConfigBuilder<S> {
1157 config: JamNodeConfig,
1158 validation_context: Rc<RefCell<ValidationContext>>,
1159 errors: Vec<anyhow::Error>,
1160 _state: PhantomData<S>,
1161}
1162
1163impl Default for JamNodeConfigBuilder<Initial> {
1164 fn default() -> Self {
1165 Self {
1166 config: JamNodeConfig {
1167 rpc_port: None,
1168 base_config: BaseNodeConfig {
1169 name: "".into(), image: None,
1171 command: None,
1172 subcommand: None,
1173 args: vec![],
1174 env: vec![],
1175 resources: None,
1176 chain_context: Default::default(),
1177 },
1178 mode: JamNodeMode::Validator, telemetry_endpoint: None,
1180 },
1181 validation_context: Default::default(),
1182 errors: vec![],
1183 _state: PhantomData,
1184 }
1185 }
1186}
1187
1188impl<A> JamNodeConfigBuilder<A> {
1189 fn transition<B>(
1190 config: JamNodeConfig,
1191 validation_context: Rc<RefCell<ValidationContext>>,
1192 errors: Vec<anyhow::Error>,
1193 ) -> JamNodeConfigBuilder<B> {
1194 JamNodeConfigBuilder {
1195 config,
1196 validation_context,
1197 errors,
1198 _state: PhantomData,
1199 }
1200 }
1201}
1202
1203impl JamNodeConfigBuilder<Initial> {
1204 pub fn new(
1205 chain_context: ChainDefaultContext,
1206 validation_context: Rc<RefCell<ValidationContext>>,
1207 ) -> Self {
1208 let base_config = BaseNodeConfig {
1209 command: chain_context.default_command.clone(),
1210 image: chain_context.default_image.clone(),
1211 resources: chain_context.default_resources.clone(),
1212 args: chain_context.default_args.clone(),
1213 chain_context,
1214 ..Default::default()
1215 };
1216
1217 Self::transition(
1218 JamNodeConfig {
1219 base_config,
1220 ..JamNodeConfig::default()
1221 },
1222 validation_context,
1223 vec![],
1224 )
1225 }
1226
1227 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> JamNodeConfigBuilder<Buildable> {
1229 let name: String = generate_unique_node_name(name, self.validation_context.clone());
1230
1231 match ensure_value_is_not_empty(&name) {
1232 Ok(_) => Self::transition(
1233 JamNodeConfig {
1234 base_config: BaseNodeConfig {
1235 name,
1236 ..self.config.base_config
1237 },
1238 ..self.config
1239 },
1240 self.validation_context,
1241 self.errors,
1242 ),
1243 Err(e) => Self::transition(
1244 JamNodeConfig {
1245 base_config: BaseNodeConfig {
1247 name,
1248 ..self.config.base_config
1249 },
1250 ..self.config
1251 },
1252 self.validation_context,
1253 merge_errors(self.errors, FieldError::Name(e).into()),
1254 ),
1255 }
1256 }
1257}
1258
1259impl JamNodeConfigBuilder<Buildable> {
1260 pub fn with_command<T>(self, command: T) -> Self
1262 where
1263 T: TryInto<Command>,
1264 T::Error: Error + Send + Sync + 'static,
1265 {
1266 match command.try_into() {
1267 Ok(command) => Self::transition(
1268 JamNodeConfig {
1269 base_config: BaseNodeConfig {
1270 command: Some(command),
1271 ..self.config.base_config
1272 },
1273 ..self.config
1274 },
1275 self.validation_context,
1276 self.errors,
1277 ),
1278 Err(error) => Self::transition(
1279 self.config,
1280 self.validation_context,
1281 merge_errors(self.errors, FieldError::Command(error.into()).into()),
1282 ),
1283 }
1284 }
1285
1286 pub fn with_subcommand<T>(self, subcommand: T) -> Self
1288 where
1289 T: TryInto<Command>,
1290 T::Error: Error + Send + Sync + 'static,
1291 {
1292 match subcommand.try_into() {
1293 Ok(subcommand) => Self::transition(
1294 JamNodeConfig {
1295 base_config: BaseNodeConfig {
1296 subcommand: Some(subcommand),
1297 ..self.config.base_config
1298 },
1299 ..self.config
1300 },
1301 self.validation_context,
1302 self.errors,
1303 ),
1304 Err(error) => Self::transition(
1305 self.config,
1306 self.validation_context,
1307 merge_errors(self.errors, FieldError::Command(error.into()).into()),
1308 ),
1309 }
1310 }
1311
1312 pub fn with_image<T>(self, image: T) -> Self
1314 where
1315 T: TryInto<Image>,
1316 T::Error: Error + Send + Sync + 'static,
1317 {
1318 match image.try_into() {
1319 Ok(image) => Self::transition(
1320 JamNodeConfig {
1321 base_config: BaseNodeConfig {
1322 image: Some(image),
1323 ..self.config.base_config
1324 },
1325 ..self.config
1326 },
1327 self.validation_context,
1328 self.errors,
1329 ),
1330 Err(error) => Self::transition(
1331 self.config,
1332 self.validation_context,
1333 merge_errors(self.errors, FieldError::Image(error.into()).into()),
1334 ),
1335 }
1336 }
1337
1338 pub fn with_args(self, args: Vec<Arg>) -> Self {
1340 Self::transition(
1341 JamNodeConfig {
1342 base_config: BaseNodeConfig {
1343 args,
1344 ..self.config.base_config
1345 },
1346 ..self.config
1347 },
1348 self.validation_context,
1349 self.errors,
1350 )
1351 }
1352
1353 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
1355 match f(ResourcesBuilder::new()).build() {
1356 Ok(resources) => Self::transition(
1357 JamNodeConfig {
1358 base_config: BaseNodeConfig {
1359 resources: Some(resources),
1360 ..self.config.base_config
1361 },
1362 ..self.config
1363 },
1364 self.validation_context,
1365 self.errors,
1366 ),
1367 Err(errors) => Self::transition(
1368 self.config,
1369 self.validation_context,
1370 merge_errors_vecs(
1371 self.errors,
1372 errors
1373 .into_iter()
1374 .map(|error| FieldError::Resources(error).into())
1375 .collect::<Vec<_>>(),
1376 ),
1377 ),
1378 }
1379 }
1380
1381 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
1383 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
1384
1385 Self::transition(
1386 JamNodeConfig {
1387 base_config: BaseNodeConfig {
1388 env,
1389 ..self.config.base_config
1390 },
1391 ..self.config
1392 },
1393 self.validation_context,
1394 self.errors,
1395 )
1396 }
1397
1398 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
1400 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
1401 Ok(_) => Self::transition(
1402 JamNodeConfig {
1403 rpc_port: Some(rpc_port),
1404 ..self.config
1405 },
1406 self.validation_context,
1407 self.errors,
1408 ),
1409 Err(error) => Self::transition(
1410 self.config,
1411 self.validation_context,
1412 merge_errors(self.errors, FieldError::RpcPort(error).into()),
1413 ),
1414 }
1415 }
1416
1417 pub fn with_mode(self, mode: JamNodeMode) -> Self {
1419 Self::transition(
1420 JamNodeConfig {
1421 mode,
1422 ..self.config
1423 },
1424 self.validation_context,
1425 self.errors,
1426 )
1427 }
1428
1429 pub fn with_telemetry_endpoint(self, tel_endpoint: impl Into<String>) -> Self {
1431 Self::transition(
1432 JamNodeConfig {
1433 telemetry_endpoint: Some(tel_endpoint.into()),
1434 ..self.config
1435 },
1436 self.validation_context,
1437 self.errors,
1438 )
1439 }
1440
1441 pub fn build(self) -> Result<JamNodeConfig, (String, Vec<anyhow::Error>)> {
1443 if !self.errors.is_empty() {
1444 return Err((self.config.base_config.name.clone(), self.errors));
1445 }
1446
1447 Ok(self.config)
1448 }
1449}
1450#[cfg(test)]
1451mod tests {
1452 use std::{collections::HashSet, println};
1453
1454 use super::*;
1455
1456 #[test]
1457 fn jam_default_node_serialize() {
1458 let jam_node = JamNodeConfig::default();
1459 let s = serde_json::to_string_pretty(&jam_node);
1460 println!("{:?}", s);
1461 }
1462
1463 #[test]
1464 fn jam_default_node_deserialize() {
1465 let toml_text = r#"
1466 name = "alice"
1467 mode = "validator"
1468 "#;
1469 let jam_node: JamNodeConfig = toml::from_str(toml_text).unwrap();
1470
1471 println!("{:?}", jam_node);
1472 }
1473
1474 #[test]
1475 fn node_config_builder_should_succeeds_and_returns_a_node_config() {
1476 let node_config =
1477 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1478 .with_name("node")
1479 .with_command("mycommand")
1480 .with_image("myrepo:myimage")
1481 .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
1482 .validator(true)
1483 .invulnerable(true)
1484 .bootnode(true)
1485 .with_override_eth_key("0x0123456789abcdef0123456789abcdef01234567")
1486 .with_initial_balance(100_000_042)
1487 .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
1488 .with_raw_bootnodes_addresses(vec![
1489 "/ip4/10.41.122.55/tcp/45421",
1490 "/ip4/51.144.222.10/tcp/2333",
1491 ])
1492 .with_resources(|resources| {
1493 resources
1494 .with_request_cpu("200M")
1495 .with_request_memory("500M")
1496 .with_limit_cpu("1G")
1497 .with_limit_memory("2G")
1498 })
1499 .with_ws_port(5000)
1500 .with_rpc_port(6000)
1501 .with_prometheus_port(7000)
1502 .with_p2p_port(8000)
1503 .with_p2p_cert_hash(
1504 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
1505 )
1506 .with_db_snapshot("/tmp/mysnapshot")
1507 .with_keystore_path("/tmp/mykeystore")
1508 .build()
1509 .unwrap();
1510
1511 assert_eq!(node_config.name(), "node");
1512 assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
1513 assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
1514 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
1515 assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
1516 assert!(node_config.is_validator());
1517 assert!(node_config.is_invulnerable());
1518 assert!(node_config.is_bootnode());
1519 assert_eq!(
1520 node_config.override_eth_key(),
1521 Some("0x0123456789abcdef0123456789abcdef01234567")
1522 );
1523 assert_eq!(node_config.initial_balance(), 100_000_042);
1524 let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
1525 assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
1526 let bootnodes_addresses: Vec<Multiaddr> = vec![
1527 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1528 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1529 ];
1530 assert_eq!(
1531 node_config.bootnodes_addresses(),
1532 bootnodes_addresses.iter().collect::<Vec<_>>()
1533 );
1534 let resources = node_config.resources().unwrap();
1535 assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
1536 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
1537 assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
1538 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
1539 assert_eq!(node_config.ws_port().unwrap(), 5000);
1540 assert_eq!(node_config.rpc_port().unwrap(), 6000);
1541 assert_eq!(node_config.prometheus_port().unwrap(), 7000);
1542 assert_eq!(node_config.p2p_port().unwrap(), 8000);
1543 assert_eq!(
1544 node_config.p2p_cert_hash().unwrap(),
1545 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
1546 );
1547 assert!(matches!(
1548 node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1549 ));
1550 assert!(matches!(
1551 node_config.keystore_path().unwrap().to_str().unwrap(),
1552 "/tmp/mykeystore"
1553 ));
1554 }
1555
1556 #[test]
1557 fn with_optional_db_snapshot_applies_when_some() {
1558 let node_config =
1559 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1560 .with_name("node")
1561 .with_command("mycommand")
1562 .with_optional_db_snapshot(Some("/tmp/mysnapshot"))
1563 .build()
1564 .unwrap();
1565 assert!(matches!(
1566 node_config.db_snapshot().unwrap(),
1567 AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1568 ));
1569 }
1570
1571 #[test]
1572 fn with_optional_db_snapshot_is_noop_when_none() {
1573 let node_config =
1574 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1575 .with_name("node")
1576 .with_command("mycommand")
1577 .with_optional_db_snapshot(None::<&str>)
1578 .build()
1579 .unwrap();
1580 assert!(node_config.db_snapshot().is_none());
1581 }
1582
1583 #[test]
1584 fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
1585 let mut used_nodes_names = HashSet::new();
1586 used_nodes_names.insert("mynode".into());
1587 let validation_context = Rc::new(RefCell::new(ValidationContext {
1588 used_nodes_names,
1589 ..Default::default()
1590 }));
1591 let node_config =
1592 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1593 .with_name("mynode")
1594 .build()
1595 .unwrap();
1596
1597 assert_eq!(node_config.name, "mynode-1");
1598 }
1599
1600 #[test]
1601 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
1602 let (node_name, errors) =
1603 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1604 .with_name("node")
1605 .with_command("invalid command")
1606 .build()
1607 .unwrap_err();
1608
1609 assert_eq!(node_name, "node");
1610 assert_eq!(errors.len(), 1);
1611 assert_eq!(
1612 errors.first().unwrap().to_string(),
1613 "command: 'invalid command' shouldn't contains whitespace"
1614 );
1615 }
1616
1617 #[test]
1618 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
1619 let (node_name, errors) =
1620 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1621 .with_name("node")
1622 .with_image("myinvalid.image")
1623 .build()
1624 .unwrap_err();
1625
1626 assert_eq!(node_name, "node");
1627 assert_eq!(errors.len(), 1);
1628 assert_eq!(
1629 errors.first().unwrap().to_string(),
1630 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1631 );
1632 }
1633
1634 #[test]
1635 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
1636 ) {
1637 let (node_name, errors) =
1638 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1639 .with_name("node")
1640 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1641 .build()
1642 .unwrap_err();
1643
1644 assert_eq!(node_name, "node");
1645 assert_eq!(errors.len(), 1);
1646 assert_eq!(
1647 errors.first().unwrap().to_string(),
1648 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1649 );
1650 }
1651
1652 #[test]
1653 fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
1654 ) {
1655 let (node_name, errors) =
1656 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1657 .with_name("node")
1658 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1659 .build()
1660 .unwrap_err();
1661
1662 assert_eq!(node_name, "node");
1663 assert_eq!(errors.len(), 2);
1664 assert_eq!(
1665 errors.first().unwrap().to_string(),
1666 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1667 );
1668 assert_eq!(
1669 errors.get(1).unwrap().to_string(),
1670 "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1671 );
1672 }
1673
1674 #[test]
1675 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1676 ) {
1677 let (node_name, errors) =
1678 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1679 .with_name("node")
1680 .with_resources(|resources| resources.with_limit_cpu("invalid"))
1681 .build()
1682 .unwrap_err();
1683
1684 assert_eq!(node_name, "node");
1685 assert_eq!(errors.len(), 1);
1686 assert_eq!(
1687 errors.first().unwrap().to_string(),
1688 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1689 );
1690 }
1691
1692 #[test]
1693 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1694 ) {
1695 let (node_name, errors) =
1696 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1697 .with_name("node")
1698 .with_resources(|resources| {
1699 resources
1700 .with_limit_cpu("invalid")
1701 .with_request_memory("invalid")
1702 })
1703 .build()
1704 .unwrap_err();
1705
1706 assert_eq!(node_name, "node");
1707 assert_eq!(errors.len(), 2);
1708 assert_eq!(
1709 errors.first().unwrap().to_string(),
1710 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1711 );
1712 assert_eq!(
1713 errors.get(1).unwrap().to_string(),
1714 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1715 );
1716 }
1717
1718 #[test]
1719 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1720 ) {
1721 let (node_name, errors) =
1722 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1723 .with_name("node")
1724 .with_command("invalid command")
1725 .with_image("myinvalid.image")
1726 .with_resources(|resources| {
1727 resources
1728 .with_limit_cpu("invalid")
1729 .with_request_memory("invalid")
1730 })
1731 .build()
1732 .unwrap_err();
1733
1734 assert_eq!(node_name, "node");
1735 assert_eq!(errors.len(), 4);
1736 assert_eq!(
1737 errors.first().unwrap().to_string(),
1738 "command: 'invalid command' shouldn't contains whitespace"
1739 );
1740 assert_eq!(
1741 errors.get(1).unwrap().to_string(),
1742 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1743 );
1744 assert_eq!(
1745 errors.get(2).unwrap().to_string(),
1746 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1747 );
1748 assert_eq!(
1749 errors.get(3).unwrap().to_string(),
1750 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1751 );
1752 }
1753
1754 #[test]
1755 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1756 ) {
1757 let validation_context = Rc::new(RefCell::new(ValidationContext {
1758 used_ports: vec![30333],
1759 ..Default::default()
1760 }));
1761 let (node_name, errors) =
1762 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1763 .with_name("node")
1764 .with_ws_port(30333)
1765 .build()
1766 .unwrap_err();
1767
1768 assert_eq!(node_name, "node");
1769 assert_eq!(errors.len(), 1);
1770 assert_eq!(
1771 errors.first().unwrap().to_string(),
1772 "ws_port: '30333' is already used across config"
1773 );
1774 }
1775
1776 #[test]
1777 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1778 ) {
1779 let validation_context = Rc::new(RefCell::new(ValidationContext {
1780 used_ports: vec![4444],
1781 ..Default::default()
1782 }));
1783 let (node_name, errors) =
1784 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1785 .with_name("node")
1786 .with_rpc_port(4444)
1787 .build()
1788 .unwrap_err();
1789
1790 assert_eq!(node_name, "node");
1791 assert_eq!(errors.len(), 1);
1792 assert_eq!(
1793 errors.first().unwrap().to_string(),
1794 "rpc_port: '4444' is already used across config"
1795 );
1796 }
1797
1798 #[test]
1799 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1800 ) {
1801 let validation_context = Rc::new(RefCell::new(ValidationContext {
1802 used_ports: vec![9089],
1803 ..Default::default()
1804 }));
1805 let (node_name, errors) =
1806 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1807 .with_name("node")
1808 .with_prometheus_port(9089)
1809 .build()
1810 .unwrap_err();
1811
1812 assert_eq!(node_name, "node");
1813 assert_eq!(errors.len(), 1);
1814 assert_eq!(
1815 errors.first().unwrap().to_string(),
1816 "prometheus_port: '9089' is already used across config"
1817 );
1818 }
1819
1820 #[test]
1821 fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1822 ) {
1823 let validation_context = Rc::new(RefCell::new(ValidationContext {
1824 used_ports: vec![45093],
1825 ..Default::default()
1826 }));
1827 let (node_name, errors) =
1828 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1829 .with_name("node")
1830 .with_p2p_port(45093)
1831 .build()
1832 .unwrap_err();
1833
1834 assert_eq!(node_name, "node");
1835 assert_eq!(errors.len(), 1);
1836 assert_eq!(
1837 errors.first().unwrap().to_string(),
1838 "p2p_port: '45093' is already used across config"
1839 );
1840 }
1841
1842 #[test]
1843 fn node_config_builder_should_fails_if_node_name_is_empty() {
1844 let validation_context = Rc::new(RefCell::new(ValidationContext {
1845 ..Default::default()
1846 }));
1847
1848 let (_, errors) =
1849 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1850 .with_name("")
1851 .build()
1852 .unwrap_err();
1853
1854 assert_eq!(errors.len(), 1);
1855 assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1856 }
1857
1858 #[test]
1859 fn group_default_base_node() {
1860 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1861
1862 let group_config =
1863 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1864 .with_base_node(|node| node.with_name("validator"))
1865 .build()
1866 .unwrap();
1867
1868 assert_eq!(group_config.count, 1);
1870 assert_eq!(group_config.base_config.name(), "validator");
1871 }
1872
1873 #[test]
1874 fn group_custom_base_node() {
1875 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1876 let node_config =
1877 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1878 .with_name("node")
1879 .with_command("some_command")
1880 .with_image("repo:image")
1881 .validator(true)
1882 .invulnerable(true)
1883 .bootnode(true);
1884
1885 let group_config =
1886 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1887 .with_count(5)
1888 .with_base_node(|_node| node_config)
1889 .build()
1890 .unwrap();
1891
1892 assert_eq!(group_config.count, 5);
1894
1895 assert_eq!(group_config.base_config.name(), "node");
1896 assert_eq!(
1897 group_config.base_config.command().unwrap().as_str(),
1898 "some_command"
1899 );
1900 assert_eq!(
1901 group_config.base_config.image().unwrap().as_str(),
1902 "repo:image"
1903 );
1904 assert!(group_config.base_config.is_validator());
1905 assert!(group_config.base_config.is_invulnerable());
1906 assert!(group_config.base_config.is_bootnode());
1907 }
1908
1909 #[test]
1910 fn ensure_default_args_are_overrided() {
1911 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1912 let chain_context = ChainDefaultContext {
1913 default_args: vec!["-lruntime=trace".into()],
1914 ..Default::default()
1915 };
1916 let node_config = NodeConfigBuilder::new(chain_context, validation_context)
1917 .with_name("node")
1918 .with_args(vec!["-lruntime=info".into()])
1919 .build()
1920 .unwrap();
1921
1922 assert_eq!(node_config.args, vec!["-lruntime=info".into()]);
1923 }
1924}