1use std::{cell::RefCell, error::Error, fmt::Debug, marker::PhantomData, rc::Rc};
2
3use serde::{Deserialize, Serialize};
4use support::constants::{DEFAULT_TYPESTATE, THIS_IS_A_BUG};
5
6use crate::{
7 shared::{
8 errors::{ConfigError, FieldError},
9 helpers::{merge_errors, merge_errors_vecs},
10 macros::states,
11 node::{self, NodeConfig, NodeConfigBuilder},
12 resources::{Resources, ResourcesBuilder},
13 types::{
14 Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext,
15 },
16 },
17 utils::{default_command_polkadot, default_relaychain_chain, is_false},
18};
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct RelaychainConfig {
23 #[serde(default = "default_relaychain_chain")]
24 chain: Chain,
25 #[serde(default = "default_command_polkadot")]
26 default_command: Option<Command>,
27 default_image: Option<Image>,
28 default_resources: Option<Resources>,
29 default_db_snapshot: Option<AssetLocation>,
30 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
31 default_args: Vec<Arg>,
32 chain_spec_path: Option<AssetLocation>,
33 chain_spec_command: Option<String>,
37 #[serde(skip_serializing_if = "is_false", default)]
38 chain_spec_command_is_local: bool,
39 random_nominators_count: Option<u32>,
40 max_nominations: Option<u8>,
41 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
42 nodes: Vec<NodeConfig>,
43 #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
44 runtime_genesis_patch: Option<serde_json::Value>,
45 wasm_override: Option<AssetLocation>,
47 command: Option<Command>,
48}
49
50impl RelaychainConfig {
51 pub fn chain(&self) -> &Chain {
53 &self.chain
54 }
55
56 pub fn default_command(&self) -> Option<&Command> {
58 self.default_command.as_ref()
59 }
60
61 pub fn default_image(&self) -> Option<&Image> {
63 self.default_image.as_ref()
64 }
65
66 pub fn default_resources(&self) -> Option<&Resources> {
68 self.default_resources.as_ref()
69 }
70
71 pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
73 self.default_db_snapshot.as_ref()
74 }
75
76 pub fn default_args(&self) -> Vec<&Arg> {
78 self.default_args.iter().collect::<Vec<&Arg>>()
79 }
80
81 pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
83 self.chain_spec_path.as_ref()
84 }
85
86 pub fn wasm_override(&self) -> Option<&AssetLocation> {
88 self.wasm_override.as_ref()
89 }
90
91 pub fn chain_spec_command(&self) -> Option<&str> {
93 self.chain_spec_command.as_deref()
94 }
95
96 pub fn chain_spec_command_is_local(&self) -> bool {
98 self.chain_spec_command_is_local
99 }
100
101 pub fn command(&self) -> Option<&Command> {
103 self.command.as_ref()
104 }
105
106 pub fn random_nominators_count(&self) -> Option<u32> {
108 self.random_nominators_count
109 }
110
111 pub fn max_nominations(&self) -> Option<u8> {
113 self.max_nominations
114 }
115
116 pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
118 self.runtime_genesis_patch.as_ref()
119 }
120
121 pub fn nodes(&self) -> Vec<&NodeConfig> {
123 self.nodes.iter().collect::<Vec<&NodeConfig>>()
124 }
125
126 pub(crate) fn set_nodes(&mut self, nodes: Vec<NodeConfig>) {
127 self.nodes = nodes;
128 }
129}
130
131states! {
132 Initial,
133 WithChain,
134 WithAtLeastOneNode
135}
136
137pub struct RelaychainConfigBuilder<State> {
139 config: RelaychainConfig,
140 validation_context: Rc<RefCell<ValidationContext>>,
141 errors: Vec<anyhow::Error>,
142 _state: PhantomData<State>,
143}
144
145impl Default for RelaychainConfigBuilder<Initial> {
146 fn default() -> Self {
147 Self {
148 config: RelaychainConfig {
149 chain: "default"
150 .try_into()
151 .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
152 default_command: None,
153 default_image: None,
154 default_resources: None,
155 default_db_snapshot: None,
156 default_args: vec![],
157 chain_spec_path: None,
158 chain_spec_command: None,
159 wasm_override: None,
160 chain_spec_command_is_local: false, command: None,
162 random_nominators_count: None,
163 max_nominations: None,
164 runtime_genesis_patch: None,
165 nodes: vec![],
166 },
167 validation_context: Default::default(),
168 errors: vec![],
169 _state: PhantomData,
170 }
171 }
172}
173
174impl<A> RelaychainConfigBuilder<A> {
175 fn transition<B>(
176 config: RelaychainConfig,
177 validation_context: Rc<RefCell<ValidationContext>>,
178 errors: Vec<anyhow::Error>,
179 ) -> RelaychainConfigBuilder<B> {
180 RelaychainConfigBuilder {
181 config,
182 validation_context,
183 errors,
184 _state: PhantomData,
185 }
186 }
187
188 fn default_chain_context(&self) -> ChainDefaultContext {
189 ChainDefaultContext {
190 default_command: self.config.default_command.clone(),
191 default_image: self.config.default_image.clone(),
192 default_resources: self.config.default_resources.clone(),
193 default_db_snapshot: self.config.default_db_snapshot.clone(),
194 default_args: self.config.default_args.clone(),
195 }
196 }
197}
198
199impl RelaychainConfigBuilder<Initial> {
200 pub fn new(
201 validation_context: Rc<RefCell<ValidationContext>>,
202 ) -> RelaychainConfigBuilder<Initial> {
203 Self {
204 validation_context,
205 ..Self::default()
206 }
207 }
208
209 pub fn with_chain<T>(self, chain: T) -> RelaychainConfigBuilder<WithChain>
211 where
212 T: TryInto<Chain>,
213 T::Error: Error + Send + Sync + 'static,
214 {
215 match chain.try_into() {
216 Ok(chain) => Self::transition(
217 RelaychainConfig {
218 chain,
219 ..self.config
220 },
221 self.validation_context,
222 self.errors,
223 ),
224 Err(error) => Self::transition(
225 self.config,
226 self.validation_context,
227 merge_errors(self.errors, FieldError::Chain(error.into()).into()),
228 ),
229 }
230 }
231}
232
233impl RelaychainConfigBuilder<WithChain> {
234 pub fn with_default_command<T>(self, command: T) -> Self
236 where
237 T: TryInto<Command>,
238 T::Error: Error + Send + Sync + 'static,
239 {
240 match command.try_into() {
241 Ok(command) => Self::transition(
242 RelaychainConfig {
243 default_command: Some(command),
244 ..self.config
245 },
246 self.validation_context,
247 self.errors,
248 ),
249 Err(error) => Self::transition(
250 self.config,
251 self.validation_context,
252 merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
253 ),
254 }
255 }
256
257 pub fn with_default_image<T>(self, image: T) -> Self
259 where
260 T: TryInto<Image>,
261 T::Error: Error + Send + Sync + 'static,
262 {
263 match image.try_into() {
264 Ok(image) => Self::transition(
265 RelaychainConfig {
266 default_image: Some(image),
267 ..self.config
268 },
269 self.validation_context,
270 self.errors,
271 ),
272 Err(error) => Self::transition(
273 self.config,
274 self.validation_context,
275 merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
276 ),
277 }
278 }
279
280 pub fn with_default_resources(
282 self,
283 f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
284 ) -> Self {
285 match f(ResourcesBuilder::new()).build() {
286 Ok(default_resources) => Self::transition(
287 RelaychainConfig {
288 default_resources: Some(default_resources),
289 ..self.config
290 },
291 self.validation_context,
292 self.errors,
293 ),
294 Err(errors) => Self::transition(
295 self.config,
296 self.validation_context,
297 merge_errors_vecs(
298 self.errors,
299 errors
300 .into_iter()
301 .map(|error| FieldError::DefaultResources(error).into())
302 .collect::<Vec<_>>(),
303 ),
304 ),
305 }
306 }
307
308 pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
310 Self::transition(
311 RelaychainConfig {
312 default_db_snapshot: Some(location.into()),
313 ..self.config
314 },
315 self.validation_context,
316 self.errors,
317 )
318 }
319
320 pub fn with_default_args(self, args: Vec<Arg>) -> Self {
322 Self::transition(
323 RelaychainConfig {
324 default_args: args,
325 ..self.config
326 },
327 self.validation_context,
328 self.errors,
329 )
330 }
331
332 pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
334 Self::transition(
335 RelaychainConfig {
336 chain_spec_path: Some(location.into()),
337 ..self.config
338 },
339 self.validation_context,
340 self.errors,
341 )
342 }
343
344 pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
346 Self::transition(
347 RelaychainConfig {
348 wasm_override: Some(location.into()),
349 ..self.config
350 },
351 self.validation_context,
352 self.errors,
353 )
354 }
355
356 pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
358 Self::transition(
359 RelaychainConfig {
360 chain_spec_command: Some(cmd_template.into()),
361 ..self.config
362 },
363 self.validation_context,
364 self.errors,
365 )
366 }
367
368 pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
370 Self::transition(
371 RelaychainConfig {
372 chain_spec_command_is_local: choice,
373 ..self.config
374 },
375 self.validation_context,
376 self.errors,
377 )
378 }
379
380 pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
382 Self::transition(
383 RelaychainConfig {
384 random_nominators_count: Some(random_nominators_count),
385 ..self.config
386 },
387 self.validation_context,
388 self.errors,
389 )
390 }
391
392 pub fn with_max_nominations(self, max_nominations: u8) -> Self {
394 Self::transition(
395 RelaychainConfig {
396 max_nominations: Some(max_nominations),
397 ..self.config
398 },
399 self.validation_context,
400 self.errors,
401 )
402 }
403
404 pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
406 Self::transition(
407 RelaychainConfig {
408 runtime_genesis_patch: Some(genesis_overrides.into()),
409 ..self.config
410 },
411 self.validation_context,
412 self.errors,
413 )
414 }
415
416 pub fn with_node(
418 self,
419 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
420 ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
421 match f(NodeConfigBuilder::new(
422 self.default_chain_context(),
423 self.validation_context.clone(),
424 ))
425 .build()
426 {
427 Ok(node) => Self::transition(
428 RelaychainConfig {
429 nodes: vec![node],
430 ..self.config
431 },
432 self.validation_context,
433 self.errors,
434 ),
435 Err((name, errors)) => Self::transition(
436 self.config,
437 self.validation_context,
438 merge_errors_vecs(
439 self.errors,
440 errors
441 .into_iter()
442 .map(|error| ConfigError::Node(name.clone(), error).into())
443 .collect::<Vec<_>>(),
444 ),
445 ),
446 }
447 }
448}
449
450impl RelaychainConfigBuilder<WithAtLeastOneNode> {
451 pub fn with_node(
453 self,
454 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
455 ) -> Self {
456 match f(NodeConfigBuilder::new(
457 self.default_chain_context(),
458 self.validation_context.clone(),
459 ))
460 .build()
461 {
462 Ok(node) => Self::transition(
463 RelaychainConfig {
464 nodes: [self.config.nodes, vec![node]].concat(),
465 ..self.config
466 },
467 self.validation_context,
468 self.errors,
469 ),
470 Err((name, errors)) => Self::transition(
471 self.config,
472 self.validation_context,
473 merge_errors_vecs(
474 self.errors,
475 errors
476 .into_iter()
477 .map(|error| ConfigError::Node(name.clone(), error).into())
478 .collect::<Vec<_>>(),
479 ),
480 ),
481 }
482 }
483
484 pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
486 if !self.errors.is_empty() {
487 return Err(self
488 .errors
489 .into_iter()
490 .map(|error| ConfigError::Relaychain(error).into())
491 .collect::<Vec<_>>());
492 }
493
494 Ok(self.config)
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
504 let relaychain_config = RelaychainConfigBuilder::new(Default::default())
505 .with_chain("polkadot")
506 .with_default_image("myrepo:myimage")
507 .with_default_command("default_command")
508 .with_default_resources(|resources| {
509 resources
510 .with_limit_cpu("500M")
511 .with_limit_memory("1G")
512 .with_request_cpu("250M")
513 })
514 .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
515 .with_chain_spec_path("./path/to/chain/spec.json")
516 .with_wasm_override("./path/to/override/runtime.wasm")
517 .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
518 .with_random_nominators_count(42)
519 .with_max_nominations(5)
520 .with_node(|node| node.with_name("node1").bootnode(true))
521 .with_node(|node| {
522 node.with_name("node2")
523 .with_command("command2")
524 .validator(true)
525 })
526 .build()
527 .unwrap();
528
529 assert_eq!(relaychain_config.chain().as_str(), "polkadot");
530 assert_eq!(relaychain_config.nodes().len(), 2);
531 let &node1 = relaychain_config.nodes().first().unwrap();
532 assert_eq!(node1.name(), "node1");
533 assert_eq!(node1.command().unwrap().as_str(), "default_command");
534 assert!(node1.is_bootnode());
535 let &node2 = relaychain_config.nodes().last().unwrap();
536 assert_eq!(node2.name(), "node2");
537 assert_eq!(node2.command().unwrap().as_str(), "command2");
538 assert!(node2.is_validator());
539 assert_eq!(
540 relaychain_config.default_command().unwrap().as_str(),
541 "default_command"
542 );
543 assert_eq!(
544 relaychain_config.default_image().unwrap().as_str(),
545 "myrepo:myimage"
546 );
547 let default_resources = relaychain_config.default_resources().unwrap();
548 assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
549 assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
550 assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
551 assert!(matches!(
552 relaychain_config.default_db_snapshot().unwrap(),
553 AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
554 ));
555 assert!(matches!(
556 relaychain_config.chain_spec_path().unwrap(),
557 AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
558 ));
559 assert!(matches!(
560 relaychain_config.wasm_override().unwrap(),
561 AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
562 ));
563 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
564 assert_eq!(
565 relaychain_config.default_args(),
566 args.iter().collect::<Vec<_>>()
567 );
568 assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
569 assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
570 }
571
572 #[test]
573 fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
574 let errors = RelaychainConfigBuilder::new(Default::default())
575 .with_chain("invalid chain")
576 .with_node(|node| {
577 node.with_name("node")
578 .with_command("command")
579 .validator(true)
580 })
581 .build()
582 .unwrap_err();
583
584 assert_eq!(errors.len(), 1);
585 assert_eq!(
586 errors.first().unwrap().to_string(),
587 "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
588 );
589 }
590
591 #[test]
592 fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
593 let errors = RelaychainConfigBuilder::new(Default::default())
594 .with_chain("chain")
595 .with_default_command("invalid command")
596 .with_node(|node| {
597 node.with_name("node")
598 .with_command("command")
599 .validator(true)
600 })
601 .build()
602 .unwrap_err();
603
604 assert_eq!(errors.len(), 1);
605 assert_eq!(
606 errors.first().unwrap().to_string(),
607 "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
608 );
609 }
610
611 #[test]
612 fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
613 let errors = RelaychainConfigBuilder::new(Default::default())
614 .with_chain("chain")
615 .with_default_image("invalid image")
616 .with_node(|node| {
617 node.with_name("node")
618 .with_command("command")
619 .validator(true)
620 })
621 .build()
622 .unwrap_err();
623
624 assert_eq!(errors.len(), 1);
625 assert_eq!(
626 errors.first().unwrap().to_string(),
627 r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
628 );
629 }
630
631 #[test]
632 fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
633 ) {
634 let errors = RelaychainConfigBuilder::new(Default::default())
635 .with_chain("chain")
636 .with_default_resources(|default_resources| {
637 default_resources
638 .with_limit_memory("100m")
639 .with_request_cpu("invalid")
640 })
641 .with_node(|node| {
642 node.with_name("node")
643 .with_command("command")
644 .validator(true)
645 })
646 .build()
647 .unwrap_err();
648
649 assert_eq!(errors.len(), 1);
650 assert_eq!(
651 errors.first().unwrap().to_string(),
652 r"relaychain.default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
653 );
654 }
655
656 #[test]
657 fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
658 let errors = RelaychainConfigBuilder::new(Default::default())
659 .with_chain("chain")
660 .with_node(|node| {
661 node.with_name("node")
662 .with_command("invalid command")
663 .validator(true)
664 })
665 .build()
666 .unwrap_err();
667
668 assert_eq!(errors.len(), 1);
669 assert_eq!(
670 errors.first().unwrap().to_string(),
671 "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
672 );
673 }
674
675 #[test]
676 fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
677 ) {
678 let errors = RelaychainConfigBuilder::new(Default::default())
679 .with_chain("chain")
680 .with_node(|node| {
681 node.with_name("node1")
682 .with_command("command1")
683 .validator(true)
684 })
685 .with_node(|node| {
686 node.with_name("node2")
687 .with_command("invalid command")
688 .validator(true)
689 })
690 .build()
691 .unwrap_err();
692
693 assert_eq!(errors.len(), 1);
694 assert_eq!(
695 errors.first().unwrap().to_string(),
696 "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
697 );
698 }
699
700 #[test]
701 fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
702 ) {
703 let errors = RelaychainConfigBuilder::new(Default::default())
704 .with_chain("chain")
705 .with_default_resources(|resources| {
706 resources
707 .with_request_cpu("100Mi")
708 .with_limit_memory("1Gi")
709 .with_limit_cpu("invalid")
710 })
711 .with_node(|node| {
712 node.with_name("node")
713 .with_image("invalid image")
714 .validator(true)
715 })
716 .build()
717 .unwrap_err();
718
719 assert_eq!(errors.len(), 2);
720 assert_eq!(
721 errors.first().unwrap().to_string(),
722 "relaychain.default_resources.limit_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
723 );
724 assert_eq!(
725 errors.get(1).unwrap().to_string(),
726 "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
727 );
728 }
729
730 #[test]
731 fn relaychain_config_builder_should_works_with_chain_spec_command() {
732 const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
733 let config = RelaychainConfigBuilder::new(Default::default())
734 .with_chain("polkadot")
735 .with_default_image("myrepo:myimage")
736 .with_default_command("default_command")
737 .with_chain_spec_command(CMD_TPL)
738 .with_node(|node| node.with_name("node1").bootnode(true))
739 .build()
740 .unwrap();
741
742 assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
743 assert!(!config.chain_spec_command_is_local());
744 }
745
746 #[test]
747 fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
748 const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
749 let config = RelaychainConfigBuilder::new(Default::default())
750 .with_chain("polkadot")
751 .with_default_image("myrepo:myimage")
752 .with_default_command("default_command")
753 .with_chain_spec_command(CMD_TPL)
754 .chain_spec_command_is_local(true)
755 .with_node(|node| node.with_name("node1").bootnode(true))
756 .build()
757 .unwrap();
758
759 assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
760 assert!(config.chain_spec_command_is_local());
761 }
762}