feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo

- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt
- Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk
- Convert git dependencies to path dependencies
- Add vendor crates to workspace members
- Remove test/example crates from vendor (not needed for SDK)
- Fix feature propagation issues detected by zepter
- Fix workspace inheritance for internal dependencies
- All 606 crates now in workspace
- All 6919 internal dependency links verified correct
- No git dependencies remaining
This commit is contained in:
2025-12-22 23:31:24 +03:00
parent 4c8f281051
commit 70ddb6516f
386 changed files with 76759 additions and 36 deletions
@@ -0,0 +1,2 @@
/target
/Cargo.lock
@@ -0,0 +1,29 @@
[package]
name = "zombienet-configuration"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = true
license.workspace = true
repository.workspace = true
description = "Zombienet sdk config builder, allow to build a network configuration"
keywords = ["zombienet", "configuration", "sdk"]
[dependencies]
regex = { workspace = true }
lazy_static = { workspace = true }
multiaddr = { workspace = true }
url = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
anyhow = { workspace = true }
serde = { workspace = true, features = ["derive"] }
toml = { workspace = true }
serde_json = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
tracing = { workspace = true }
# zombienet deps
support = { workspace = true }
@@ -0,0 +1,383 @@
use std::{
error::Error,
fmt::Display,
net::IpAddr,
path::{Path, PathBuf},
str::FromStr,
};
use multiaddr::Multiaddr;
use serde::{Deserialize, Serialize};
use crate::{
shared::{
errors::{ConfigError, FieldError},
helpers::{merge_errors, merge_errors_vecs},
types::Duration,
},
utils::{default_as_true, default_node_spawn_timeout, default_timeout},
};
/// Global settings applied to an entire network.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GlobalSettings {
/// Global bootnodes to use (we will then add more)
#[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
bootnodes_addresses: Vec<Multiaddr>,
// TODO: parse both case in zombienet node version to avoid renamed ?
/// Global spawn timeout
#[serde(rename = "timeout", default = "default_timeout")]
network_spawn_timeout: Duration,
// TODO: not used yet
/// Node spawn timeout
#[serde(default = "default_node_spawn_timeout")]
node_spawn_timeout: Duration,
// TODO: not used yet
/// Local ip to use for construct the direct links
local_ip: Option<IpAddr>,
/// Directory to use as base dir
/// Used to reuse the same files (database) from a previous run,
/// also note that we will override the content of some of those files.
base_dir: Option<PathBuf>,
/// Number of concurrent spawning process to launch, None means try to spawn all at the same time.
spawn_concurrency: Option<usize>,
/// If enabled, will launch a task to monitor nodes' liveness and tear down the network if there are any.
#[serde(default = "default_as_true")]
tear_down_on_failure: bool,
}
impl GlobalSettings {
/// External bootnode address.
pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
self.bootnodes_addresses.iter().collect()
}
/// Global spawn timeout in seconds.
pub fn network_spawn_timeout(&self) -> Duration {
self.network_spawn_timeout
}
/// Individual node spawn timeout in seconds.
pub fn node_spawn_timeout(&self) -> Duration {
self.node_spawn_timeout
}
/// Local IP used to expose local services (including RPC, metrics and monitoring).
pub fn local_ip(&self) -> Option<&IpAddr> {
self.local_ip.as_ref()
}
/// Base directory to use (instead a random tmp one)
/// All the artifacts will be created in this directory.
pub fn base_dir(&self) -> Option<&Path> {
self.base_dir.as_deref()
}
/// Number of concurrent spawning process to launch
pub fn spawn_concurrency(&self) -> Option<usize> {
self.spawn_concurrency
}
/// A flag to tear down the network if there are any unresponsive nodes detected.
pub fn tear_down_on_failure(&self) -> bool {
self.tear_down_on_failure
}
}
impl Default for GlobalSettings {
fn default() -> Self {
Self {
bootnodes_addresses: Default::default(),
network_spawn_timeout: default_timeout(),
node_spawn_timeout: default_node_spawn_timeout(),
local_ip: Default::default(),
base_dir: Default::default(),
spawn_concurrency: Default::default(),
tear_down_on_failure: true,
}
}
}
/// A global settings builder, used to build [`GlobalSettings`] declaratively with fields validation.
#[derive(Default)]
pub struct GlobalSettingsBuilder {
config: GlobalSettings,
errors: Vec<anyhow::Error>,
}
impl GlobalSettingsBuilder {
pub fn new() -> Self {
Self::default()
}
// Transition to the next state of the builder.
fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
Self { config, errors }
}
/// Set the external bootnode address.
///
/// Note: Bootnode address replacements are NOT supported here.
/// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address.
pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
where
T: TryInto<Multiaddr> + Display + Copy,
T::Error: Error + Send + Sync + 'static,
{
let mut addrs = vec![];
let mut errors = vec![];
for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
match addr.try_into() {
Ok(addr) => addrs.push(addr),
Err(error) => errors.push(
FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
),
}
}
Self::transition(
GlobalSettings {
bootnodes_addresses: addrs,
..self.config
},
merge_errors_vecs(self.errors, errors),
)
}
/// Set global spawn timeout in seconds.
pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
Self::transition(
GlobalSettings {
network_spawn_timeout: timeout,
..self.config
},
self.errors,
)
}
/// Set individual node spawn timeout in seconds.
pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
Self::transition(
GlobalSettings {
node_spawn_timeout: timeout,
..self.config
},
self.errors,
)
}
/// Set local IP used to expose local services (including RPC, metrics and monitoring).
pub fn with_local_ip(self, local_ip: &str) -> Self {
match IpAddr::from_str(local_ip) {
Ok(local_ip) => Self::transition(
GlobalSettings {
local_ip: Some(local_ip),
..self.config
},
self.errors,
),
Err(error) => Self::transition(
self.config,
merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
),
}
}
/// Set the directory to use as base (instead of a random tmp one).
pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
Self::transition(
GlobalSettings {
base_dir: Some(base_dir.into()),
..self.config
},
self.errors,
)
}
/// Set the spawn concurrency
pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
Self::transition(
GlobalSettings {
spawn_concurrency: Some(spawn_concurrency),
..self.config
},
self.errors,
)
}
/// Set the `tear_down_on_failure` flag
pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
Self::transition(
GlobalSettings {
tear_down_on_failure,
..self.config
},
self.errors,
)
}
/// Seals the builder and returns a [`GlobalSettings`] if there are no validation errors, else returns errors.
pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
if !self.errors.is_empty() {
return Err(self
.errors
.into_iter()
.map(|error| ConfigError::GlobalSettings(error).into())
.collect::<Vec<_>>());
}
Ok(self.config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
let global_settings_config = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec![
"/ip4/10.41.122.55/tcp/45421",
"/ip4/51.144.222.10/tcp/2333",
])
.with_network_spawn_timeout(600)
.with_node_spawn_timeout(120)
.with_local_ip("10.0.0.1")
.with_base_dir("/home/nonroot/mynetwork")
.with_spawn_concurrency(5)
.with_tear_down_on_failure(true)
.build()
.unwrap();
let bootnodes_addresses: Vec<Multiaddr> = vec![
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
];
assert_eq!(
global_settings_config.bootnodes_addresses(),
bootnodes_addresses.iter().collect::<Vec<_>>()
);
assert_eq!(global_settings_config.network_spawn_timeout(), 600);
assert_eq!(global_settings_config.node_spawn_timeout(), 120);
assert_eq!(
global_settings_config
.local_ip()
.unwrap()
.to_string()
.as_str(),
"10.0.0.1"
);
assert_eq!(
global_settings_config.base_dir().unwrap(),
Path::new("/home/nonroot/mynetwork")
);
assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
assert!(global_settings_config.tear_down_on_failure());
}
#[test]
fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
let global_settings_config = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec![
"/ip4/10.41.122.55/tcp/45421",
"/ip4/51.144.222.10/tcp/2333",
])
.with_network_spawn_timeout(600)
.with_local_ip("10.0.0.1")
.build()
.unwrap();
let bootnodes_addresses: Vec<Multiaddr> = vec![
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
];
assert_eq!(
global_settings_config.bootnodes_addresses(),
bootnodes_addresses.iter().collect::<Vec<_>>()
);
assert_eq!(global_settings_config.network_spawn_timeout(), 600);
assert_eq!(global_settings_config.node_spawn_timeout(), 600);
assert_eq!(
global_settings_config
.local_ip()
.unwrap()
.to_string()
.as_str(),
"10.0.0.1"
);
}
#[test]
fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
) {
let errors = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
);
}
#[test]
fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
) {
let errors = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
.build()
.unwrap_err();
assert_eq!(errors.len(), 2);
assert_eq!(
errors.first().unwrap().to_string(),
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
);
assert_eq!(
errors.get(1).unwrap().to_string(),
"global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
);
}
#[test]
fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
let errors = GlobalSettingsBuilder::new()
.with_local_ip("invalid")
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"global_settings.local_ip: invalid IP address syntax"
);
}
#[test]
fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
) {
let errors = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
.with_local_ip("invalid")
.build()
.unwrap_err();
assert_eq!(errors.len(), 3);
assert_eq!(
errors.first().unwrap().to_string(),
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
);
assert_eq!(
errors.get(1).unwrap().to_string(),
"global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
);
assert_eq!(
errors.get(2).unwrap().to_string(),
"global_settings.local_ip: invalid IP address syntax"
);
}
}
@@ -0,0 +1,137 @@
use std::marker::PhantomData;
use serde::{Deserialize, Serialize};
use crate::shared::{macros::states, types::ParaId};
/// HRMP channel configuration, with fine-grained configuration options.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HrmpChannelConfig {
sender: ParaId,
recipient: ParaId,
max_capacity: u32,
max_message_size: u32,
}
impl HrmpChannelConfig {
/// The sending parachain ID.
pub fn sender(&self) -> ParaId {
self.sender
}
/// The receiving parachain ID.
pub fn recipient(&self) -> ParaId {
self.recipient
}
/// The maximum capacity of messages in the channel.
pub fn max_capacity(&self) -> u32 {
self.max_capacity
}
/// The maximum size of a message in the channel.
pub fn max_message_size(&self) -> u32 {
self.max_message_size
}
}
states! {
Initial,
WithSender,
WithRecipient
}
/// HRMP channel configuration builder, used to build an [`HrmpChannelConfig`] declaratively with fields validation.
pub struct HrmpChannelConfigBuilder<State> {
config: HrmpChannelConfig,
_state: PhantomData<State>,
}
impl Default for HrmpChannelConfigBuilder<Initial> {
fn default() -> Self {
Self {
config: HrmpChannelConfig {
sender: 0,
recipient: 0,
max_capacity: 8,
max_message_size: 512,
},
_state: PhantomData,
}
}
}
impl<A> HrmpChannelConfigBuilder<A> {
fn transition<B>(&self, config: HrmpChannelConfig) -> HrmpChannelConfigBuilder<B> {
HrmpChannelConfigBuilder {
config,
_state: PhantomData,
}
}
}
impl HrmpChannelConfigBuilder<Initial> {
pub fn new() -> Self {
Self::default()
}
/// Set the sending parachain ID.
pub fn with_sender(self, sender: ParaId) -> HrmpChannelConfigBuilder<WithSender> {
self.transition(HrmpChannelConfig {
sender,
..self.config
})
}
}
impl HrmpChannelConfigBuilder<WithSender> {
/// Set the receiving parachain ID.
pub fn with_recipient(self, recipient: ParaId) -> HrmpChannelConfigBuilder<WithRecipient> {
self.transition(HrmpChannelConfig {
recipient,
..self.config
})
}
}
impl HrmpChannelConfigBuilder<WithRecipient> {
/// Set the max capacity of messages in the channel.
pub fn with_max_capacity(self, max_capacity: u32) -> Self {
self.transition(HrmpChannelConfig {
max_capacity,
..self.config
})
}
/// Set the maximum size of a message in the channel.
pub fn with_max_message_size(self, max_message_size: u32) -> Self {
self.transition(HrmpChannelConfig {
max_message_size,
..self.config
})
}
pub fn build(self) -> HrmpChannelConfig {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hrmp_channel_config_builder_should_build_a_new_hrmp_channel_config_correctly() {
let hrmp_channel_config = HrmpChannelConfigBuilder::new()
.with_sender(1000)
.with_recipient(2000)
.with_max_capacity(50)
.with_max_message_size(100)
.build();
assert_eq!(hrmp_channel_config.sender(), 1000);
assert_eq!(hrmp_channel_config.recipient(), 2000);
assert_eq!(hrmp_channel_config.max_capacity(), 50);
assert_eq!(hrmp_channel_config.max_message_size(), 100);
}
}
@@ -0,0 +1,102 @@
//! This crate is used to create type safe configuration for Zombienet SDK using nested builders.
//!
//!
//! The main entry point of this crate is the [`NetworkConfigBuilder`] which is used to build a full network configuration
//! but all inner builders are also exposed to allow more granular control over the configuration.
//!
//! **Note**: Not all options can be checked at compile time and some will be checked at runtime when spawning a
//! network (e.g.: supported args for a specific node version).
//!
//! # Example
//! ```
//! use zombienet_configuration::NetworkConfigBuilder;
//!
//! let simple_configuration = NetworkConfigBuilder::new()
//! .with_relaychain(|relaychain| {
//! relaychain
//! .with_chain("polkadot")
//! .with_random_nominators_count(10)
//! .with_default_resources(|resources| {
//! resources
//! .with_limit_cpu("1000m")
//! .with_request_memory("1Gi")
//! .with_request_cpu(100_000)
//! })
//! .with_node(|node| {
//! node.with_name("node")
//! .with_command("command")
//! .validator(true)
//! })
//! })
//! .with_parachain(|parachain| {
//! parachain
//! .with_id(1000)
//! .with_chain("myparachain1")
//! .with_initial_balance(100_000)
//! .with_default_image("myimage:version")
//! .with_collator(|collator| {
//! collator
//! .with_name("collator1")
//! .with_command("command1")
//! .validator(true)
//! })
//! })
//! .with_parachain(|parachain| {
//! parachain
//! .with_id(2000)
//! .with_chain("myparachain2")
//! .with_initial_balance(50_0000)
//! .with_collator(|collator| {
//! collator
//! .with_name("collator2")
//! .with_command("command2")
//! .validator(true)
//! })
//! })
//! .with_hrmp_channel(|hrmp_channel1| {
//! hrmp_channel1
//! .with_sender(1)
//! .with_recipient(2)
//! .with_max_capacity(200)
//! .with_max_message_size(500)
//! })
//! .with_hrmp_channel(|hrmp_channel2| {
//! hrmp_channel2
//! .with_sender(2)
//! .with_recipient(1)
//! .with_max_capacity(100)
//! .with_max_message_size(250)
//! })
//! .with_global_settings(|global_settings| {
//! global_settings
//! .with_network_spawn_timeout(1200)
//! .with_node_spawn_timeout(240)
//! })
//! .build();
//!
//! assert!(simple_configuration.is_ok())
//! ```
#![allow(clippy::expect_fun_call)]
mod global_settings;
mod hrmp_channel;
mod network;
mod relaychain;
pub mod shared;
mod teyrchain;
mod utils;
pub use global_settings::{GlobalSettings, GlobalSettingsBuilder};
pub use hrmp_channel::{HrmpChannelConfig, HrmpChannelConfigBuilder};
pub use network::{NetworkConfig, NetworkConfigBuilder, WithRelaychain};
pub use relaychain::{RelaychainConfig, RelaychainConfigBuilder};
// re-export shared
pub use shared::{node::NodeConfig, types};
pub use teyrchain::{
states as para_states, RegistrationStrategy, TeyrchainConfig, TeyrchainConfigBuilder,
};
// Backward compatibility aliases for external crates that use Polkadot SDK terminology
// These allow zombienet-orchestrator and other external crates to work with our renamed types
pub type ParachainConfig = TeyrchainConfig;
pub type ParachainConfigBuilder<S, C> = TeyrchainConfigBuilder<S, C>;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
pub mod errors;
pub mod helpers;
pub mod macros;
pub mod node;
pub mod resources;
pub mod types;
@@ -0,0 +1,116 @@
use super::types::{ParaId, Port};
/// An error at the configuration level.
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
#[error("relaychain.{0}")]
Relaychain(anyhow::Error),
#[error("teyrchain[{0}].{1}")]
Teyrchain(ParaId, anyhow::Error),
#[error("global_settings.{0}")]
GlobalSettings(anyhow::Error),
#[error("nodes['{0}'].{1}")]
Node(String, anyhow::Error),
#[error("collators['{0}'].{1}")]
Collator(String, anyhow::Error),
}
/// An error at the field level.
#[derive(thiserror::Error, Debug)]
pub enum FieldError {
#[error("name: {0}")]
Name(anyhow::Error),
#[error("chain: {0}")]
Chain(anyhow::Error),
#[error("image: {0}")]
Image(anyhow::Error),
#[error("default_image: {0}")]
DefaultImage(anyhow::Error),
#[error("command: {0}")]
Command(anyhow::Error),
#[error("default_command: {0}")]
DefaultCommand(anyhow::Error),
#[error("bootnodes_addresses[{0}]: '{1}' {2}")]
BootnodesAddress(usize, String, anyhow::Error),
#[error("genesis_wasm_generator: {0}")]
GenesisWasmGenerator(anyhow::Error),
#[error("genesis_state_generator: {0}")]
GenesisStateGenerator(anyhow::Error),
#[error("local_ip: {0}")]
LocalIp(anyhow::Error),
#[error("default_resources.{0}")]
DefaultResources(anyhow::Error),
#[error("resources.{0}")]
Resources(anyhow::Error),
#[error("request_memory: {0}")]
RequestMemory(anyhow::Error),
#[error("request_cpu: {0}")]
RequestCpu(anyhow::Error),
#[error("limit_memory: {0}")]
LimitMemory(anyhow::Error),
#[error("limit_cpu: {0}")]
LimitCpu(anyhow::Error),
#[error("ws_port: {0}")]
WsPort(anyhow::Error),
#[error("rpc_port: {0}")]
RpcPort(anyhow::Error),
#[error("prometheus_port: {0}")]
PrometheusPort(anyhow::Error),
#[error("p2p_port: {0}")]
P2pPort(anyhow::Error),
#[error("session_key: {0}")]
SessionKey(anyhow::Error),
#[error("registration_strategy: {0}")]
RegistrationStrategy(anyhow::Error),
}
/// A conversion error for shared types across fields.
#[derive(thiserror::Error, Debug, Clone)]
pub enum ConversionError {
#[error("'{0}' shouldn't contains whitespace")]
ContainsWhitespaces(String),
#[error("'{}' doesn't match regex '{}'", .value, .regex)]
DoesntMatchRegex { value: String, regex: String },
#[error("can't be empty")]
CantBeEmpty,
#[error("deserialize error")]
DeserializeError(String),
}
/// A validation error for shared types across fields.
#[derive(thiserror::Error, Debug, Clone)]
pub enum ValidationError {
#[error("'{0}' is already used across config")]
PortAlreadyUsed(Port),
#[error("can't be empty")]
CantBeEmpty(),
}
@@ -0,0 +1,118 @@
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use support::constants::{BORROWABLE, THIS_IS_A_BUG};
use tracing::warn;
use super::{
errors::ValidationError,
types::{ParaId, Port, ValidationContext},
};
pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> {
let mut errors = errors;
errors.push(new_error);
errors
}
pub fn merge_errors_vecs(
errors: Vec<anyhow::Error>,
new_errors: Vec<anyhow::Error>,
) -> Vec<anyhow::Error> {
let mut errors = errors;
for new_error in new_errors.into_iter() {
errors.push(new_error);
}
errors
}
/// Generates a unique name from a base name and the names already present in a
/// [`ValidationContext`].
///
/// Uses [`generate_unique_node_name_from_names()`] internally to ensure uniqueness.
/// Logs a warning if the generated name differs from the original due to duplicates.
pub fn generate_unique_node_name(
node_name: impl Into<String>,
validation_context: Rc<RefCell<ValidationContext>>,
) -> String {
let mut context = validation_context
.try_borrow_mut()
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
generate_unique_node_name_from_names(node_name, &mut context.used_nodes_names)
}
/// Returns `node_name` if it is not already in `names`.
///
/// Otherwise, appends an incrementing `-{counter}` suffix until a unique name is found,
/// then returns it. Logs a warning when a duplicate is detected.
pub fn generate_unique_node_name_from_names(
node_name: impl Into<String>,
names: &mut HashSet<String>,
) -> String {
let node_name = node_name.into();
if names.insert(node_name.clone()) {
return node_name;
}
let mut counter = 1;
let mut candidate = node_name.clone();
while names.contains(&candidate) {
candidate = format!("{node_name}-{counter}");
counter += 1;
}
warn!(
original = %node_name,
adjusted = %candidate,
"Duplicate node name detected."
);
names.insert(candidate.clone());
candidate
}
pub fn ensure_value_is_not_empty(value: &str) -> Result<(), anyhow::Error> {
if value.is_empty() {
Err(ValidationError::CantBeEmpty().into())
} else {
Ok(())
}
}
pub fn ensure_port_unique(
port: Port,
validation_context: Rc<RefCell<ValidationContext>>,
) -> Result<(), anyhow::Error> {
let mut context = validation_context
.try_borrow_mut()
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
if !context.used_ports.contains(&port) {
context.used_ports.push(port);
return Ok(());
}
Err(ValidationError::PortAlreadyUsed(port).into())
}
pub fn generate_unique_para_id(
para_id: ParaId,
validation_context: Rc<RefCell<ValidationContext>>,
) -> String {
let mut context = validation_context
.try_borrow_mut()
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
if let Some(suffix) = context.used_para_ids.get_mut(&para_id) {
*suffix += 1;
format!("{para_id}-{suffix}")
} else {
// insert 0, since will be used next time.
context.used_para_ids.insert(para_id, 0);
para_id.to_string()
}
}
@@ -0,0 +1,11 @@
// Helper to define states of a type.
// We use an enum with no variants because it can't be constructed by definition.
macro_rules! states {
($($ident:ident),*) => {
$(
pub enum $ident {}
)*
};
}
pub(crate) use states;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,489 @@
use std::error::Error;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{
de::{self},
ser::SerializeStruct,
Deserialize, Serialize,
};
use support::constants::{SHOULD_COMPILE, THIS_IS_A_BUG};
use super::{
errors::{ConversionError, FieldError},
helpers::merge_errors,
};
/// A resource quantity used to define limits (k8s/podman only).
/// It can be constructed from a `&str` or u64, if it fails, it returns a [`ConversionError`].
/// Possible optional prefixes are: m, K, M, G, T, P, E, Ki, Mi, Gi, Ti, Pi, Ei
///
/// # Examples
///
/// ```
/// use zombienet_configuration::shared::resources::ResourceQuantity;
///
/// let quantity1: ResourceQuantity = "100000".try_into().unwrap();
/// let quantity2: ResourceQuantity = "1000m".try_into().unwrap();
/// let quantity3: ResourceQuantity = "1Gi".try_into().unwrap();
/// let quantity4: ResourceQuantity = 10_000.into();
///
/// assert_eq!(quantity1.as_str(), "100000");
/// assert_eq!(quantity2.as_str(), "1000m");
/// assert_eq!(quantity3.as_str(), "1Gi");
/// assert_eq!(quantity4.as_str(), "10000");
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResourceQuantity(String);
impl ResourceQuantity {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for ResourceQuantity {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$")
.expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
}
if !RE.is_match(value) {
return Err(ConversionError::DoesntMatchRegex {
value: value.to_string(),
regex: r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$".to_string(),
});
}
Ok(Self(value.to_string()))
}
}
impl From<u64> for ResourceQuantity {
fn from(value: u64) -> Self {
Self(value.to_string())
}
}
/// Resources limits used in the context of podman/k8s.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Resources {
request_memory: Option<ResourceQuantity>,
request_cpu: Option<ResourceQuantity>,
limit_memory: Option<ResourceQuantity>,
limit_cpu: Option<ResourceQuantity>,
}
#[derive(Serialize, Deserialize)]
struct ResourcesField {
memory: Option<ResourceQuantity>,
cpu: Option<ResourceQuantity>,
}
impl Serialize for Resources {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("Resources", 2)?;
if self.request_memory.is_some() || self.request_memory.is_some() {
state.serialize_field(
"requests",
&ResourcesField {
memory: self.request_memory.clone(),
cpu: self.request_cpu.clone(),
},
)?;
} else {
state.skip_field("requests")?;
}
if self.limit_memory.is_some() || self.limit_memory.is_some() {
state.serialize_field(
"limits",
&ResourcesField {
memory: self.limit_memory.clone(),
cpu: self.limit_cpu.clone(),
},
)?;
} else {
state.skip_field("limits")?;
}
state.end()
}
}
struct ResourcesVisitor;
impl<'de> de::Visitor<'de> for ResourcesVisitor {
type Value = Resources;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a resources object")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut resources: Resources = Resources::default();
while let Some((key, value)) = map.next_entry::<String, ResourcesField>()? {
match key.as_str() {
"requests" => {
resources.request_memory = value.memory;
resources.request_cpu = value.cpu;
},
"limits" => {
resources.limit_memory = value.memory;
resources.limit_cpu = value.cpu;
},
_ => {
return Err(de::Error::unknown_field(
&key,
&["requests", "limits", "cpu", "memory"],
))
},
}
}
Ok(resources)
}
}
impl<'de> Deserialize<'de> for Resources {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(ResourcesVisitor)
}
}
impl Resources {
/// Memory limit applied to requests.
pub fn request_memory(&self) -> Option<&ResourceQuantity> {
self.request_memory.as_ref()
}
/// CPU limit applied to requests.
pub fn request_cpu(&self) -> Option<&ResourceQuantity> {
self.request_cpu.as_ref()
}
/// Overall memory limit applied.
pub fn limit_memory(&self) -> Option<&ResourceQuantity> {
self.limit_memory.as_ref()
}
/// Overall CPU limit applied.
pub fn limit_cpu(&self) -> Option<&ResourceQuantity> {
self.limit_cpu.as_ref()
}
}
/// A resources builder, used to build a [`Resources`] declaratively with fields validation.
#[derive(Debug, Default)]
pub struct ResourcesBuilder {
config: Resources,
errors: Vec<anyhow::Error>,
}
impl ResourcesBuilder {
pub fn new() -> ResourcesBuilder {
Self::default()
}
fn transition(config: Resources, errors: Vec<anyhow::Error>) -> Self {
Self { config, errors }
}
/// Set the requested memory for a pod. This is the minimum memory allocated for a pod.
pub fn with_request_memory<T>(self, quantity: T) -> Self
where
T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static,
{
match quantity.try_into() {
Ok(quantity) => Self::transition(
Resources {
request_memory: Some(quantity),
..self.config
},
self.errors,
),
Err(error) => Self::transition(
self.config,
merge_errors(self.errors, FieldError::RequestMemory(error.into()).into()),
),
}
}
/// Set the requested CPU limit for a pod. This is the minimum CPU allocated for a pod.
pub fn with_request_cpu<T>(self, quantity: T) -> Self
where
T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static,
{
match quantity.try_into() {
Ok(quantity) => Self::transition(
Resources {
request_cpu: Some(quantity),
..self.config
},
self.errors,
),
Err(error) => Self::transition(
self.config,
merge_errors(self.errors, FieldError::RequestCpu(error.into()).into()),
),
}
}
/// Set the overall memory limit for a pod. This is the maximum memory threshold for a pod.
pub fn with_limit_memory<T>(self, quantity: T) -> Self
where
T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static,
{
match quantity.try_into() {
Ok(quantity) => Self::transition(
Resources {
limit_memory: Some(quantity),
..self.config
},
self.errors,
),
Err(error) => Self::transition(
self.config,
merge_errors(self.errors, FieldError::LimitMemory(error.into()).into()),
),
}
}
/// Set the overall CPU limit for a pod. This is the maximum CPU threshold for a pod.
pub fn with_limit_cpu<T>(self, quantity: T) -> Self
where
T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static,
{
match quantity.try_into() {
Ok(quantity) => Self::transition(
Resources {
limit_cpu: Some(quantity),
..self.config
},
self.errors,
),
Err(error) => Self::transition(
self.config,
merge_errors(self.errors, FieldError::LimitCpu(error.into()).into()),
),
}
}
/// Seals the builder and returns a [`Resources`] if there are no validation errors, else returns errors.
pub fn build(self) -> Result<Resources, Vec<anyhow::Error>> {
if !self.errors.is_empty() {
return Err(self.errors);
}
Ok(self.config)
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
use crate::NetworkConfig;
macro_rules! impl_resources_quantity_unit_test {
($val:literal) => {{
let resources = ResourcesBuilder::new()
.with_request_memory($val)
.build()
.unwrap();
assert_eq!(resources.request_memory().unwrap().as_str(), $val);
assert_eq!(resources.request_cpu(), None);
assert_eq!(resources.limit_cpu(), None);
assert_eq!(resources.limit_memory(), None);
}};
}
#[test]
fn converting_a_string_a_resource_quantity_without_unit_should_succeeds() {
impl_resources_quantity_unit_test!("1000");
}
#[test]
fn converting_a_str_with_m_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("100m");
}
#[test]
fn converting_a_str_with_K_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("50K");
}
#[test]
fn converting_a_str_with_M_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("100M");
}
#[test]
fn converting_a_str_with_G_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("1G");
}
#[test]
fn converting_a_str_with_T_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.01T");
}
#[test]
fn converting_a_str_with_P_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.00001P");
}
#[test]
fn converting_a_str_with_E_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.000000001E");
}
#[test]
fn converting_a_str_with_Ki_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("50Ki");
}
#[test]
fn converting_a_str_with_Mi_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("100Mi");
}
#[test]
fn converting_a_str_with_Gi_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("1Gi");
}
#[test]
fn converting_a_str_with_Ti_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.01Ti");
}
#[test]
fn converting_a_str_with_Pi_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.00001Pi");
}
#[test]
fn converting_a_str_with_Ei_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.000000001Ei");
}
#[test]
fn resources_config_builder_should_succeeds_and_returns_a_resources_config() {
let resources = ResourcesBuilder::new()
.with_request_memory("200M")
.with_request_cpu("1G")
.with_limit_cpu("500M")
.with_limit_memory("2G")
.build()
.unwrap();
assert_eq!(resources.request_memory().unwrap().as_str(), "200M");
assert_eq!(resources.request_cpu().unwrap().as_str(), "1G");
assert_eq!(resources.limit_cpu().unwrap().as_str(), "500M");
assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
}
#[test]
fn resources_config_toml_import_should_succeeds_and_returns_a_resources_config() {
let load_from_toml =
NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
let resources = load_from_toml.relaychain().default_resources().unwrap();
assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
assert_eq!(resources.request_cpu().unwrap().as_str(), "100000");
assert_eq!(resources.limit_cpu().unwrap().as_str(), "10Gi");
assert_eq!(resources.limit_memory().unwrap().as_str(), "4000M");
}
#[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_memory()
{
let resources_builder = ResourcesBuilder::new().with_request_memory("invalid");
let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
);
}
#[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_cpu() {
let resources_builder = ResourcesBuilder::new().with_request_cpu("invalid");
let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
r"request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
);
}
#[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_memory() {
let resources_builder = ResourcesBuilder::new().with_limit_memory("invalid");
let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
r"limit_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
);
}
#[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_cpu() {
let resources_builder = ResourcesBuilder::new().with_limit_cpu("invalid");
let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
);
}
#[test]
fn resources_config_builder_should_fails_and_returns_multiple_error_if_couldnt_parse_multiple_fields(
) {
let resources_builder = ResourcesBuilder::new()
.with_limit_cpu("invalid")
.with_request_memory("invalid");
let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 2);
assert_eq!(
errors.first().unwrap().to_string(),
r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
);
assert_eq!(
errors.get(1).unwrap().to_string(),
r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
);
}
}
@@ -0,0 +1,930 @@
use std::{
collections::{HashMap, HashSet},
error::Error,
fmt::{self, Display},
path::PathBuf,
str::FromStr,
};
use anyhow::anyhow;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{
de::{self, IntoDeserializer},
Deserialize, Deserializer, Serialize,
};
use support::constants::{INFAILABLE, SHOULD_COMPILE, THIS_IS_A_BUG};
use tokio::fs;
use url::Url;
use super::{errors::ConversionError, resources::Resources};
/// An alias for a duration in seconds.
pub type Duration = u32;
/// An alias for a port.
pub type Port = u16;
/// An alias for a parachain ID.
pub type ParaId = u32;
/// Custom type wrapping u128 to add custom Serialization/Deserialization logic because it's not supported
/// issue tracking the problem: <https://github.com/toml-rs/toml/issues/540>
#[derive(Default, Debug, Clone, PartialEq)]
pub struct U128(pub(crate) u128);
impl From<u128> for U128 {
fn from(value: u128) -> Self {
Self(value)
}
}
impl TryFrom<&str> for U128 {
type Error = Box<dyn Error>;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self(value.to_string().parse::<u128>()?))
}
}
impl Serialize for U128 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// here we add a prefix to the string to be able to replace the wrapped
// value with "" to a value without "" in the TOML string
serializer.serialize_str(&format!("U128%{}", self.0))
}
}
struct U128Visitor;
impl de::Visitor<'_> for U128Visitor {
type Value = U128;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between 0 and 2^128 1.")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
v.try_into().map_err(de::Error::custom)
}
}
impl<'de> Deserialize<'de> for U128 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(U128Visitor)
}
}
/// A chain name.
/// It can be constructed for an `&str`, if it fails, it will returns a [`ConversionError`].
///
/// # Examples:
/// ```
/// use zombienet_configuration::shared::types::Chain;
///
/// let polkadot: Chain = "polkadot".try_into().unwrap();
/// let kusama: Chain = "kusama".try_into().unwrap();
/// let myparachain: Chain = "myparachain".try_into().unwrap();
///
/// assert_eq!(polkadot.as_str(), "polkadot");
/// assert_eq!(kusama.as_str(), "kusama");
/// assert_eq!(myparachain.as_str(), "myparachain");
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Chain(String);
impl TryFrom<&str> for Chain {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.contains(char::is_whitespace) {
return Err(ConversionError::ContainsWhitespaces(value.to_string()));
}
if value.is_empty() {
return Err(ConversionError::CantBeEmpty);
}
Ok(Self(value.to_string()))
}
}
impl Chain {
pub fn as_str(&self) -> &str {
&self.0
}
}
/// A container image.
/// It can be constructed from an `&str` including a combination of name, version, IPv4 or/and hostname, if it fails, it will returns a [`ConversionError`].
///
/// # Examples:
/// ```
/// use zombienet_configuration::shared::types::Image;
///
/// let image1: Image = "name".try_into().unwrap();
/// let image2: Image = "name:version".try_into().unwrap();
/// let image3: Image = "myrepo.com/name:version".try_into().unwrap();
/// let image4: Image = "10.15.43.155/name:version".try_into().unwrap();
///
/// assert_eq!(image1.as_str(), "name");
/// assert_eq!(image2.as_str(), "name:version");
/// assert_eq!(image3.as_str(), "myrepo.com/name:version");
/// assert_eq!(image4.as_str(), "10.15.43.155/name:version");
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Image(String);
impl TryFrom<&str> for Image {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
static IP_PART: &str = "((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))";
static HOSTNAME_PART: &str = "((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]))";
static TAG_NAME_PART: &str = "([a-z0-9](-*[a-z0-9])*)";
static TAG_VERSION_PART: &str = "([a-z0-9_]([-._a-z0-9])*)";
lazy_static! {
static ref RE: Regex = Regex::new(&format!(
"^({IP_PART}|{HOSTNAME_PART}/)?{TAG_NAME_PART}(:{TAG_VERSION_PART})?$",
))
.expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
};
if !RE.is_match(value) {
return Err(ConversionError::DoesntMatchRegex {
value: value.to_string(),
regex: "^([ip]|[hostname]/)?[tag_name]:[tag_version]?$".to_string(),
});
}
Ok(Self(value.to_string()))
}
}
impl Image {
pub fn as_str(&self) -> &str {
&self.0
}
}
/// A command that will be executed natively (native provider) or in a container (podman/k8s).
/// It can be constructed from an `&str`, if it fails, it will returns a [`ConversionError`].
///
/// # Examples:
/// ```
/// use zombienet_configuration::shared::types::Command;
///
/// let command1: Command = "mycommand".try_into().unwrap();
/// let command2: Command = "myothercommand".try_into().unwrap();
///
/// assert_eq!(command1.as_str(), "mycommand");
/// assert_eq!(command2.as_str(), "myothercommand");
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Command(String);
impl TryFrom<&str> for Command {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.contains(char::is_whitespace) {
return Err(ConversionError::ContainsWhitespaces(value.to_string()));
}
Ok(Self(value.to_string()))
}
}
impl Default for Command {
fn default() -> Self {
Self(String::from("polkadot"))
}
}
impl Command {
pub fn as_str(&self) -> &str {
&self.0
}
}
/// A command with optional custom arguments, the command will be executed natively (native provider) or in a container (podman/k8s).
/// It can be constructed from an `&str`, if it fails, it will returns a [`ConversionError`].
///
/// # Examples:
/// ```
/// use zombienet_configuration::shared::types::CommandWithCustomArgs;
///
/// let command1: CommandWithCustomArgs = "mycommand --demo=2 --other-flag".try_into().unwrap();
/// let command2: CommandWithCustomArgs = "my_other_cmd_without_args".try_into().unwrap();
///
/// assert_eq!(command1.cmd().as_str(), "mycommand");
/// assert_eq!(command2.cmd().as_str(), "my_other_cmd_without_args");
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CommandWithCustomArgs(Command, Vec<Arg>);
impl TryFrom<&str> for CommandWithCustomArgs {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.is_empty() {
return Err(ConversionError::CantBeEmpty);
}
let mut parts = value.split_whitespace().collect::<Vec<&str>>();
let cmd = parts.remove(0).try_into().unwrap();
let args = parts
.iter()
.map(|x| {
Arg::deserialize(x.into_deserializer()).map_err(|_: serde_json::Error| {
ConversionError::DeserializeError(String::from(*x))
})
})
.collect::<Result<Vec<Arg>, _>>()?;
Ok(Self(cmd, args))
}
}
impl Default for CommandWithCustomArgs {
fn default() -> Self {
Self("polkadot".try_into().unwrap(), vec![])
}
}
impl CommandWithCustomArgs {
pub fn cmd(&self) -> &Command {
&self.0
}
pub fn args(&self) -> &Vec<Arg> {
&self.1
}
}
/// A location for a locally or remotely stored asset.
/// It can be constructed from an [`url::Url`], a [`std::path::PathBuf`] or an `&str`.
///
/// # Examples:
/// ```
/// use url::Url;
/// use std::{path::PathBuf, str::FromStr};
/// use zombienet_configuration::shared::types::AssetLocation;
///
/// let url_location: AssetLocation = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap().into();
/// let url_location2: AssetLocation = "https://mycloudstorage.com/path/to/my/file.tgz".into();
/// let path_location: AssetLocation = PathBuf::from_str("/tmp/path/to/my/file").unwrap().into();
/// let path_location2: AssetLocation = "/tmp/path/to/my/file".into();
///
/// assert!(matches!(url_location, AssetLocation::Url(value) if value.as_str() == "https://mycloudstorage.com/path/to/my/file.tgz"));
/// assert!(matches!(url_location2, AssetLocation::Url(value) if value.as_str() == "https://mycloudstorage.com/path/to/my/file.tgz"));
/// assert!(matches!(path_location, AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/path/to/my/file"));
/// assert!(matches!(path_location2, AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/path/to/my/file"));
/// ```
#[derive(Debug, Clone, PartialEq)]
pub enum AssetLocation {
Url(Url),
FilePath(PathBuf),
}
impl From<Url> for AssetLocation {
fn from(value: Url) -> Self {
Self::Url(value)
}
}
impl From<PathBuf> for AssetLocation {
fn from(value: PathBuf) -> Self {
Self::FilePath(value)
}
}
impl From<&str> for AssetLocation {
fn from(value: &str) -> Self {
if let Ok(parsed_url) = Url::parse(value) {
return Self::Url(parsed_url);
}
Self::FilePath(PathBuf::from_str(value).expect(&format!("{INFAILABLE}, {THIS_IS_A_BUG}")))
}
}
impl Display for AssetLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetLocation::Url(value) => write!(f, "{}", value.as_str()),
AssetLocation::FilePath(value) => write!(f, "{}", value.display()),
}
}
}
impl AssetLocation {
/// Get the current asset (from file or url) and return the content
pub async fn get_asset(&self) -> Result<Vec<u8>, anyhow::Error> {
let contents = match self {
AssetLocation::Url(location) => {
let res = reqwest::get(location.as_ref()).await.map_err(|err| {
anyhow!("Error dowinloding asset from url {location} - {err}")
})?;
res.bytes().await.unwrap().into()
},
AssetLocation::FilePath(filepath) => {
tokio::fs::read(filepath).await.map_err(|err| {
anyhow!(
"Error reading asset from path {} - {}",
filepath.to_string_lossy(),
err
)
})?
},
};
Ok(contents)
}
/// Write asset (from file or url) to the destination path.
pub async fn dump_asset(&self, dst_path: impl Into<PathBuf>) -> Result<(), anyhow::Error> {
let contents = self.get_asset().await?;
fs::write(dst_path.into(), contents).await?;
Ok(())
}
}
impl Serialize for AssetLocation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
struct AssetLocationVisitor;
impl de::Visitor<'_> for AssetLocationVisitor {
type Value = AssetLocation;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AssetLocation::from(v))
}
}
impl<'de> Deserialize<'de> for AssetLocation {
fn deserialize<D>(deserializer: D) -> Result<AssetLocation, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(AssetLocationVisitor)
}
}
/// A CLI argument passed to an executed command, can be an option with an assigned value or a simple flag to enable/disable a feature.
/// A flag arg can be constructed from a `&str` and a option arg can be constructed from a `(&str, &str)`.
///
/// # Examples:
/// ```
/// use zombienet_configuration::shared::types::Arg;
///
/// let flag_arg: Arg = "myflag".into();
/// let option_arg: Arg = ("name", "value").into();
///
/// assert!(matches!(flag_arg, Arg::Flag(value) if value == "myflag"));
/// assert!(matches!(option_arg, Arg::Option(name, value) if name == "name" && value == "value"));
/// ```
#[derive(Debug, Clone, PartialEq)]
pub enum Arg {
Flag(String),
Option(String, String),
Array(String, Vec<String>),
}
impl From<&str> for Arg {
fn from(flag: &str) -> Self {
Self::Flag(flag.to_owned())
}
}
impl From<(&str, &str)> for Arg {
fn from((option, value): (&str, &str)) -> Self {
Self::Option(option.to_owned(), value.to_owned())
}
}
impl<T> From<(&str, &[T])> for Arg
where
T: AsRef<str> + Clone,
{
fn from((option, values): (&str, &[T])) -> Self {
Self::Array(
option.to_owned(),
values.iter().map(|v| v.as_ref().to_string()).collect(),
)
}
}
impl<T> From<(&str, Vec<T>)> for Arg
where
T: AsRef<str>,
{
fn from((option, values): (&str, Vec<T>)) -> Self {
Self::Array(
option.to_owned(),
values.into_iter().map(|v| v.as_ref().to_string()).collect(),
)
}
}
impl Serialize for Arg {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Arg::Flag(value) => serializer.serialize_str(value),
Arg::Option(option, value) => serializer.serialize_str(&format!("{option}={value}")),
Arg::Array(option, values) => {
serializer.serialize_str(&format!("{}=[{}]", option, values.join(",")))
},
}
}
}
struct ArgVisitor;
impl de::Visitor<'_> for ArgVisitor {
type Value = Arg;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
// covers the "-lruntime=debug,parachain=trace" case
// TODO: Make this more generic by adding the scenario in the regex below
if v.starts_with("-l") || v.starts_with("-log") {
return Ok(Arg::Flag(v.to_string()));
}
// Handle argument removal syntax: -:--flag-name
if v.starts_with("-:") {
return Ok(Arg::Flag(v.to_string()));
}
let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})?(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>\\[[^\\]]*\\]|[^ ]+))?$").unwrap();
let captures = re.captures(v);
if let Some(captures) = captures {
if let Some(value) = captures.name("value") {
let name_prefix = captures
.name("name_prefix")
.expect("BUG: name_prefix capture group missing")
.as_str()
.to_string();
let val = value.as_str();
if val.starts_with('[') && val.ends_with(']') {
// Remove brackets and split by comma
let inner = &val[1..val.len() - 1];
let items: Vec<String> = inner
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
return Ok(Arg::Array(name_prefix, items));
} else {
return Ok(Arg::Option(name_prefix, val.to_string()));
}
}
if let Some(name_prefix) = captures.name("name_prefix") {
return Ok(Arg::Flag(name_prefix.as_str().to_string()));
}
}
Err(de::Error::custom(
"the provided argument is invalid and doesn't match Arg::Option, Arg::Flag or Arg::Array",
))
}
}
impl<'de> Deserialize<'de> for Arg {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ArgVisitor)
}
}
#[derive(Debug, Default, Clone)]
pub struct ValidationContext {
pub used_ports: Vec<Port>,
pub used_nodes_names: HashSet<String>,
// Store para_id already used
pub used_para_ids: HashMap<ParaId, u8>,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
pub struct ChainDefaultContext {
pub(crate) default_command: Option<Command>,
pub(crate) default_image: Option<Image>,
pub(crate) default_resources: Option<Resources>,
pub(crate) default_db_snapshot: Option<AssetLocation>,
#[serde(default)]
pub(crate) default_args: Vec<Arg>,
}
/// Represent a runtime (.wasm) asset location and an
/// optional preset to use for chain-spec generation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChainSpecRuntime {
pub location: AssetLocation,
pub preset: Option<String>,
}
impl ChainSpecRuntime {
pub fn new(location: AssetLocation) -> Self {
ChainSpecRuntime {
location,
preset: None,
}
}
pub fn with_preset(location: AssetLocation, preset: impl Into<String>) -> Self {
ChainSpecRuntime {
location,
preset: Some(preset.into()),
}
}
}
/// Represents a set of JSON overrides for a configuration.
///
/// The overrides can be provided as an inline JSON object or loaded from a
/// separate file via a path or URL.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonOverrides {
/// A path or URL pointing to a JSON file containing the overrides.
Location(AssetLocation),
/// An inline JSON value representing the overrides.
Json(serde_json::Value),
}
impl From<AssetLocation> for JsonOverrides {
fn from(value: AssetLocation) -> Self {
Self::Location(value)
}
}
impl From<serde_json::Value> for JsonOverrides {
fn from(value: serde_json::Value) -> Self {
Self::Json(value)
}
}
impl From<&str> for JsonOverrides {
fn from(value: &str) -> Self {
Self::Location(AssetLocation::from(value))
}
}
impl Display for JsonOverrides {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
JsonOverrides::Location(location) => write!(f, "{location}"),
JsonOverrides::Json(json) => write!(f, "{json}"),
}
}
}
impl JsonOverrides {
pub async fn get(&self) -> Result<serde_json::Value, anyhow::Error> {
let contents = match self {
Self::Location(location) => serde_json::from_slice(&location.get_asset().await?)
.map_err(|err| anyhow!("Error converting asset to json {location} - {err}")),
Self::Json(json) => Ok(json.clone()),
};
contents
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_arg_flag_roundtrip() {
let arg = Arg::from("verbose");
let serialized = serde_json::to_string(&arg).unwrap();
let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
assert_eq!(arg, deserialized);
}
#[test]
fn test_arg_option_roundtrip() {
let arg = Arg::from(("mode", "fast"));
let serialized = serde_json::to_string(&arg).unwrap();
let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
assert_eq!(arg, deserialized);
}
#[test]
fn test_arg_array_roundtrip() {
let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
let serialized = serde_json::to_string(&arg).unwrap();
println!("serialized = {serialized}");
let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
assert_eq!(arg, deserialized);
}
#[test]
fn test_arg_option_valid_input() {
let expected = Arg::from(("--foo", "bar"));
// name and value delimited with =
let valid = "\"--foo=bar\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
// name and value delimited with space
let valid = "\"--foo bar\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
// value contains =
let expected = Arg::from(("--foo", "bar=baz"));
let valid = "\"--foo=bar=baz\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_arg_array_valid_input() {
let expected = Arg::from(("--foo", vec!["bar", "baz"]));
// name and values delimited with =
let valid = "\"--foo=[bar,baz]\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
// name and values delimited with space
let valid = "\"--foo [bar,baz]\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
// values delimited with commas and space
let valid = "\"--foo [bar , baz]\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
// empty values array
let expected = Arg::from(("--foo", Vec::<&str>::new()));
let valid = "\"--foo []\"";
let result: Result<Arg, _> = serde_json::from_str(valid);
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_arg_invalid_input() {
// missing = or space
let invalid = "\"--foo[bar]\"";
let result: Result<Arg, _> = serde_json::from_str(invalid);
assert!(result.is_err());
// value contains space
let invalid = "\"--foo=bar baz\"";
let result: Result<Arg, _> = serde_json::from_str(invalid);
println!("result = {result:?}");
assert!(result.is_err());
}
#[test]
fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
let got: Result<Chain, ConversionError> = "mychain".try_into();
assert_eq!(got.unwrap().as_str(), "mychain");
}
#[test]
fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myimage".try_into();
assert_eq!(got.unwrap().as_str(), "myimage");
}
#[test]
fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myimage:version".try_into();
assert_eq!(got.unwrap().as_str(), "myimage:version");
}
#[test]
fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
}
#[test]
fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
{
let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
}
#[test]
fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
}
#[test]
fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
}
#[test]
fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
let got: Result<Command, ConversionError> = "mycommand".try_into();
assert_eq!(got.unwrap().as_str(), "mycommand");
}
#[test]
fn converting_an_url_into_an_asset_location_should_succeeds() {
let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
let got: AssetLocation = url.clone().into();
assert!(matches!(got, AssetLocation::Url(value) if value == url));
}
#[test]
fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
let got: AssetLocation = pathbuf.clone().into();
assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
}
#[test]
fn converting_a_str_into_an_url_asset_location_should_succeeds() {
let url = "https://mycloudstorage.com/path/to/my/file.tgz";
let got: AssetLocation = url.into();
assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
}
#[test]
fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
let filepath = "/tmp/path/to/my/file";
let got: AssetLocation = filepath.into();
assert!(matches!(
got,
AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
));
}
#[test]
fn converting_a_str_into_an_flag_arg_should_succeeds() {
let got: Arg = "myflag".into();
assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
}
#[test]
fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
let got: Arg = ("name", "value").into();
assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
}
#[test]
fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
let got: Result<Chain, ConversionError> = "my chain".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::ContainsWhitespaces(_)
));
assert_eq!(
got.unwrap_err().to_string(),
"'my chain' shouldn't contains whitespace"
);
}
#[test]
fn converting_an_empty_str_into_a_chain_should_fails() {
let got: Result<Chain, ConversionError> = "".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::CantBeEmpty
));
assert_eq!(got.unwrap_err().to_string(), "can't be empty");
}
#[test]
fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(
got.unwrap_err().to_string(),
"'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
);
}
#[test]
fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
}
#[test]
fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "myrepository.com".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
}
#[test]
fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
}
#[test]
fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
let got: Result<Command, ConversionError> = "my command".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::ContainsWhitespaces(_)
));
assert_eq!(
got.unwrap_err().to_string(),
"'my command' shouldn't contains whitespace"
);
}
#[test]
fn test_convert_to_json_overrides() {
let url: AssetLocation = "https://example.com/overrides.json".into();
assert!(matches!(
url.into(),
JsonOverrides::Location(AssetLocation::Url(_))
));
let path: AssetLocation = "/path/to/overrides.json".into();
assert!(matches!(
path.into(),
JsonOverrides::Location(AssetLocation::FilePath(_))
));
let inline = serde_json::json!({ "para_id": 2000});
assert!(matches!(
inline.into(),
JsonOverrides::Json(serde_json::Value::Object(_))
));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
use std::env;
use support::constants::ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS;
use crate::types::{Chain, Command, Duration};
pub(crate) fn is_true(value: &bool) -> bool {
*value
}
pub(crate) fn is_false(value: &bool) -> bool {
!(*value)
}
pub(crate) fn default_as_true() -> bool {
true
}
pub(crate) fn default_as_false() -> bool {
false
}
pub(crate) fn default_initial_balance() -> crate::types::U128 {
2_000_000_000_000.into()
}
/// Default timeout for spawning a node (10mins)
pub(crate) fn default_node_spawn_timeout() -> Duration {
env::var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS)
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(600)
}
/// Default timeout for spawning the whole network (1hr)
pub(crate) fn default_timeout() -> Duration {
3600
}
pub(crate) fn default_command_polkadot() -> Option<Command> {
TryInto::<Command>::try_into("polkadot").ok()
}
pub(crate) fn default_relaychain_chain() -> Chain {
TryInto::<Chain>::try_into("rococo-local").expect("'rococo-local' should be a valid chain")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_node_spawn_timeout_works_before_and_after_env_is_set() {
// The default should be 600 seconds if the env var is not set
assert_eq!(default_node_spawn_timeout(), 600);
// If env var is set to a valid number, it should return that number
env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "123");
assert_eq!(default_node_spawn_timeout(), 123);
// If env var is set to a NOT valid number, it should return 600
env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "NOT_A_NUMBER");
assert_eq!(default_node_spawn_timeout(), 600);
}
}
@@ -0,0 +1,25 @@
[settings]
timeout = 3600
node_spawn_timeout = 600
tear_down_on_failure = true
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
default_image = "docker.io/parity/polkadot:latest"
default_args = ["-lparachain=debug"]
[[relaychain.nodes]]
name = "alice"
validator = true
invulnerable = true
bootnode = false
balance = 2000000000000
[[relaychain.nodes]]
name = "bob"
args = ["--database=paritydb-experimental"]
validator = true
invulnerable = false
bootnode = true
balance = 2000000000000
@@ -0,0 +1,105 @@
[settings]
timeout = 3600
node_spawn_timeout = 600
tear_down_on_failure = true
[relaychain]
chain = "polkadot"
default_command = "polkadot"
default_image = "docker.io/parity/polkadot:latest"
[relaychain.default_resources.requests]
memory = "500M"
cpu = "100000"
[relaychain.default_resources.limits]
memory = "4000M"
cpu = "10Gi"
[[relaychain.nodes]]
name = "alice"
validator = true
invulnerable = true
bootnode = true
balance = 1000000000
[[relaychain.nodes]]
name = "bob"
validator = true
invulnerable = true
bootnode = true
balance = 2000000000000
[[teyrchains]]
id = 1000
chain = "myparachain"
register_para = true
onboard_as_teyrchain = false
balance = 2000000000000
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
chain_spec_path = "/path/to/my/chain/spec.json"
cumulus_based = true
evm_based = false
[[teyrchains.collators]]
name = "john"
validator = true
invulnerable = true
bootnode = true
balance = 5000000000
[[teyrchains.collators]]
name = "charles"
validator = false
invulnerable = true
bootnode = true
balance = 0
[[teyrchains.collators]]
name = "frank"
validator = true
invulnerable = false
bootnode = true
balance = 1000000000
[[teyrchains]]
id = 2000
chain = "myotherparachain"
add_to_genesis = true
balance = 2000000000000
chain_spec_path = "/path/to/my/other/chain/spec.json"
cumulus_based = true
evm_based = false
[[teyrchains.collators]]
name = "mike"
validator = true
invulnerable = true
bootnode = true
balance = 5000000000
[[teyrchains.collators]]
name = "georges"
validator = false
invulnerable = true
bootnode = true
balance = 0
[[teyrchains.collators]]
name = "victor"
validator = true
invulnerable = false
bootnode = true
balance = 1000000000
[[hrmp_channels]]
sender = 1000
recipient = 2000
max_capacity = 150
max_message_size = 5000
[[hrmp_channels]]
sender = 2000
recipient = 1000
max_capacity = 200
max_message_size = 8000
@@ -0,0 +1,76 @@
[settings]
timeout = 3600
node_spawn_timeout = 600
tear_down_on_failure = true
[relaychain]
chain = "polkadot"
default_command = "polkadot"
default_image = "docker.io/parity/polkadot:latest"
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
default_args = [
"-name=value",
"--flag",
]
[relaychain.default_resources.requests]
memory = "500M"
cpu = "100000"
[relaychain.default_resources.limits]
memory = "4000M"
cpu = "10Gi"
[[relaychain.nodes]]
name = "alice"
validator = true
invulnerable = true
bootnode = true
balance = 1000000000
[[relaychain.nodes]]
name = "bob"
image = "mycustomimage:latest"
command = "my-custom-command"
args = ["-myothername=value"]
validator = true
invulnerable = true
bootnode = true
balance = 2000000000000
db_snapshot = "https://storage.com/path/to/other/db_snapshot.tgz"
[relaychain.nodes.resources.requests]
memory = "250Mi"
cpu = "1000"
[relaychain.nodes.resources.limits]
memory = "2Gi"
cpu = "5Gi"
[[teyrchains]]
id = 1000
chain = "myparachain"
add_to_genesis = true
balance = 2000000000000
default_command = "my-default-command"
default_image = "mydefaultimage:latest"
default_db_snapshot = "https://storage.com/path/to/other_snapshot.tgz"
chain_spec_path = "/path/to/my/chain/spec.json"
cumulus_based = true
evm_based = false
[[teyrchains.collators]]
name = "john"
image = "anotherimage:latest"
command = "my-non-default-command"
validator = true
invulnerable = true
bootnode = true
balance = 5000000000
[[teyrchains.collators]]
name = "charles"
validator = false
invulnerable = true
bootnode = true
balance = 0
@@ -0,0 +1,57 @@
[settings]
timeout = 3600
node_spawn_timeout = 600
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
default_image = "docker.io/parity/polkadot:latest"
default_args = ["-lparachain=debug"]
[[relaychain.nodes]]
name = "alice"
validator = true
invulnerable = true
bootnode = false
balance = 2000000000000
[[relaychain.nodes]]
name = "bob"
args = ["--database=paritydb-experimental"]
validator = true
invulnerable = false
bootnode = true
balance = 2000000000000
[[teyrchains]]
id = 1000
chain = "myparachain"
onboard_as_teyrchain = false
balance = 2000000000000
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
chain_spec_path = "/path/to/my/chain/spec.json"
cumulus_based = true
[teyrchains.collator]
name = "john"
validator = true
invulnerable = true
bootnode = true
balance = 5000000000
[[teyrchains]]
id = 1000
chain = "myparachain"
onboard_as_teyrchain = false
balance = 2000000000000
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
chain_spec_path = "/path/to/my/chain/spec.json"
cumulus_based = true
evm_based = true
[[teyrchains.collators]]
name = "john"
validator = true
invulnerable = true
bootnode = true
balance = 5000000000
@@ -0,0 +1,9 @@
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
[[relaychain.nodes]]
name = "alice"
[[relaychain.nodes]]
name = "bob"
@@ -0,0 +1,17 @@
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
wasm_override = "/some/path/runtime.wasm"
[[relaychain.nodes]]
name = "alice"
[[relaychain.nodes]]
name = "bob"
[[teyrchains]]
id = 1000
wasm_override = "https://some.com/runtime.wasm"
[teyrchains.collator]
name = "john"
@@ -0,0 +1,27 @@
[relaychain]
default_command = "polkadot"
chain_spec_path = "./rc.json"
[[relaychain.nodes]]
name = "alice"
validator = true
rpc_port = 9944
[[relaychain.nodes]]
name = "bob"
validator = true
rpc_port = 9945
args = [
"-lruntime::system=debug,runtime::session=trace,runtime::staking::ah-client=trace,runtime::ah-client=debug",
]
[[teyrchains]]
id = 1100
chain_spec_path = "./parachain.json"
[teyrchains.collator]
name = "charlie"
rpc_port = 9946
args = [
"-lruntime::system=debug,runtime::multiblock-election=trace,runtime::staking=debug,runtime::staking::rc-client=trace,runtime::rc-client=debug",
]
@@ -0,0 +1,40 @@
[settings]
timeout = 3600
node_spawn_timeout = 600
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
default_image = "docker.io/parity/polkadot:latest"
default_args = ["-lparachain=debug"]
[[relaychain.nodes]]
name = "alice"
validator = true
invulnerable = true
bootnode = false
balance = 2000000000000
[[relaychain.nodes]]
name = "bob"
args = ["--database=paritydb-experimental"]
validator = true
invulnerable = false
bootnode = true
balance = 2000000000000
[[teyrchains]]
id = 1000
chain = "myparachain"
onboard_as_teyrchain = false
balance = 2000000000000
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
chain_spec_path = "/path/to/my/chain/spec.json"
cumulus_based = true
[teyrchains.collator]
name = "alice"
validator = true
invulnerable = true
bootnode = true
balance = 5000000000
@@ -0,0 +1,17 @@
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
raw_spec_override = "/some/path/raw_spec_override.json"
[[relaychain.nodes]]
name = "alice"
[[relaychain.nodes]]
name = "bob"
[[teyrchains]]
id = 1000
raw_spec_override = "https://some.com/raw_spec_override.json"
[teyrchains.collator]
name = "john"