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) = match NodeConfigBuilder::new(
1070 chain_context.clone(),
1071 validation_context.clone(),
1072 )
1073 .with_name(" ") .build()
1075 {
1076 Ok(base_config) => (vec![], base_config),
1077 Err((_name, errors)) => (errors, NodeConfig::default()),
1078 };
1079
1080 Self {
1081 base_config,
1082 count: 1,
1083 validation_context,
1084 errors,
1085 _state: PhantomData,
1086 }
1087 }
1088
1089 pub fn with_base_node(
1091 mut self,
1092 f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
1093 ) -> GroupNodeConfigBuilder<Buildable> {
1094 match f(NodeConfigBuilder::new(
1095 ChainDefaultContext::default(),
1096 self.validation_context.clone(),
1097 ))
1098 .build()
1099 {
1100 Ok(node) => {
1101 self.base_config = node;
1102 GroupNodeConfigBuilder {
1103 base_config: self.base_config,
1104 count: self.count,
1105 validation_context: self.validation_context,
1106 errors: self.errors,
1107 _state: PhantomData,
1108 }
1109 },
1110 Err((_name, errors)) => {
1111 self.errors.extend(errors);
1112 GroupNodeConfigBuilder {
1113 base_config: self.base_config,
1114 count: self.count,
1115 validation_context: self.validation_context,
1116 errors: self.errors,
1117 _state: PhantomData,
1118 }
1119 },
1120 }
1121 }
1122
1123 pub fn with_count(mut self, count: usize) -> Self {
1125 self.count = count;
1126 self
1127 }
1128}
1129
1130impl GroupNodeConfigBuilder<Buildable> {
1131 pub fn with_count(mut self, count: usize) -> Self {
1133 self.count = count;
1134 self
1135 }
1136
1137 pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
1138 if self.count == 0 {
1139 return Err((
1140 self.base_config.name().to_string(),
1141 vec![anyhow::anyhow!("Count cannot be zero")],
1142 ));
1143 }
1144
1145 if !self.errors.is_empty() {
1146 return Err((self.base_config.name().to_string(), self.errors));
1147 }
1148
1149 Ok(GroupNodeConfig {
1150 base_config: self.base_config,
1151 count: self.count,
1152 })
1153 }
1154}
1155
1156pub struct JamNodeConfigBuilder<S> {
1159 config: JamNodeConfig,
1160 validation_context: Rc<RefCell<ValidationContext>>,
1161 errors: Vec<anyhow::Error>,
1162 _state: PhantomData<S>,
1163}
1164
1165impl Default for JamNodeConfigBuilder<Initial> {
1166 fn default() -> Self {
1167 Self {
1168 config: JamNodeConfig {
1169 rpc_port: None,
1170 base_config: BaseNodeConfig {
1171 name: "".into(), image: None,
1173 command: None,
1174 subcommand: None,
1175 args: vec![],
1176 env: vec![],
1177 resources: None,
1178 chain_context: Default::default(),
1179 },
1180 mode: JamNodeMode::Validator, telemetry_endpoint: None,
1182 },
1183 validation_context: Default::default(),
1184 errors: vec![],
1185 _state: PhantomData,
1186 }
1187 }
1188}
1189
1190impl<A> JamNodeConfigBuilder<A> {
1191 fn transition<B>(
1192 config: JamNodeConfig,
1193 validation_context: Rc<RefCell<ValidationContext>>,
1194 errors: Vec<anyhow::Error>,
1195 ) -> JamNodeConfigBuilder<B> {
1196 JamNodeConfigBuilder {
1197 config,
1198 validation_context,
1199 errors,
1200 _state: PhantomData,
1201 }
1202 }
1203}
1204
1205impl JamNodeConfigBuilder<Initial> {
1206 pub fn new(
1207 chain_context: ChainDefaultContext,
1208 validation_context: Rc<RefCell<ValidationContext>>,
1209 ) -> Self {
1210 let base_config = BaseNodeConfig {
1211 command: chain_context.default_command.clone(),
1212 image: chain_context.default_image.clone(),
1213 resources: chain_context.default_resources.clone(),
1214 args: chain_context.default_args.clone(),
1215 chain_context,
1216 ..Default::default()
1217 };
1218
1219 Self::transition(
1220 JamNodeConfig {
1221 base_config,
1222 ..JamNodeConfig::default()
1223 },
1224 validation_context,
1225 vec![],
1226 )
1227 }
1228
1229 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> JamNodeConfigBuilder<Buildable> {
1231 let name: String = generate_unique_node_name(name, self.validation_context.clone());
1232
1233 match ensure_value_is_not_empty(&name) {
1234 Ok(_) => Self::transition(
1235 JamNodeConfig {
1236 base_config: BaseNodeConfig {
1237 name,
1238 ..self.config.base_config
1239 },
1240 ..self.config
1241 },
1242 self.validation_context,
1243 self.errors,
1244 ),
1245 Err(e) => Self::transition(
1246 JamNodeConfig {
1247 base_config: BaseNodeConfig {
1249 name,
1250 ..self.config.base_config
1251 },
1252 ..self.config
1253 },
1254 self.validation_context,
1255 merge_errors(self.errors, FieldError::Name(e).into()),
1256 ),
1257 }
1258 }
1259}
1260
1261impl JamNodeConfigBuilder<Buildable> {
1262 pub fn with_command<T>(self, command: T) -> Self
1264 where
1265 T: TryInto<Command>,
1266 T::Error: Error + Send + Sync + 'static,
1267 {
1268 match command.try_into() {
1269 Ok(command) => Self::transition(
1270 JamNodeConfig {
1271 base_config: BaseNodeConfig {
1272 command: Some(command),
1273 ..self.config.base_config
1274 },
1275 ..self.config
1276 },
1277 self.validation_context,
1278 self.errors,
1279 ),
1280 Err(error) => Self::transition(
1281 self.config,
1282 self.validation_context,
1283 merge_errors(self.errors, FieldError::Command(error.into()).into()),
1284 ),
1285 }
1286 }
1287
1288 pub fn with_subcommand<T>(self, subcommand: T) -> Self
1290 where
1291 T: TryInto<Command>,
1292 T::Error: Error + Send + Sync + 'static,
1293 {
1294 match subcommand.try_into() {
1295 Ok(subcommand) => Self::transition(
1296 JamNodeConfig {
1297 base_config: BaseNodeConfig {
1298 subcommand: Some(subcommand),
1299 ..self.config.base_config
1300 },
1301 ..self.config
1302 },
1303 self.validation_context,
1304 self.errors,
1305 ),
1306 Err(error) => Self::transition(
1307 self.config,
1308 self.validation_context,
1309 merge_errors(self.errors, FieldError::Command(error.into()).into()),
1310 ),
1311 }
1312 }
1313
1314 pub fn with_image<T>(self, image: T) -> Self
1316 where
1317 T: TryInto<Image>,
1318 T::Error: Error + Send + Sync + 'static,
1319 {
1320 match image.try_into() {
1321 Ok(image) => Self::transition(
1322 JamNodeConfig {
1323 base_config: BaseNodeConfig {
1324 image: Some(image),
1325 ..self.config.base_config
1326 },
1327 ..self.config
1328 },
1329 self.validation_context,
1330 self.errors,
1331 ),
1332 Err(error) => Self::transition(
1333 self.config,
1334 self.validation_context,
1335 merge_errors(self.errors, FieldError::Image(error.into()).into()),
1336 ),
1337 }
1338 }
1339
1340 pub fn with_args(self, args: Vec<Arg>) -> Self {
1342 Self::transition(
1343 JamNodeConfig {
1344 base_config: BaseNodeConfig {
1345 args,
1346 ..self.config.base_config
1347 },
1348 ..self.config
1349 },
1350 self.validation_context,
1351 self.errors,
1352 )
1353 }
1354
1355 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
1357 match f(ResourcesBuilder::new()).build() {
1358 Ok(resources) => Self::transition(
1359 JamNodeConfig {
1360 base_config: BaseNodeConfig {
1361 resources: Some(resources),
1362 ..self.config.base_config
1363 },
1364 ..self.config
1365 },
1366 self.validation_context,
1367 self.errors,
1368 ),
1369 Err(errors) => Self::transition(
1370 self.config,
1371 self.validation_context,
1372 merge_errors_vecs(
1373 self.errors,
1374 errors
1375 .into_iter()
1376 .map(|error| FieldError::Resources(error).into())
1377 .collect::<Vec<_>>(),
1378 ),
1379 ),
1380 }
1381 }
1382
1383 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
1385 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
1386
1387 Self::transition(
1388 JamNodeConfig {
1389 base_config: BaseNodeConfig {
1390 env,
1391 ..self.config.base_config
1392 },
1393 ..self.config
1394 },
1395 self.validation_context,
1396 self.errors,
1397 )
1398 }
1399
1400 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
1402 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
1403 Ok(_) => Self::transition(
1404 JamNodeConfig {
1405 rpc_port: Some(rpc_port),
1406 ..self.config
1407 },
1408 self.validation_context,
1409 self.errors,
1410 ),
1411 Err(error) => Self::transition(
1412 self.config,
1413 self.validation_context,
1414 merge_errors(self.errors, FieldError::RpcPort(error).into()),
1415 ),
1416 }
1417 }
1418
1419 pub fn with_mode(self, mode: JamNodeMode) -> Self {
1421 Self::transition(
1422 JamNodeConfig {
1423 mode,
1424 ..self.config
1425 },
1426 self.validation_context,
1427 self.errors,
1428 )
1429 }
1430
1431 pub fn with_telemetry_endpoint(self, tel_endpoint: impl Into<String>) -> Self {
1433 Self::transition(
1434 JamNodeConfig {
1435 telemetry_endpoint: Some(tel_endpoint.into()),
1436 ..self.config
1437 },
1438 self.validation_context,
1439 self.errors,
1440 )
1441 }
1442
1443 pub fn build(self) -> Result<JamNodeConfig, (String, Vec<anyhow::Error>)> {
1445 if !self.errors.is_empty() {
1446 return Err((self.config.base_config.name.clone(), self.errors));
1447 }
1448
1449 Ok(self.config)
1450 }
1451}
1452#[cfg(test)]
1453mod tests {
1454 use std::{collections::HashSet, println};
1455
1456 use super::*;
1457
1458 #[test]
1459 fn jam_default_node_serialize() {
1460 let jam_node = JamNodeConfig::default();
1461 let s = serde_json::to_string_pretty(&jam_node);
1462 println!("{:?}", s);
1463 }
1464
1465 #[test]
1466 fn jam_default_node_deserialize() {
1467 let toml_text = r#"
1468 name = "alice"
1469 mode = "validator"
1470 "#;
1471 let jam_node: JamNodeConfig = toml::from_str(toml_text).unwrap();
1472
1473 println!("{:?}", jam_node);
1474 }
1475
1476 #[test]
1477 fn node_config_builder_should_succeeds_and_returns_a_node_config() {
1478 let node_config =
1479 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1480 .with_name("node")
1481 .with_command("mycommand")
1482 .with_image("myrepo:myimage")
1483 .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
1484 .validator(true)
1485 .invulnerable(true)
1486 .bootnode(true)
1487 .with_override_eth_key("0x0123456789abcdef0123456789abcdef01234567")
1488 .with_initial_balance(100_000_042)
1489 .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
1490 .with_raw_bootnodes_addresses(vec![
1491 "/ip4/10.41.122.55/tcp/45421",
1492 "/ip4/51.144.222.10/tcp/2333",
1493 ])
1494 .with_resources(|resources| {
1495 resources
1496 .with_request_cpu("200M")
1497 .with_request_memory("500M")
1498 .with_limit_cpu("1G")
1499 .with_limit_memory("2G")
1500 })
1501 .with_ws_port(5000)
1502 .with_rpc_port(6000)
1503 .with_prometheus_port(7000)
1504 .with_p2p_port(8000)
1505 .with_p2p_cert_hash(
1506 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
1507 )
1508 .with_db_snapshot("/tmp/mysnapshot")
1509 .with_keystore_path("/tmp/mykeystore")
1510 .build()
1511 .unwrap();
1512
1513 assert_eq!(node_config.name(), "node");
1514 assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
1515 assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
1516 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
1517 assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
1518 assert!(node_config.is_validator());
1519 assert!(node_config.is_invulnerable());
1520 assert!(node_config.is_bootnode());
1521 assert_eq!(
1522 node_config.override_eth_key(),
1523 Some("0x0123456789abcdef0123456789abcdef01234567")
1524 );
1525 assert_eq!(node_config.initial_balance(), 100_000_042);
1526 let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
1527 assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
1528 let bootnodes_addresses: Vec<Multiaddr> = vec![
1529 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1530 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1531 ];
1532 assert_eq!(
1533 node_config.bootnodes_addresses(),
1534 bootnodes_addresses.iter().collect::<Vec<_>>()
1535 );
1536 let resources = node_config.resources().unwrap();
1537 assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
1538 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
1539 assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
1540 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
1541 assert_eq!(node_config.ws_port().unwrap(), 5000);
1542 assert_eq!(node_config.rpc_port().unwrap(), 6000);
1543 assert_eq!(node_config.prometheus_port().unwrap(), 7000);
1544 assert_eq!(node_config.p2p_port().unwrap(), 8000);
1545 assert_eq!(
1546 node_config.p2p_cert_hash().unwrap(),
1547 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
1548 );
1549 assert!(matches!(
1550 node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1551 ));
1552 assert!(matches!(
1553 node_config.keystore_path().unwrap().to_str().unwrap(),
1554 "/tmp/mykeystore"
1555 ));
1556 }
1557
1558 #[test]
1559 fn with_optional_db_snapshot_applies_when_some() {
1560 let node_config =
1561 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1562 .with_name("node")
1563 .with_command("mycommand")
1564 .with_optional_db_snapshot(Some("/tmp/mysnapshot"))
1565 .build()
1566 .unwrap();
1567 assert!(matches!(
1568 node_config.db_snapshot().unwrap(),
1569 AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1570 ));
1571 }
1572
1573 #[test]
1574 fn with_optional_db_snapshot_is_noop_when_none() {
1575 let node_config =
1576 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1577 .with_name("node")
1578 .with_command("mycommand")
1579 .with_optional_db_snapshot(None::<&str>)
1580 .build()
1581 .unwrap();
1582 assert!(node_config.db_snapshot().is_none());
1583 }
1584
1585 #[test]
1586 fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
1587 let mut used_nodes_names = HashSet::new();
1588 used_nodes_names.insert("mynode".into());
1589 let validation_context = Rc::new(RefCell::new(ValidationContext {
1590 used_nodes_names,
1591 ..Default::default()
1592 }));
1593 let node_config =
1594 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1595 .with_name("mynode")
1596 .build()
1597 .unwrap();
1598
1599 assert_eq!(node_config.name, "mynode-1");
1600 }
1601
1602 #[test]
1603 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
1604 let (node_name, errors) =
1605 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1606 .with_name("node")
1607 .with_command("invalid command")
1608 .build()
1609 .unwrap_err();
1610
1611 assert_eq!(node_name, "node");
1612 assert_eq!(errors.len(), 1);
1613 assert_eq!(
1614 errors.first().unwrap().to_string(),
1615 "command: 'invalid command' shouldn't contains whitespace"
1616 );
1617 }
1618
1619 #[test]
1620 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
1621 let (node_name, errors) =
1622 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1623 .with_name("node")
1624 .with_image("myinvalid.image")
1625 .build()
1626 .unwrap_err();
1627
1628 assert_eq!(node_name, "node");
1629 assert_eq!(errors.len(), 1);
1630 assert_eq!(
1631 errors.first().unwrap().to_string(),
1632 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1633 );
1634 }
1635
1636 #[test]
1637 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
1638 ) {
1639 let (node_name, errors) =
1640 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1641 .with_name("node")
1642 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1643 .build()
1644 .unwrap_err();
1645
1646 assert_eq!(node_name, "node");
1647 assert_eq!(errors.len(), 1);
1648 assert_eq!(
1649 errors.first().unwrap().to_string(),
1650 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1651 );
1652 }
1653
1654 #[test]
1655 fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
1656 ) {
1657 let (node_name, errors) =
1658 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1659 .with_name("node")
1660 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1661 .build()
1662 .unwrap_err();
1663
1664 assert_eq!(node_name, "node");
1665 assert_eq!(errors.len(), 2);
1666 assert_eq!(
1667 errors.first().unwrap().to_string(),
1668 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1669 );
1670 assert_eq!(
1671 errors.get(1).unwrap().to_string(),
1672 "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1673 );
1674 }
1675
1676 #[test]
1677 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1678 ) {
1679 let (node_name, errors) =
1680 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1681 .with_name("node")
1682 .with_resources(|resources| resources.with_limit_cpu("invalid"))
1683 .build()
1684 .unwrap_err();
1685
1686 assert_eq!(node_name, "node");
1687 assert_eq!(errors.len(), 1);
1688 assert_eq!(
1689 errors.first().unwrap().to_string(),
1690 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1691 );
1692 }
1693
1694 #[test]
1695 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1696 ) {
1697 let (node_name, errors) =
1698 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1699 .with_name("node")
1700 .with_resources(|resources| {
1701 resources
1702 .with_limit_cpu("invalid")
1703 .with_request_memory("invalid")
1704 })
1705 .build()
1706 .unwrap_err();
1707
1708 assert_eq!(node_name, "node");
1709 assert_eq!(errors.len(), 2);
1710 assert_eq!(
1711 errors.first().unwrap().to_string(),
1712 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1713 );
1714 assert_eq!(
1715 errors.get(1).unwrap().to_string(),
1716 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1717 );
1718 }
1719
1720 #[test]
1721 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1722 ) {
1723 let (node_name, errors) =
1724 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1725 .with_name("node")
1726 .with_command("invalid command")
1727 .with_image("myinvalid.image")
1728 .with_resources(|resources| {
1729 resources
1730 .with_limit_cpu("invalid")
1731 .with_request_memory("invalid")
1732 })
1733 .build()
1734 .unwrap_err();
1735
1736 assert_eq!(node_name, "node");
1737 assert_eq!(errors.len(), 4);
1738 assert_eq!(
1739 errors.first().unwrap().to_string(),
1740 "command: 'invalid command' shouldn't contains whitespace"
1741 );
1742 assert_eq!(
1743 errors.get(1).unwrap().to_string(),
1744 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1745 );
1746 assert_eq!(
1747 errors.get(2).unwrap().to_string(),
1748 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1749 );
1750 assert_eq!(
1751 errors.get(3).unwrap().to_string(),
1752 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1753 );
1754 }
1755
1756 #[test]
1757 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1758 ) {
1759 let validation_context = Rc::new(RefCell::new(ValidationContext {
1760 used_ports: vec![30333],
1761 ..Default::default()
1762 }));
1763 let (node_name, errors) =
1764 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1765 .with_name("node")
1766 .with_ws_port(30333)
1767 .build()
1768 .unwrap_err();
1769
1770 assert_eq!(node_name, "node");
1771 assert_eq!(errors.len(), 1);
1772 assert_eq!(
1773 errors.first().unwrap().to_string(),
1774 "ws_port: '30333' is already used across config"
1775 );
1776 }
1777
1778 #[test]
1779 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1780 ) {
1781 let validation_context = Rc::new(RefCell::new(ValidationContext {
1782 used_ports: vec![4444],
1783 ..Default::default()
1784 }));
1785 let (node_name, errors) =
1786 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1787 .with_name("node")
1788 .with_rpc_port(4444)
1789 .build()
1790 .unwrap_err();
1791
1792 assert_eq!(node_name, "node");
1793 assert_eq!(errors.len(), 1);
1794 assert_eq!(
1795 errors.first().unwrap().to_string(),
1796 "rpc_port: '4444' is already used across config"
1797 );
1798 }
1799
1800 #[test]
1801 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1802 ) {
1803 let validation_context = Rc::new(RefCell::new(ValidationContext {
1804 used_ports: vec![9089],
1805 ..Default::default()
1806 }));
1807 let (node_name, errors) =
1808 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1809 .with_name("node")
1810 .with_prometheus_port(9089)
1811 .build()
1812 .unwrap_err();
1813
1814 assert_eq!(node_name, "node");
1815 assert_eq!(errors.len(), 1);
1816 assert_eq!(
1817 errors.first().unwrap().to_string(),
1818 "prometheus_port: '9089' is already used across config"
1819 );
1820 }
1821
1822 #[test]
1823 fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1824 ) {
1825 let validation_context = Rc::new(RefCell::new(ValidationContext {
1826 used_ports: vec![45093],
1827 ..Default::default()
1828 }));
1829 let (node_name, errors) =
1830 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1831 .with_name("node")
1832 .with_p2p_port(45093)
1833 .build()
1834 .unwrap_err();
1835
1836 assert_eq!(node_name, "node");
1837 assert_eq!(errors.len(), 1);
1838 assert_eq!(
1839 errors.first().unwrap().to_string(),
1840 "p2p_port: '45093' is already used across config"
1841 );
1842 }
1843
1844 #[test]
1845 fn node_config_builder_should_fails_if_node_name_is_empty() {
1846 let validation_context = Rc::new(RefCell::new(ValidationContext {
1847 ..Default::default()
1848 }));
1849
1850 let (_, errors) =
1851 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1852 .with_name("")
1853 .build()
1854 .unwrap_err();
1855
1856 assert_eq!(errors.len(), 1);
1857 assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1858 }
1859
1860 #[test]
1861 fn group_default_base_node() {
1862 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1863
1864 let group_config =
1865 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1866 .with_base_node(|node| node.with_name("validator"))
1867 .build()
1868 .unwrap();
1869
1870 assert_eq!(group_config.count, 1);
1872 assert_eq!(group_config.base_config.name(), "validator");
1873 }
1874
1875 #[test]
1876 fn group_custom_base_node() {
1877 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1878 let node_config =
1879 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1880 .with_name("node")
1881 .with_command("some_command")
1882 .with_image("repo:image")
1883 .validator(true)
1884 .invulnerable(true)
1885 .bootnode(true);
1886
1887 let group_config =
1888 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1889 .with_count(5)
1890 .with_base_node(|_node| node_config)
1891 .build()
1892 .unwrap();
1893
1894 assert_eq!(group_config.count, 5);
1896
1897 assert_eq!(group_config.base_config.name(), "node");
1898 assert_eq!(
1899 group_config.base_config.command().unwrap().as_str(),
1900 "some_command"
1901 );
1902 assert_eq!(
1903 group_config.base_config.image().unwrap().as_str(),
1904 "repo:image"
1905 );
1906 assert!(group_config.base_config.is_validator());
1907 assert!(group_config.base_config.is_invulnerable());
1908 assert!(group_config.base_config.is_bootnode());
1909 }
1910
1911 #[test]
1912 fn ensure_default_args_are_overrided() {
1913 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1914 let chain_context = ChainDefaultContext {
1915 default_args: vec!["-lruntime=trace".into()],
1916 ..Default::default()
1917 };
1918 let node_config = NodeConfigBuilder::new(chain_context, validation_context)
1919 .with_name("node")
1920 .with_args(vec!["-lruntime=info".into()])
1921 .build()
1922 .unwrap();
1923
1924 assert_eq!(node_config.args, vec!["-lruntime=info".into()]);
1925 }
1926}