fix: migrate vendor rustfmt.toml to stable-only features

- Update vendor/pezkuwi-zombienet-sdk/rustfmt.toml to stable-only
- Reformat 74 vendor files with stable rustfmt
- Remove nightly-only features causing CI failures
This commit is contained in:
2025-12-23 10:00:48 +03:00
parent ae7321e239
commit 44cbe4a280
74 changed files with 19895 additions and 21681 deletions
+3 -1
View File
@@ -302,7 +302,9 @@ impl<T: Config> ExtrinsicEvents<T> {
/// ///
/// This works in the same way that [`events::Events::find()`] does, with the /// This works in the same way that [`events::Events::find()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic. /// exception that it filters out events not related to the submitted extrinsic.
pub fn find<'a, Ev: events::StaticEvent + 'a>(&'a self) -> impl Iterator<Item = Result<Ev, EventsError>> + 'a { pub fn find<'a, Ev: events::StaticEvent + 'a>(
&'a self,
) -> impl Iterator<Item = Result<Ev, EventsError>> + 'a {
self.iter().filter_map(|ev| ev.and_then(|ev| ev.as_event::<Ev>()).transpose()) self.iter().filter_map(|ev| ev.and_then(|ev| ev.as_event::<Ev>()).transpose())
} }
@@ -1,383 +1,339 @@
use std::{ use std::{
error::Error, error::Error,
fmt::Display, fmt::Display,
net::IpAddr, net::IpAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use multiaddr::Multiaddr; use multiaddr::Multiaddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
shared::{ shared::{
errors::{ConfigError, FieldError}, errors::{ConfigError, FieldError},
helpers::{merge_errors, merge_errors_vecs}, helpers::{merge_errors, merge_errors_vecs},
types::Duration, types::Duration,
}, },
utils::{default_as_true, default_node_spawn_timeout, default_timeout}, utils::{default_as_true, default_node_spawn_timeout, default_timeout},
}; };
/// Global settings applied to an entire network. /// Global settings applied to an entire network.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GlobalSettings { pub struct GlobalSettings {
/// Global bootnodes to use (we will then add more) /// Global bootnodes to use (we will then add more)
#[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)] #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
bootnodes_addresses: Vec<Multiaddr>, bootnodes_addresses: Vec<Multiaddr>,
// TODO: parse both case in zombienet node version to avoid renamed ? // TODO: parse both case in zombienet node version to avoid renamed ?
/// Global spawn timeout /// Global spawn timeout
#[serde(rename = "timeout", default = "default_timeout")] #[serde(rename = "timeout", default = "default_timeout")]
network_spawn_timeout: Duration, network_spawn_timeout: Duration,
// TODO: not used yet // TODO: not used yet
/// Node spawn timeout /// Node spawn timeout
#[serde(default = "default_node_spawn_timeout")] #[serde(default = "default_node_spawn_timeout")]
node_spawn_timeout: Duration, node_spawn_timeout: Duration,
// TODO: not used yet // TODO: not used yet
/// Local ip to use for construct the direct links /// Local ip to use for construct the direct links
local_ip: Option<IpAddr>, local_ip: Option<IpAddr>,
/// Directory to use as base dir /// Directory to use as base dir
/// Used to reuse the same files (database) from a previous run, /// Used to reuse the same files (database) from a previous run,
/// also note that we will override the content of some of those files. /// also note that we will override the content of some of those files.
base_dir: Option<PathBuf>, base_dir: Option<PathBuf>,
/// Number of concurrent spawning process to launch, None means try to spawn all at the same time. /// Number of concurrent spawning process to launch, None means try to spawn all at the same time.
spawn_concurrency: Option<usize>, spawn_concurrency: Option<usize>,
/// If enabled, will launch a task to monitor nodes' liveness and tear down the network if there are any. /// If enabled, will launch a task to monitor nodes' liveness and tear down the network if there are any.
#[serde(default = "default_as_true")] #[serde(default = "default_as_true")]
tear_down_on_failure: bool, tear_down_on_failure: bool,
} }
impl GlobalSettings { impl GlobalSettings {
/// External bootnode address. /// External bootnode address.
pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> { pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
self.bootnodes_addresses.iter().collect() self.bootnodes_addresses.iter().collect()
} }
/// Global spawn timeout in seconds. /// Global spawn timeout in seconds.
pub fn network_spawn_timeout(&self) -> Duration { pub fn network_spawn_timeout(&self) -> Duration {
self.network_spawn_timeout self.network_spawn_timeout
} }
/// Individual node spawn timeout in seconds. /// Individual node spawn timeout in seconds.
pub fn node_spawn_timeout(&self) -> Duration { pub fn node_spawn_timeout(&self) -> Duration {
self.node_spawn_timeout self.node_spawn_timeout
} }
/// Local IP used to expose local services (including RPC, metrics and monitoring). /// Local IP used to expose local services (including RPC, metrics and monitoring).
pub fn local_ip(&self) -> Option<&IpAddr> { pub fn local_ip(&self) -> Option<&IpAddr> {
self.local_ip.as_ref() self.local_ip.as_ref()
} }
/// Base directory to use (instead a random tmp one) /// Base directory to use (instead a random tmp one)
/// All the artifacts will be created in this directory. /// All the artifacts will be created in this directory.
pub fn base_dir(&self) -> Option<&Path> { pub fn base_dir(&self) -> Option<&Path> {
self.base_dir.as_deref() self.base_dir.as_deref()
} }
/// Number of concurrent spawning process to launch /// Number of concurrent spawning process to launch
pub fn spawn_concurrency(&self) -> Option<usize> { pub fn spawn_concurrency(&self) -> Option<usize> {
self.spawn_concurrency self.spawn_concurrency
} }
/// A flag to tear down the network if there are any unresponsive nodes detected. /// A flag to tear down the network if there are any unresponsive nodes detected.
pub fn tear_down_on_failure(&self) -> bool { pub fn tear_down_on_failure(&self) -> bool {
self.tear_down_on_failure self.tear_down_on_failure
} }
} }
impl Default for GlobalSettings { impl Default for GlobalSettings {
fn default() -> Self { fn default() -> Self {
Self { Self {
bootnodes_addresses: Default::default(), bootnodes_addresses: Default::default(),
network_spawn_timeout: default_timeout(), network_spawn_timeout: default_timeout(),
node_spawn_timeout: default_node_spawn_timeout(), node_spawn_timeout: default_node_spawn_timeout(),
local_ip: Default::default(), local_ip: Default::default(),
base_dir: Default::default(), base_dir: Default::default(),
spawn_concurrency: Default::default(), spawn_concurrency: Default::default(),
tear_down_on_failure: true, tear_down_on_failure: true,
} }
} }
} }
/// A global settings builder, used to build [`GlobalSettings`] declaratively with fields validation. /// A global settings builder, used to build [`GlobalSettings`] declaratively with fields validation.
#[derive(Default)] #[derive(Default)]
pub struct GlobalSettingsBuilder { pub struct GlobalSettingsBuilder {
config: GlobalSettings, config: GlobalSettings,
errors: Vec<anyhow::Error>, errors: Vec<anyhow::Error>,
} }
impl GlobalSettingsBuilder { impl GlobalSettingsBuilder {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
// Transition to the next state of the builder. // Transition to the next state of the builder.
fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self { fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
Self { config, errors } Self { config, errors }
} }
/// Set the external bootnode address. /// Set the external bootnode address.
/// ///
/// Note: Bootnode address replacements are NOT supported here. /// Note: Bootnode address replacements are NOT supported here.
/// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address. /// 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 pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
where where
T: TryInto<Multiaddr> + Display + Copy, T: TryInto<Multiaddr> + Display + Copy,
T::Error: Error + Send + Sync + 'static, T::Error: Error + Send + Sync + 'static,
{ {
let mut addrs = vec![]; let mut addrs = vec![];
let mut errors = vec![]; let mut errors = vec![];
for (index, addr) in bootnodes_addresses.into_iter().enumerate() { for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
match addr.try_into() { match addr.try_into() {
Ok(addr) => addrs.push(addr), Ok(addr) => addrs.push(addr),
Err(error) => errors.push( Err(error) => errors.push(
FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(), FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
), ),
} }
} }
Self::transition( Self::transition(
GlobalSettings { GlobalSettings { bootnodes_addresses: addrs, ..self.config },
bootnodes_addresses: addrs, merge_errors_vecs(self.errors, errors),
..self.config )
}, }
merge_errors_vecs(self.errors, errors),
)
}
/// Set global spawn timeout in seconds. /// Set global spawn timeout in seconds.
pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self { pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
Self::transition( Self::transition(
GlobalSettings { GlobalSettings { network_spawn_timeout: timeout, ..self.config },
network_spawn_timeout: timeout, self.errors,
..self.config )
}, }
self.errors,
)
}
/// Set individual node spawn timeout in seconds. /// Set individual node spawn timeout in seconds.
pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self { pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
Self::transition( Self::transition(GlobalSettings { node_spawn_timeout: timeout, ..self.config }, self.errors)
GlobalSettings { }
node_spawn_timeout: timeout,
..self.config
},
self.errors,
)
}
/// Set local IP used to expose local services (including RPC, metrics and monitoring). /// Set local IP used to expose local services (including RPC, metrics and monitoring).
pub fn with_local_ip(self, local_ip: &str) -> Self { pub fn with_local_ip(self, local_ip: &str) -> Self {
match IpAddr::from_str(local_ip) { match IpAddr::from_str(local_ip) {
Ok(local_ip) => Self::transition( Ok(local_ip) => Self::transition(
GlobalSettings { GlobalSettings { local_ip: Some(local_ip), ..self.config },
local_ip: Some(local_ip), self.errors,
..self.config ),
}, Err(error) => Self::transition(
self.errors, self.config,
), merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
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). /// 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 { pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
Self::transition( Self::transition(
GlobalSettings { GlobalSettings { base_dir: Some(base_dir.into()), ..self.config },
base_dir: Some(base_dir.into()), self.errors,
..self.config )
}, }
self.errors,
)
}
/// Set the spawn concurrency /// Set the spawn concurrency
pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self { pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
Self::transition( Self::transition(
GlobalSettings { GlobalSettings { spawn_concurrency: Some(spawn_concurrency), ..self.config },
spawn_concurrency: Some(spawn_concurrency), self.errors,
..self.config )
}, }
self.errors,
)
}
/// Set the `tear_down_on_failure` flag /// Set the `tear_down_on_failure` flag
pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self { pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
Self::transition( Self::transition(GlobalSettings { tear_down_on_failure, ..self.config }, self.errors)
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. /// 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>> { pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
if !self.errors.is_empty() { if !self.errors.is_empty() {
return Err(self return Err(self
.errors .errors
.into_iter() .into_iter()
.map(|error| ConfigError::GlobalSettings(error).into()) .map(|error| ConfigError::GlobalSettings(error).into())
.collect::<Vec<_>>()); .collect::<Vec<_>>());
} }
Ok(self.config) Ok(self.config)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() { fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
let global_settings_config = GlobalSettingsBuilder::new() let global_settings_config = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec![ .with_raw_bootnodes_addresses(vec![
"/ip4/10.41.122.55/tcp/45421", "/ip4/10.41.122.55/tcp/45421",
"/ip4/51.144.222.10/tcp/2333", "/ip4/51.144.222.10/tcp/2333",
]) ])
.with_network_spawn_timeout(600) .with_network_spawn_timeout(600)
.with_node_spawn_timeout(120) .with_node_spawn_timeout(120)
.with_local_ip("10.0.0.1") .with_local_ip("10.0.0.1")
.with_base_dir("/home/nonroot/mynetwork") .with_base_dir("/home/nonroot/mynetwork")
.with_spawn_concurrency(5) .with_spawn_concurrency(5)
.with_tear_down_on_failure(true) .with_tear_down_on_failure(true)
.build() .build()
.unwrap(); .unwrap();
let bootnodes_addresses: Vec<Multiaddr> = vec![ let bootnodes_addresses: Vec<Multiaddr> = vec![
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(), "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(), "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
]; ];
assert_eq!( assert_eq!(
global_settings_config.bootnodes_addresses(), global_settings_config.bootnodes_addresses(),
bootnodes_addresses.iter().collect::<Vec<_>>() bootnodes_addresses.iter().collect::<Vec<_>>()
); );
assert_eq!(global_settings_config.network_spawn_timeout(), 600); assert_eq!(global_settings_config.network_spawn_timeout(), 600);
assert_eq!(global_settings_config.node_spawn_timeout(), 120); assert_eq!(global_settings_config.node_spawn_timeout(), 120);
assert_eq!( assert_eq!(global_settings_config.local_ip().unwrap().to_string().as_str(), "10.0.0.1");
global_settings_config assert_eq!(
.local_ip() global_settings_config.base_dir().unwrap(),
.unwrap() Path::new("/home/nonroot/mynetwork")
.to_string() );
.as_str(), assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
"10.0.0.1" assert!(global_settings_config.tear_down_on_failure());
); }
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] #[test]
fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() { fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
let global_settings_config = GlobalSettingsBuilder::new() let global_settings_config = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec![ .with_raw_bootnodes_addresses(vec![
"/ip4/10.41.122.55/tcp/45421", "/ip4/10.41.122.55/tcp/45421",
"/ip4/51.144.222.10/tcp/2333", "/ip4/51.144.222.10/tcp/2333",
]) ])
.with_network_spawn_timeout(600) .with_network_spawn_timeout(600)
.with_local_ip("10.0.0.1") .with_local_ip("10.0.0.1")
.build() .build()
.unwrap(); .unwrap();
let bootnodes_addresses: Vec<Multiaddr> = vec![ let bootnodes_addresses: Vec<Multiaddr> = vec![
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(), "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(), "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
]; ];
assert_eq!( assert_eq!(
global_settings_config.bootnodes_addresses(), global_settings_config.bootnodes_addresses(),
bootnodes_addresses.iter().collect::<Vec<_>>() bootnodes_addresses.iter().collect::<Vec<_>>()
); );
assert_eq!(global_settings_config.network_spawn_timeout(), 600); assert_eq!(global_settings_config.network_spawn_timeout(), 600);
assert_eq!(global_settings_config.node_spawn_timeout(), 600); assert_eq!(global_settings_config.node_spawn_timeout(), 600);
assert_eq!( assert_eq!(global_settings_config.local_ip().unwrap().to_string().as_str(), "10.0.0.1");
global_settings_config }
.local_ip()
.unwrap()
.to_string()
.as_str(),
"10.0.0.1"
);
}
#[test] #[test]
fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid( fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
) { ) {
let errors = GlobalSettingsBuilder::new() let errors = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"]) .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
.build() .build()
.unwrap_err(); .unwrap_err();
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), errors.first().unwrap().to_string(),
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax" "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
); );
} }
#[test] #[test]
fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid( fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
) { ) {
let errors = GlobalSettingsBuilder::new() let errors = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"]) .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
.build() .build()
.unwrap_err(); .unwrap_err();
assert_eq!(errors.len(), 2); assert_eq!(errors.len(), 2);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), errors.first().unwrap().to_string(),
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax" "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
); );
assert_eq!( assert_eq!(
errors.get(1).unwrap().to_string(), errors.get(1).unwrap().to_string(),
"global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: " "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
); );
} }
#[test] #[test]
fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() { fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
let errors = GlobalSettingsBuilder::new() let errors = GlobalSettingsBuilder::new().with_local_ip("invalid").build().unwrap_err();
.with_local_ip("invalid")
.build()
.unwrap_err();
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), errors.first().unwrap().to_string(),
"global_settings.local_ip: invalid IP address syntax" "global_settings.local_ip: invalid IP address syntax"
); );
} }
#[test] #[test]
fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid( fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
) { ) {
let errors = GlobalSettingsBuilder::new() let errors = GlobalSettingsBuilder::new()
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"]) .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
.with_local_ip("invalid") .with_local_ip("invalid")
.build() .build()
.unwrap_err(); .unwrap_err();
assert_eq!(errors.len(), 3); assert_eq!(errors.len(), 3);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), errors.first().unwrap().to_string(),
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax" "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
); );
assert_eq!( assert_eq!(
errors.get(1).unwrap().to_string(), errors.get(1).unwrap().to_string(),
"global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: " "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
); );
assert_eq!( assert_eq!(
errors.get(2).unwrap().to_string(), errors.get(2).unwrap().to_string(),
"global_settings.local_ip: invalid IP address syntax" "global_settings.local_ip: invalid IP address syntax"
); );
} }
} }
@@ -7,131 +7,116 @@ use crate::shared::{macros::states, types::ParaId};
/// HRMP channel configuration, with fine-grained configuration options. /// HRMP channel configuration, with fine-grained configuration options.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HrmpChannelConfig { pub struct HrmpChannelConfig {
sender: ParaId, sender: ParaId,
recipient: ParaId, recipient: ParaId,
max_capacity: u32, max_capacity: u32,
max_message_size: u32, max_message_size: u32,
} }
impl HrmpChannelConfig { impl HrmpChannelConfig {
/// The sending parachain ID. /// The sending parachain ID.
pub fn sender(&self) -> ParaId { pub fn sender(&self) -> ParaId {
self.sender self.sender
} }
/// The receiving parachain ID. /// The receiving parachain ID.
pub fn recipient(&self) -> ParaId { pub fn recipient(&self) -> ParaId {
self.recipient self.recipient
} }
/// The maximum capacity of messages in the channel. /// The maximum capacity of messages in the channel.
pub fn max_capacity(&self) -> u32 { pub fn max_capacity(&self) -> u32 {
self.max_capacity self.max_capacity
} }
/// The maximum size of a message in the channel. /// The maximum size of a message in the channel.
pub fn max_message_size(&self) -> u32 { pub fn max_message_size(&self) -> u32 {
self.max_message_size self.max_message_size
} }
} }
states! { states! {
Initial, Initial,
WithSender, WithSender,
WithRecipient WithRecipient
} }
/// HRMP channel configuration builder, used to build an [`HrmpChannelConfig`] declaratively with fields validation. /// HRMP channel configuration builder, used to build an [`HrmpChannelConfig`] declaratively with fields validation.
pub struct HrmpChannelConfigBuilder<State> { pub struct HrmpChannelConfigBuilder<State> {
config: HrmpChannelConfig, config: HrmpChannelConfig,
_state: PhantomData<State>, _state: PhantomData<State>,
} }
impl Default for HrmpChannelConfigBuilder<Initial> { impl Default for HrmpChannelConfigBuilder<Initial> {
fn default() -> Self { fn default() -> Self {
Self { Self {
config: HrmpChannelConfig { config: HrmpChannelConfig {
sender: 0, sender: 0,
recipient: 0, recipient: 0,
max_capacity: 8, max_capacity: 8,
max_message_size: 512, max_message_size: 512,
}, },
_state: PhantomData, _state: PhantomData,
} }
} }
} }
impl<A> HrmpChannelConfigBuilder<A> { impl<A> HrmpChannelConfigBuilder<A> {
fn transition<B>(&self, config: HrmpChannelConfig) -> HrmpChannelConfigBuilder<B> { fn transition<B>(&self, config: HrmpChannelConfig) -> HrmpChannelConfigBuilder<B> {
HrmpChannelConfigBuilder { HrmpChannelConfigBuilder { config, _state: PhantomData }
config, }
_state: PhantomData,
}
}
} }
impl HrmpChannelConfigBuilder<Initial> { impl HrmpChannelConfigBuilder<Initial> {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
/// Set the sending parachain ID. /// Set the sending parachain ID.
pub fn with_sender(self, sender: ParaId) -> HrmpChannelConfigBuilder<WithSender> { pub fn with_sender(self, sender: ParaId) -> HrmpChannelConfigBuilder<WithSender> {
self.transition(HrmpChannelConfig { self.transition(HrmpChannelConfig { sender, ..self.config })
sender, }
..self.config
})
}
} }
impl HrmpChannelConfigBuilder<WithSender> { impl HrmpChannelConfigBuilder<WithSender> {
/// Set the receiving parachain ID. /// Set the receiving parachain ID.
pub fn with_recipient(self, recipient: ParaId) -> HrmpChannelConfigBuilder<WithRecipient> { pub fn with_recipient(self, recipient: ParaId) -> HrmpChannelConfigBuilder<WithRecipient> {
self.transition(HrmpChannelConfig { self.transition(HrmpChannelConfig { recipient, ..self.config })
recipient, }
..self.config
})
}
} }
impl HrmpChannelConfigBuilder<WithRecipient> { impl HrmpChannelConfigBuilder<WithRecipient> {
/// Set the max capacity of messages in the channel. /// Set the max capacity of messages in the channel.
pub fn with_max_capacity(self, max_capacity: u32) -> Self { pub fn with_max_capacity(self, max_capacity: u32) -> Self {
self.transition(HrmpChannelConfig { self.transition(HrmpChannelConfig { max_capacity, ..self.config })
max_capacity, }
..self.config
})
}
/// Set the maximum size of a message in the channel. /// Set the maximum size of a message in the channel.
pub fn with_max_message_size(self, max_message_size: u32) -> Self { pub fn with_max_message_size(self, max_message_size: u32) -> Self {
self.transition(HrmpChannelConfig { self.transition(HrmpChannelConfig { max_message_size, ..self.config })
max_message_size, }
..self.config
})
}
pub fn build(self) -> HrmpChannelConfig { pub fn build(self) -> HrmpChannelConfig {
self.config self.config
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn hrmp_channel_config_builder_should_build_a_new_hrmp_channel_config_correctly() { fn hrmp_channel_config_builder_should_build_a_new_hrmp_channel_config_correctly() {
let hrmp_channel_config = HrmpChannelConfigBuilder::new() let hrmp_channel_config = HrmpChannelConfigBuilder::new()
.with_sender(1000) .with_sender(1000)
.with_recipient(2000) .with_recipient(2000)
.with_max_capacity(50) .with_max_capacity(50)
.with_max_message_size(100) .with_max_message_size(100)
.build(); .build();
assert_eq!(hrmp_channel_config.sender(), 1000); assert_eq!(hrmp_channel_config.sender(), 1000);
assert_eq!(hrmp_channel_config.recipient(), 2000); assert_eq!(hrmp_channel_config.recipient(), 2000);
assert_eq!(hrmp_channel_config.max_capacity(), 50); assert_eq!(hrmp_channel_config.max_capacity(), 50);
assert_eq!(hrmp_channel_config.max_message_size(), 100); assert_eq!(hrmp_channel_config.max_message_size(), 100);
} }
} }
@@ -93,7 +93,7 @@ pub use relaychain::{RelaychainConfig, RelaychainConfigBuilder};
// re-export shared // re-export shared
pub use shared::{node::NodeConfig, types}; pub use shared::{node::NodeConfig, types};
pub use teyrchain::{ pub use teyrchain::{
states as para_states, RegistrationStrategy, TeyrchainConfig, TeyrchainConfigBuilder, states as para_states, RegistrationStrategy, TeyrchainConfig, TeyrchainConfigBuilder,
}; };
// Backward compatibility aliases for external crates that use Polkadot SDK terminology // Backward compatibility aliases for external crates that use Polkadot SDK terminology
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,114 +3,114 @@ use super::types::{ParaId, Port};
/// An error at the configuration level. /// An error at the configuration level.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum ConfigError { pub enum ConfigError {
#[error("relaychain.{0}")] #[error("relaychain.{0}")]
Relaychain(anyhow::Error), Relaychain(anyhow::Error),
#[error("teyrchain[{0}].{1}")] #[error("teyrchain[{0}].{1}")]
Teyrchain(ParaId, anyhow::Error), Teyrchain(ParaId, anyhow::Error),
#[error("global_settings.{0}")] #[error("global_settings.{0}")]
GlobalSettings(anyhow::Error), GlobalSettings(anyhow::Error),
#[error("nodes['{0}'].{1}")] #[error("nodes['{0}'].{1}")]
Node(String, anyhow::Error), Node(String, anyhow::Error),
#[error("collators['{0}'].{1}")] #[error("collators['{0}'].{1}")]
Collator(String, anyhow::Error), Collator(String, anyhow::Error),
} }
/// An error at the field level. /// An error at the field level.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum FieldError { pub enum FieldError {
#[error("name: {0}")] #[error("name: {0}")]
Name(anyhow::Error), Name(anyhow::Error),
#[error("chain: {0}")] #[error("chain: {0}")]
Chain(anyhow::Error), Chain(anyhow::Error),
#[error("image: {0}")] #[error("image: {0}")]
Image(anyhow::Error), Image(anyhow::Error),
#[error("default_image: {0}")] #[error("default_image: {0}")]
DefaultImage(anyhow::Error), DefaultImage(anyhow::Error),
#[error("command: {0}")] #[error("command: {0}")]
Command(anyhow::Error), Command(anyhow::Error),
#[error("default_command: {0}")] #[error("default_command: {0}")]
DefaultCommand(anyhow::Error), DefaultCommand(anyhow::Error),
#[error("bootnodes_addresses[{0}]: '{1}' {2}")] #[error("bootnodes_addresses[{0}]: '{1}' {2}")]
BootnodesAddress(usize, String, anyhow::Error), BootnodesAddress(usize, String, anyhow::Error),
#[error("genesis_wasm_generator: {0}")] #[error("genesis_wasm_generator: {0}")]
GenesisWasmGenerator(anyhow::Error), GenesisWasmGenerator(anyhow::Error),
#[error("genesis_state_generator: {0}")] #[error("genesis_state_generator: {0}")]
GenesisStateGenerator(anyhow::Error), GenesisStateGenerator(anyhow::Error),
#[error("local_ip: {0}")] #[error("local_ip: {0}")]
LocalIp(anyhow::Error), LocalIp(anyhow::Error),
#[error("default_resources.{0}")] #[error("default_resources.{0}")]
DefaultResources(anyhow::Error), DefaultResources(anyhow::Error),
#[error("resources.{0}")] #[error("resources.{0}")]
Resources(anyhow::Error), Resources(anyhow::Error),
#[error("request_memory: {0}")] #[error("request_memory: {0}")]
RequestMemory(anyhow::Error), RequestMemory(anyhow::Error),
#[error("request_cpu: {0}")] #[error("request_cpu: {0}")]
RequestCpu(anyhow::Error), RequestCpu(anyhow::Error),
#[error("limit_memory: {0}")] #[error("limit_memory: {0}")]
LimitMemory(anyhow::Error), LimitMemory(anyhow::Error),
#[error("limit_cpu: {0}")] #[error("limit_cpu: {0}")]
LimitCpu(anyhow::Error), LimitCpu(anyhow::Error),
#[error("ws_port: {0}")] #[error("ws_port: {0}")]
WsPort(anyhow::Error), WsPort(anyhow::Error),
#[error("rpc_port: {0}")] #[error("rpc_port: {0}")]
RpcPort(anyhow::Error), RpcPort(anyhow::Error),
#[error("prometheus_port: {0}")] #[error("prometheus_port: {0}")]
PrometheusPort(anyhow::Error), PrometheusPort(anyhow::Error),
#[error("p2p_port: {0}")] #[error("p2p_port: {0}")]
P2pPort(anyhow::Error), P2pPort(anyhow::Error),
#[error("session_key: {0}")] #[error("session_key: {0}")]
SessionKey(anyhow::Error), SessionKey(anyhow::Error),
#[error("registration_strategy: {0}")] #[error("registration_strategy: {0}")]
RegistrationStrategy(anyhow::Error), RegistrationStrategy(anyhow::Error),
} }
/// A conversion error for shared types across fields. /// A conversion error for shared types across fields.
#[derive(thiserror::Error, Debug, Clone)] #[derive(thiserror::Error, Debug, Clone)]
pub enum ConversionError { pub enum ConversionError {
#[error("'{0}' shouldn't contains whitespace")] #[error("'{0}' shouldn't contains whitespace")]
ContainsWhitespaces(String), ContainsWhitespaces(String),
#[error("'{}' doesn't match regex '{}'", .value, .regex)] #[error("'{}' doesn't match regex '{}'", .value, .regex)]
DoesntMatchRegex { value: String, regex: String }, DoesntMatchRegex { value: String, regex: String },
#[error("can't be empty")] #[error("can't be empty")]
CantBeEmpty, CantBeEmpty,
#[error("deserialize error")] #[error("deserialize error")]
DeserializeError(String), DeserializeError(String),
} }
/// A validation error for shared types across fields. /// A validation error for shared types across fields.
#[derive(thiserror::Error, Debug, Clone)] #[derive(thiserror::Error, Debug, Clone)]
pub enum ValidationError { pub enum ValidationError {
#[error("'{0}' is already used across config")] #[error("'{0}' is already used across config")]
PortAlreadyUsed(Port), PortAlreadyUsed(Port),
#[error("can't be empty")] #[error("can't be empty")]
CantBeEmpty(), CantBeEmpty(),
} }
@@ -4,28 +4,28 @@ use support::constants::{BORROWABLE, THIS_IS_A_BUG};
use tracing::warn; use tracing::warn;
use super::{ use super::{
errors::ValidationError, errors::ValidationError,
types::{ParaId, Port, ValidationContext}, types::{ParaId, Port, ValidationContext},
}; };
pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> { pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> {
let mut errors = errors; let mut errors = errors;
errors.push(new_error); errors.push(new_error);
errors errors
} }
pub fn merge_errors_vecs( pub fn merge_errors_vecs(
errors: Vec<anyhow::Error>, errors: Vec<anyhow::Error>,
new_errors: Vec<anyhow::Error>, new_errors: Vec<anyhow::Error>,
) -> Vec<anyhow::Error> { ) -> Vec<anyhow::Error> {
let mut errors = errors; let mut errors = errors;
for new_error in new_errors.into_iter() { for new_error in new_errors.into_iter() {
errors.push(new_error); errors.push(new_error);
} }
errors errors
} }
/// Generates a unique name from a base name and the names already present in a /// Generates a unique name from a base name and the names already present in a
@@ -34,14 +34,13 @@ pub fn merge_errors_vecs(
/// Uses [`generate_unique_node_name_from_names()`] internally to ensure uniqueness. /// 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. /// Logs a warning if the generated name differs from the original due to duplicates.
pub fn generate_unique_node_name( pub fn generate_unique_node_name(
node_name: impl Into<String>, node_name: impl Into<String>,
validation_context: Rc<RefCell<ValidationContext>>, validation_context: Rc<RefCell<ValidationContext>>,
) -> String { ) -> String {
let mut context = validation_context let mut context =
.try_borrow_mut() validation_context.try_borrow_mut().expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
generate_unique_node_name_from_names(node_name, &mut context.used_nodes_names) generate_unique_node_name_from_names(node_name, &mut context.used_nodes_names)
} }
/// Returns `node_name` if it is not already in `names`. /// Returns `node_name` if it is not already in `names`.
@@ -49,70 +48,68 @@ pub fn generate_unique_node_name(
/// Otherwise, appends an incrementing `-{counter}` suffix until a unique name is found, /// Otherwise, appends an incrementing `-{counter}` suffix until a unique name is found,
/// then returns it. Logs a warning when a duplicate is detected. /// then returns it. Logs a warning when a duplicate is detected.
pub fn generate_unique_node_name_from_names( pub fn generate_unique_node_name_from_names(
node_name: impl Into<String>, node_name: impl Into<String>,
names: &mut HashSet<String>, names: &mut HashSet<String>,
) -> String { ) -> String {
let node_name = node_name.into(); let node_name = node_name.into();
if names.insert(node_name.clone()) { if names.insert(node_name.clone()) {
return node_name; return node_name;
} }
let mut counter = 1; let mut counter = 1;
let mut candidate = node_name.clone(); let mut candidate = node_name.clone();
while names.contains(&candidate) { while names.contains(&candidate) {
candidate = format!("{node_name}-{counter}"); candidate = format!("{node_name}-{counter}");
counter += 1; counter += 1;
} }
warn!( warn!(
original = %node_name, original = %node_name,
adjusted = %candidate, adjusted = %candidate,
"Duplicate node name detected." "Duplicate node name detected."
); );
names.insert(candidate.clone()); names.insert(candidate.clone());
candidate candidate
} }
pub fn ensure_value_is_not_empty(value: &str) -> Result<(), anyhow::Error> { pub fn ensure_value_is_not_empty(value: &str) -> Result<(), anyhow::Error> {
if value.is_empty() { if value.is_empty() {
Err(ValidationError::CantBeEmpty().into()) Err(ValidationError::CantBeEmpty().into())
} else { } else {
Ok(()) Ok(())
} }
} }
pub fn ensure_port_unique( pub fn ensure_port_unique(
port: Port, port: Port,
validation_context: Rc<RefCell<ValidationContext>>, validation_context: Rc<RefCell<ValidationContext>>,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let mut context = validation_context let mut context =
.try_borrow_mut() validation_context.try_borrow_mut().expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
if !context.used_ports.contains(&port) { if !context.used_ports.contains(&port) {
context.used_ports.push(port); context.used_ports.push(port);
return Ok(()); return Ok(());
} }
Err(ValidationError::PortAlreadyUsed(port).into()) Err(ValidationError::PortAlreadyUsed(port).into())
} }
pub fn generate_unique_para_id( pub fn generate_unique_para_id(
para_id: ParaId, para_id: ParaId,
validation_context: Rc<RefCell<ValidationContext>>, validation_context: Rc<RefCell<ValidationContext>>,
) -> String { ) -> String {
let mut context = validation_context let mut context =
.try_borrow_mut() validation_context.try_borrow_mut().expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
if let Some(suffix) = context.used_para_ids.get_mut(&para_id) { if let Some(suffix) = context.used_para_ids.get_mut(&para_id) {
*suffix += 1; *suffix += 1;
format!("{para_id}-{suffix}") format!("{para_id}-{suffix}")
} else { } else {
// insert 0, since will be used next time. // insert 0, since will be used next time.
context.used_para_ids.insert(para_id, 0); context.used_para_ids.insert(para_id, 0);
para_id.to_string() para_id.to_string()
} }
} }
File diff suppressed because it is too large Load Diff
@@ -3,15 +3,15 @@ use std::error::Error;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use serde::{ use serde::{
de::{self}, de::{self},
ser::SerializeStruct, ser::SerializeStruct,
Deserialize, Serialize, Deserialize, Serialize,
}; };
use support::constants::{SHOULD_COMPILE, THIS_IS_A_BUG}; use support::constants::{SHOULD_COMPILE, THIS_IS_A_BUG};
use super::{ use super::{
errors::{ConversionError, FieldError}, errors::{ConversionError, FieldError},
helpers::merge_errors, helpers::merge_errors,
}; };
/// A resource quantity used to define limits (k8s/podman only). /// A resource quantity used to define limits (k8s/podman only).
@@ -37,453 +37,434 @@ use super::{
pub struct ResourceQuantity(String); pub struct ResourceQuantity(String);
impl ResourceQuantity { impl ResourceQuantity {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.0 &self.0
} }
} }
impl TryFrom<&str> for ResourceQuantity { impl TryFrom<&str> for ResourceQuantity {
type Error = ConversionError; type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: &str) -> Result<Self, Self::Error> {
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new(r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$") 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}")); .expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
} }
if !RE.is_match(value) { if !RE.is_match(value) {
return Err(ConversionError::DoesntMatchRegex { return Err(ConversionError::DoesntMatchRegex {
value: value.to_string(), value: value.to_string(),
regex: r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$".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())) Ok(Self(value.to_string()))
} }
} }
impl From<u64> for ResourceQuantity { impl From<u64> for ResourceQuantity {
fn from(value: u64) -> Self { fn from(value: u64) -> Self {
Self(value.to_string()) Self(value.to_string())
} }
} }
/// Resources limits used in the context of podman/k8s. /// Resources limits used in the context of podman/k8s.
#[derive(Debug, Default, Clone, PartialEq)] #[derive(Debug, Default, Clone, PartialEq)]
pub struct Resources { pub struct Resources {
request_memory: Option<ResourceQuantity>, request_memory: Option<ResourceQuantity>,
request_cpu: Option<ResourceQuantity>, request_cpu: Option<ResourceQuantity>,
limit_memory: Option<ResourceQuantity>, limit_memory: Option<ResourceQuantity>,
limit_cpu: Option<ResourceQuantity>, limit_cpu: Option<ResourceQuantity>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct ResourcesField { struct ResourcesField {
memory: Option<ResourceQuantity>, memory: Option<ResourceQuantity>,
cpu: Option<ResourceQuantity>, cpu: Option<ResourceQuantity>,
} }
impl Serialize for Resources { impl Serialize for Resources {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
let mut state = serializer.serialize_struct("Resources", 2)?; let mut state = serializer.serialize_struct("Resources", 2)?;
if self.request_memory.is_some() || self.request_memory.is_some() { if self.request_memory.is_some() || self.request_memory.is_some() {
state.serialize_field( state.serialize_field(
"requests", "requests",
&ResourcesField { &ResourcesField {
memory: self.request_memory.clone(), memory: self.request_memory.clone(),
cpu: self.request_cpu.clone(), cpu: self.request_cpu.clone(),
}, },
)?; )?;
} else { } else {
state.skip_field("requests")?; state.skip_field("requests")?;
} }
if self.limit_memory.is_some() || self.limit_memory.is_some() { if self.limit_memory.is_some() || self.limit_memory.is_some() {
state.serialize_field( state.serialize_field(
"limits", "limits",
&ResourcesField { &ResourcesField { memory: self.limit_memory.clone(), cpu: self.limit_cpu.clone() },
memory: self.limit_memory.clone(), )?;
cpu: self.limit_cpu.clone(), } else {
}, state.skip_field("limits")?;
)?; }
} else {
state.skip_field("limits")?;
}
state.end() state.end()
} }
} }
struct ResourcesVisitor; struct ResourcesVisitor;
impl<'de> de::Visitor<'de> for ResourcesVisitor { impl<'de> de::Visitor<'de> for ResourcesVisitor {
type Value = Resources; type Value = Resources;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a resources object") formatter.write_str("a resources object")
} }
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where where
A: de::MapAccess<'de>, A: de::MapAccess<'de>,
{ {
let mut resources: Resources = Resources::default(); let mut resources: Resources = Resources::default();
while let Some((key, value)) = map.next_entry::<String, ResourcesField>()? { while let Some((key, value)) = map.next_entry::<String, ResourcesField>()? {
match key.as_str() { match key.as_str() {
"requests" => { "requests" => {
resources.request_memory = value.memory; resources.request_memory = value.memory;
resources.request_cpu = value.cpu; resources.request_cpu = value.cpu;
}, },
"limits" => { "limits" => {
resources.limit_memory = value.memory; resources.limit_memory = value.memory;
resources.limit_cpu = value.cpu; resources.limit_cpu = value.cpu;
}, },
_ => { _ => {
return Err(de::Error::unknown_field( return Err(de::Error::unknown_field(
&key, &key,
&["requests", "limits", "cpu", "memory"], &["requests", "limits", "cpu", "memory"],
)) ))
}, },
} }
} }
Ok(resources) Ok(resources)
} }
} }
impl<'de> Deserialize<'de> for Resources { impl<'de> Deserialize<'de> for Resources {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
deserializer.deserialize_any(ResourcesVisitor) deserializer.deserialize_any(ResourcesVisitor)
} }
} }
impl Resources { impl Resources {
/// Memory limit applied to requests. /// Memory limit applied to requests.
pub fn request_memory(&self) -> Option<&ResourceQuantity> { pub fn request_memory(&self) -> Option<&ResourceQuantity> {
self.request_memory.as_ref() self.request_memory.as_ref()
} }
/// CPU limit applied to requests. /// CPU limit applied to requests.
pub fn request_cpu(&self) -> Option<&ResourceQuantity> { pub fn request_cpu(&self) -> Option<&ResourceQuantity> {
self.request_cpu.as_ref() self.request_cpu.as_ref()
} }
/// Overall memory limit applied. /// Overall memory limit applied.
pub fn limit_memory(&self) -> Option<&ResourceQuantity> { pub fn limit_memory(&self) -> Option<&ResourceQuantity> {
self.limit_memory.as_ref() self.limit_memory.as_ref()
} }
/// Overall CPU limit applied. /// Overall CPU limit applied.
pub fn limit_cpu(&self) -> Option<&ResourceQuantity> { pub fn limit_cpu(&self) -> Option<&ResourceQuantity> {
self.limit_cpu.as_ref() self.limit_cpu.as_ref()
} }
} }
/// A resources builder, used to build a [`Resources`] declaratively with fields validation. /// A resources builder, used to build a [`Resources`] declaratively with fields validation.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ResourcesBuilder { pub struct ResourcesBuilder {
config: Resources, config: Resources,
errors: Vec<anyhow::Error>, errors: Vec<anyhow::Error>,
} }
impl ResourcesBuilder { impl ResourcesBuilder {
pub fn new() -> ResourcesBuilder { pub fn new() -> ResourcesBuilder {
Self::default() Self::default()
} }
fn transition(config: Resources, errors: Vec<anyhow::Error>) -> Self { fn transition(config: Resources, errors: Vec<anyhow::Error>) -> Self {
Self { config, errors } Self { config, errors }
} }
/// Set the requested memory for a pod. This is the minimum memory allocated for a pod. /// 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 pub fn with_request_memory<T>(self, quantity: T) -> Self
where where
T: TryInto<ResourceQuantity>, T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static, T::Error: Error + Send + Sync + 'static,
{ {
match quantity.try_into() { match quantity.try_into() {
Ok(quantity) => Self::transition( Ok(quantity) => Self::transition(
Resources { Resources { request_memory: Some(quantity), ..self.config },
request_memory: Some(quantity), self.errors,
..self.config ),
}, Err(error) => Self::transition(
self.errors, self.config,
), merge_errors(self.errors, FieldError::RequestMemory(error.into()).into()),
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. /// 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 pub fn with_request_cpu<T>(self, quantity: T) -> Self
where where
T: TryInto<ResourceQuantity>, T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static, T::Error: Error + Send + Sync + 'static,
{ {
match quantity.try_into() { match quantity.try_into() {
Ok(quantity) => Self::transition( Ok(quantity) => Self::transition(
Resources { Resources { request_cpu: Some(quantity), ..self.config },
request_cpu: Some(quantity), self.errors,
..self.config ),
}, Err(error) => Self::transition(
self.errors, self.config,
), merge_errors(self.errors, FieldError::RequestCpu(error.into()).into()),
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. /// 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 pub fn with_limit_memory<T>(self, quantity: T) -> Self
where where
T: TryInto<ResourceQuantity>, T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static, T::Error: Error + Send + Sync + 'static,
{ {
match quantity.try_into() { match quantity.try_into() {
Ok(quantity) => Self::transition( Ok(quantity) => Self::transition(
Resources { Resources { limit_memory: Some(quantity), ..self.config },
limit_memory: Some(quantity), self.errors,
..self.config ),
}, Err(error) => Self::transition(
self.errors, self.config,
), merge_errors(self.errors, FieldError::LimitMemory(error.into()).into()),
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. /// 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 pub fn with_limit_cpu<T>(self, quantity: T) -> Self
where where
T: TryInto<ResourceQuantity>, T: TryInto<ResourceQuantity>,
T::Error: Error + Send + Sync + 'static, T::Error: Error + Send + Sync + 'static,
{ {
match quantity.try_into() { match quantity.try_into() {
Ok(quantity) => Self::transition( Ok(quantity) => Self::transition(
Resources { Resources { limit_cpu: Some(quantity), ..self.config },
limit_cpu: Some(quantity), self.errors,
..self.config ),
}, Err(error) => Self::transition(
self.errors, self.config,
), merge_errors(self.errors, FieldError::LimitCpu(error.into()).into()),
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. /// 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>> { pub fn build(self) -> Result<Resources, Vec<anyhow::Error>> {
if !self.errors.is_empty() { if !self.errors.is_empty() {
return Err(self.errors); return Err(self.errors);
} }
Ok(self.config) Ok(self.config)
} }
} }
#[cfg(test)] #[cfg(test)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
mod tests { mod tests {
use super::*; use super::*;
use crate::NetworkConfig; use crate::NetworkConfig;
macro_rules! impl_resources_quantity_unit_test { macro_rules! impl_resources_quantity_unit_test {
($val:literal) => {{ ($val:literal) => {{
let resources = ResourcesBuilder::new() let resources = ResourcesBuilder::new().with_request_memory($val).build().unwrap();
.with_request_memory($val)
.build()
.unwrap();
assert_eq!(resources.request_memory().unwrap().as_str(), $val); assert_eq!(resources.request_memory().unwrap().as_str(), $val);
assert_eq!(resources.request_cpu(), None); assert_eq!(resources.request_cpu(), None);
assert_eq!(resources.limit_cpu(), None); assert_eq!(resources.limit_cpu(), None);
assert_eq!(resources.limit_memory(), None); assert_eq!(resources.limit_memory(), None);
}}; }};
} }
#[test] #[test]
fn converting_a_string_a_resource_quantity_without_unit_should_succeeds() { fn converting_a_string_a_resource_quantity_without_unit_should_succeeds() {
impl_resources_quantity_unit_test!("1000"); impl_resources_quantity_unit_test!("1000");
} }
#[test] #[test]
fn converting_a_str_with_m_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_m_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("100m"); impl_resources_quantity_unit_test!("100m");
} }
#[test] #[test]
fn converting_a_str_with_K_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_K_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("50K"); impl_resources_quantity_unit_test!("50K");
} }
#[test] #[test]
fn converting_a_str_with_M_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_M_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("100M"); impl_resources_quantity_unit_test!("100M");
} }
#[test] #[test]
fn converting_a_str_with_G_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_G_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("1G"); impl_resources_quantity_unit_test!("1G");
} }
#[test] #[test]
fn converting_a_str_with_T_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_T_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.01T"); impl_resources_quantity_unit_test!("0.01T");
} }
#[test] #[test]
fn converting_a_str_with_P_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_P_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.00001P"); impl_resources_quantity_unit_test!("0.00001P");
} }
#[test] #[test]
fn converting_a_str_with_E_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_E_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.000000001E"); impl_resources_quantity_unit_test!("0.000000001E");
} }
#[test] #[test]
fn converting_a_str_with_Ki_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_Ki_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("50Ki"); impl_resources_quantity_unit_test!("50Ki");
} }
#[test] #[test]
fn converting_a_str_with_Mi_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_Mi_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("100Mi"); impl_resources_quantity_unit_test!("100Mi");
} }
#[test] #[test]
fn converting_a_str_with_Gi_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_Gi_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("1Gi"); impl_resources_quantity_unit_test!("1Gi");
} }
#[test] #[test]
fn converting_a_str_with_Ti_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_Ti_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.01Ti"); impl_resources_quantity_unit_test!("0.01Ti");
} }
#[test] #[test]
fn converting_a_str_with_Pi_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_Pi_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.00001Pi"); impl_resources_quantity_unit_test!("0.00001Pi");
} }
#[test] #[test]
fn converting_a_str_with_Ei_unit_into_a_resource_quantity_should_succeeds() { fn converting_a_str_with_Ei_unit_into_a_resource_quantity_should_succeeds() {
impl_resources_quantity_unit_test!("0.000000001Ei"); impl_resources_quantity_unit_test!("0.000000001Ei");
} }
#[test] #[test]
fn resources_config_builder_should_succeeds_and_returns_a_resources_config() { fn resources_config_builder_should_succeeds_and_returns_a_resources_config() {
let resources = ResourcesBuilder::new() let resources = ResourcesBuilder::new()
.with_request_memory("200M") .with_request_memory("200M")
.with_request_cpu("1G") .with_request_cpu("1G")
.with_limit_cpu("500M") .with_limit_cpu("500M")
.with_limit_memory("2G") .with_limit_memory("2G")
.build() .build()
.unwrap(); .unwrap();
assert_eq!(resources.request_memory().unwrap().as_str(), "200M"); assert_eq!(resources.request_memory().unwrap().as_str(), "200M");
assert_eq!(resources.request_cpu().unwrap().as_str(), "1G"); assert_eq!(resources.request_cpu().unwrap().as_str(), "1G");
assert_eq!(resources.limit_cpu().unwrap().as_str(), "500M"); assert_eq!(resources.limit_cpu().unwrap().as_str(), "500M");
assert_eq!(resources.limit_memory().unwrap().as_str(), "2G"); assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
} }
#[test] #[test]
fn resources_config_toml_import_should_succeeds_and_returns_a_resources_config() { fn resources_config_toml_import_should_succeeds_and_returns_a_resources_config() {
let load_from_toml = let load_from_toml =
NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap(); NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
let resources = load_from_toml.relaychain().default_resources().unwrap(); let resources = load_from_toml.relaychain().default_resources().unwrap();
assert_eq!(resources.request_memory().unwrap().as_str(), "500M"); assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
assert_eq!(resources.request_cpu().unwrap().as_str(), "100000"); assert_eq!(resources.request_cpu().unwrap().as_str(), "100000");
assert_eq!(resources.limit_cpu().unwrap().as_str(), "10Gi"); assert_eq!(resources.limit_cpu().unwrap().as_str(), "10Gi");
assert_eq!(resources.limit_memory().unwrap().as_str(), "4000M"); assert_eq!(resources.limit_memory().unwrap().as_str(), "4000M");
} }
#[test] #[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_memory() 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 resources_builder = ResourcesBuilder::new().with_request_memory("invalid");
let errors = resources_builder.build().err().unwrap(); let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), 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)?$'" r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
); );
} }
#[test] #[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_cpu() { 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 resources_builder = ResourcesBuilder::new().with_request_cpu("invalid");
let errors = resources_builder.build().err().unwrap(); let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), 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)?$'" r"request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
); );
} }
#[test] #[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_memory() { 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 resources_builder = ResourcesBuilder::new().with_limit_memory("invalid");
let errors = resources_builder.build().err().unwrap(); let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), 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)?$'" r"limit_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
); );
} }
#[test] #[test]
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_cpu() { 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 resources_builder = ResourcesBuilder::new().with_limit_cpu("invalid");
let errors = resources_builder.build().err().unwrap(); let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), 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)?$'" r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
); );
} }
#[test] #[test]
fn resources_config_builder_should_fails_and_returns_multiple_error_if_couldnt_parse_multiple_fields( fn resources_config_builder_should_fails_and_returns_multiple_error_if_couldnt_parse_multiple_fields(
) { ) {
let resources_builder = ResourcesBuilder::new() let resources_builder =
.with_limit_cpu("invalid") ResourcesBuilder::new().with_limit_cpu("invalid").with_request_memory("invalid");
.with_request_memory("invalid");
let errors = resources_builder.build().err().unwrap(); let errors = resources_builder.build().err().unwrap();
assert_eq!(errors.len(), 2); assert_eq!(errors.len(), 2);
assert_eq!( assert_eq!(
errors.first().unwrap().to_string(), 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)?$'" 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!( assert_eq!(
errors.get(1).unwrap().to_string(), 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)?$'" r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
); );
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,61 +5,61 @@ use support::constants::ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS;
use crate::types::{Chain, Command, Duration}; use crate::types::{Chain, Command, Duration};
pub(crate) fn is_true(value: &bool) -> bool { pub(crate) fn is_true(value: &bool) -> bool {
*value *value
} }
pub(crate) fn is_false(value: &bool) -> bool { pub(crate) fn is_false(value: &bool) -> bool {
!(*value) !(*value)
} }
pub(crate) fn default_as_true() -> bool { pub(crate) fn default_as_true() -> bool {
true true
} }
pub(crate) fn default_as_false() -> bool { pub(crate) fn default_as_false() -> bool {
false false
} }
pub(crate) fn default_initial_balance() -> crate::types::U128 { pub(crate) fn default_initial_balance() -> crate::types::U128 {
2_000_000_000_000.into() 2_000_000_000_000.into()
} }
/// Default timeout for spawning a node (10mins) /// Default timeout for spawning a node (10mins)
pub(crate) fn default_node_spawn_timeout() -> Duration { pub(crate) fn default_node_spawn_timeout() -> Duration {
env::var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS) env::var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS)
.ok() .ok()
.and_then(|s| s.parse::<u32>().ok()) .and_then(|s| s.parse::<u32>().ok())
.unwrap_or(600) .unwrap_or(600)
} }
/// Default timeout for spawning the whole network (1hr) /// Default timeout for spawning the whole network (1hr)
pub(crate) fn default_timeout() -> Duration { pub(crate) fn default_timeout() -> Duration {
3600 3600
} }
pub(crate) fn default_command_polkadot() -> Option<Command> { pub(crate) fn default_command_polkadot() -> Option<Command> {
TryInto::<Command>::try_into("polkadot").ok() TryInto::<Command>::try_into("polkadot").ok()
} }
pub(crate) fn default_relaychain_chain() -> Chain { pub(crate) fn default_relaychain_chain() -> Chain {
TryInto::<Chain>::try_into("rococo-local").expect("'rococo-local' should be a valid chain") TryInto::<Chain>::try_into("rococo-local").expect("'rococo-local' should be a valid chain")
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn default_node_spawn_timeout_works_before_and_after_env_is_set() { 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 // The default should be 600 seconds if the env var is not set
assert_eq!(default_node_spawn_timeout(), 600); assert_eq!(default_node_spawn_timeout(), 600);
// If env var is set to a valid number, it should return that number // If env var is set to a valid number, it should return that number
env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "123"); env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "123");
assert_eq!(default_node_spawn_timeout(), 123); assert_eq!(default_node_spawn_timeout(), 123);
// If env var is set to a NOT valid number, it should return 600 // 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"); env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "NOT_A_NUMBER");
assert_eq!(default_node_spawn_timeout(), 600); assert_eq!(default_node_spawn_timeout(), 600);
} }
} }
@@ -7,25 +7,25 @@ use crate::generators;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum OrchestratorError { pub enum OrchestratorError {
// TODO: improve invalid config reporting // TODO: improve invalid config reporting
#[error("Invalid network configuration: {0}")] #[error("Invalid network configuration: {0}")]
InvalidConfig(String), InvalidConfig(String),
#[error("Invalid network config to use provider {0}: {1}")] #[error("Invalid network config to use provider {0}: {1}")]
InvalidConfigForProvider(String, String), InvalidConfigForProvider(String, String),
#[error("Invalid configuration for node: {0}, field: {1}")] #[error("Invalid configuration for node: {0}, field: {1}")]
InvalidNodeConfig(String, String), InvalidNodeConfig(String, String),
#[error("Invariant not fulfilled {0}")] #[error("Invariant not fulfilled {0}")]
InvariantError(&'static str), InvariantError(&'static str),
#[error("Global network spawn timeout: {0} secs")] #[error("Global network spawn timeout: {0} secs")]
GlobalTimeOut(u32), GlobalTimeOut(u32),
#[error("Generator error: {0}")] #[error("Generator error: {0}")]
GeneratorError(#[from] generators::errors::GeneratorError), GeneratorError(#[from] generators::errors::GeneratorError),
#[error("Provider error")] #[error("Provider error")]
ProviderError(#[from] ProviderError), ProviderError(#[from] ProviderError),
#[error("FileSystem error")] #[error("FileSystem error")]
FileSystemError(#[from] FileSystemError), FileSystemError(#[from] FileSystemError),
#[error("Serialization error")] #[error("Serialization error")]
SerializationError(#[from] serde_json::Error), SerializationError(#[from] serde_json::Error),
#[error(transparent)] #[error(transparent)]
SpawnerError(#[from] anyhow::Error), SpawnerError(#[from] anyhow::Error),
} }
@@ -13,8 +13,8 @@ mod port;
pub use bootnode_addr::generate as generate_node_bootnode_addr; pub use bootnode_addr::generate as generate_node_bootnode_addr;
pub use command::{ pub use command::{
generate_for_cumulus_node as generate_node_command_cumulus, generate_for_cumulus_node as generate_node_command_cumulus,
generate_for_node as generate_node_command, GenCmdOptions, generate_for_node as generate_node_command, GenCmdOptions,
}; };
pub use identity::generate as generate_node_identity; pub use identity::generate as generate_node_identity;
pub use key::generate as generate_node_keys; pub use key::generate as generate_node_keys;
@@ -8,21 +8,21 @@ use configuration::types::Arg;
/// - `-:insecure-validator` -> removes `--insecure-validator` (normalized) /// - `-:insecure-validator` -> removes `--insecure-validator` (normalized)
/// - `-:--prometheus-port` -> removes `--prometheus-port` /// - `-:--prometheus-port` -> removes `--prometheus-port`
pub fn parse_removal_args(args: &[Arg]) -> Vec<String> { pub fn parse_removal_args(args: &[Arg]) -> Vec<String> {
args.iter() args.iter()
.filter_map(|arg| match arg { .filter_map(|arg| match arg {
Arg::Flag(flag) if flag.starts_with("-:") => { Arg::Flag(flag) if flag.starts_with("-:") => {
let mut flag_to_exclude = flag[2..].to_string(); let mut flag_to_exclude = flag[2..].to_string();
// Normalize flag format - ensure it starts with -- // Normalize flag format - ensure it starts with --
if !flag_to_exclude.starts_with("--") { if !flag_to_exclude.starts_with("--") {
flag_to_exclude = format!("--{flag_to_exclude}"); flag_to_exclude = format!("--{flag_to_exclude}");
} }
Some(flag_to_exclude) Some(flag_to_exclude)
}, },
_ => None, _ => None,
}) })
.collect() .collect()
} }
/// Apply arg removals to a vector of string arguments. /// Apply arg removals to a vector of string arguments.
@@ -35,104 +35,104 @@ pub fn parse_removal_args(args: &[Arg]) -> Vec<String> {
/// # Returns /// # Returns
/// Filtered vector with specified args removed /// Filtered vector with specified args removed
pub fn apply_arg_removals(args: Vec<String>, removals: &[String]) -> Vec<String> { pub fn apply_arg_removals(args: Vec<String>, removals: &[String]) -> Vec<String> {
if removals.is_empty() { if removals.is_empty() {
return args; return args;
} }
let mut res = Vec::new(); let mut res = Vec::new();
let mut skip_next = false; let mut skip_next = false;
for (i, arg) in args.iter().enumerate() { for (i, arg) in args.iter().enumerate() {
if skip_next { if skip_next {
skip_next = false; skip_next = false;
continue; continue;
} }
let should_remove = removals let should_remove = removals
.iter() .iter()
.any(|removal| arg == removal || arg.starts_with(&format!("{removal}="))); .any(|removal| arg == removal || arg.starts_with(&format!("{removal}=")));
if should_remove { if should_remove {
// Only skip next if this looks like an option (starts with --) and next arg doesn't start with -- // Only skip next if this looks like an option (starts with --) and next arg doesn't start with --
if !arg.contains("=") && i + 1 < args.len() { if !arg.contains("=") && i + 1 < args.len() {
let next_arg = &args[i + 1]; let next_arg = &args[i + 1];
if !next_arg.starts_with("-") { if !next_arg.starts_with("-") {
skip_next = true; skip_next = true;
} }
} }
continue; continue;
} }
res.push(arg.clone()); res.push(arg.clone());
} }
res res
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_parse_removal_args() { fn test_parse_removal_args() {
let args = vec![ let args = vec![
Arg::Flag("-:--insecure-validator-i-know-what-i-do".to_string()), Arg::Flag("-:--insecure-validator-i-know-what-i-do".to_string()),
Arg::Flag("--validator".to_string()), Arg::Flag("--validator".to_string()),
Arg::Flag("-:--no-telemetry".to_string()), Arg::Flag("-:--no-telemetry".to_string()),
]; ];
let removals = parse_removal_args(&args); let removals = parse_removal_args(&args);
assert_eq!(removals.len(), 2); assert_eq!(removals.len(), 2);
assert!(removals.contains(&"--insecure-validator-i-know-what-i-do".to_string())); assert!(removals.contains(&"--insecure-validator-i-know-what-i-do".to_string()));
assert!(removals.contains(&"--no-telemetry".to_string())); assert!(removals.contains(&"--no-telemetry".to_string()));
} }
#[test] #[test]
fn test_apply_arg_removals_flag() { fn test_apply_arg_removals_flag() {
let args = vec![ let args = vec![
"--validator".to_string(), "--validator".to_string(),
"--insecure-validator-i-know-what-i-do".to_string(), "--insecure-validator-i-know-what-i-do".to_string(),
"--no-telemetry".to_string(), "--no-telemetry".to_string(),
]; ];
let removals = vec!["--insecure-validator-i-know-what-i-do".to_string()]; let removals = vec!["--insecure-validator-i-know-what-i-do".to_string()];
let res = apply_arg_removals(args, &removals); let res = apply_arg_removals(args, &removals);
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
assert!(res.contains(&"--validator".to_string())); assert!(res.contains(&"--validator".to_string()));
assert!(res.contains(&"--no-telemetry".to_string())); assert!(res.contains(&"--no-telemetry".to_string()));
assert!(!res.contains(&"--insecure-validator-i-know-what-i-do".to_string())); assert!(!res.contains(&"--insecure-validator-i-know-what-i-do".to_string()));
} }
#[test] #[test]
fn test_apply_arg_removals_option_with_equals() { fn test_apply_arg_removals_option_with_equals() {
let args = vec!["--name=alice".to_string(), "--port=30333".to_string()]; let args = vec!["--name=alice".to_string(), "--port=30333".to_string()];
let removals = vec!["--port".to_string()]; let removals = vec!["--port".to_string()];
let res = apply_arg_removals(args, &removals); let res = apply_arg_removals(args, &removals);
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
assert_eq!(res[0], "--name=alice"); assert_eq!(res[0], "--name=alice");
} }
#[test] #[test]
fn test_apply_arg_removals_option_with_space() { fn test_apply_arg_removals_option_with_space() {
let args = vec![ let args = vec![
"--name".to_string(), "--name".to_string(),
"alice".to_string(), "alice".to_string(),
"--port".to_string(), "--port".to_string(),
"30333".to_string(), "30333".to_string(),
]; ];
let removals = vec!["--port".to_string()]; let removals = vec!["--port".to_string()];
let res = apply_arg_removals(args, &removals); let res = apply_arg_removals(args, &removals);
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
assert_eq!(res[0], "--name"); assert_eq!(res[0], "--name");
assert_eq!(res[1], "alice"); assert_eq!(res[1], "alice");
} }
#[test] #[test]
fn test_apply_arg_removals_empty() { fn test_apply_arg_removals_empty() {
let args = vec!["--validator".to_string()]; let args = vec!["--validator".to_string()];
let removals = vec![]; let removals = vec![];
let res = apply_arg_removals(args, &removals); let res = apply_arg_removals(args, &removals);
assert_eq!(res, vec!["--validator".to_string()]); assert_eq!(res, vec!["--validator".to_string()]);
} }
} }
@@ -3,109 +3,94 @@ use std::{fmt::Display, net::IpAddr};
use super::errors::GeneratorError; use super::errors::GeneratorError;
pub fn generate<T: AsRef<str> + Display>( pub fn generate<T: AsRef<str> + Display>(
peer_id: &str, peer_id: &str,
ip: &IpAddr, ip: &IpAddr,
port: u16, port: u16,
args: &[T], args: &[T],
p2p_cert: &Option<String>, p2p_cert: &Option<String>,
) -> Result<String, GeneratorError> { ) -> Result<String, GeneratorError> {
let addr = if let Some(index) = args.iter().position(|arg| arg.as_ref().eq("--listen-addr")) { let addr = if let Some(index) = args.iter().position(|arg| arg.as_ref().eq("--listen-addr")) {
let listen_value = args let listen_value = args
.as_ref() .as_ref()
.get(index + 1) .get(index + 1)
.ok_or(GeneratorError::BootnodeAddrGeneration( .ok_or(GeneratorError::BootnodeAddrGeneration(
"can not generate bootnode address from args".into(), "can not generate bootnode address from args".into(),
))? ))?
.to_string(); .to_string();
let ip_str = ip.to_string(); let ip_str = ip.to_string();
let port_str = port.to_string(); let port_str = port.to_string();
let mut parts = listen_value.split('/').collect::<Vec<&str>>(); let mut parts = listen_value.split('/').collect::<Vec<&str>>();
parts[2] = &ip_str; parts[2] = &ip_str;
parts[4] = port_str.as_str(); parts[4] = port_str.as_str();
parts.join("/") parts.join("/")
} else { } else {
format!("/ip4/{ip}/tcp/{port}/ws") format!("/ip4/{ip}/tcp/{port}/ws")
}; };
let mut addr_with_peer = format!("{addr}/p2p/{peer_id}"); let mut addr_with_peer = format!("{addr}/p2p/{peer_id}");
if let Some(p2p_cert) = p2p_cert { if let Some(p2p_cert) = p2p_cert {
addr_with_peer.push_str("/certhash/"); addr_with_peer.push_str("/certhash/");
addr_with_peer.push_str(p2p_cert) addr_with_peer.push_str(p2p_cert)
} }
Ok(addr_with_peer) Ok(addr_with_peer)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use provider::constants::LOCALHOST; use provider::constants::LOCALHOST;
use super::*; use super::*;
#[test] #[test]
fn generate_for_alice_without_args() { fn generate_for_alice_without_args() {
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<&str> = vec![]; let args: Vec<&str> = vec![];
let bootnode_addr = generate(peer_id, &LOCALHOST, 5678, &args, &None).unwrap(); let bootnode_addr = generate(peer_id, &LOCALHOST, 5678, &args, &None).unwrap();
assert_eq!( assert_eq!(
&bootnode_addr, &bootnode_addr,
"/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm" "/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
); );
} }
#[test] #[test]
fn generate_for_alice_with_listen_addr() { fn generate_for_alice_with_listen_addr() {
// Should override the ip/port // Should override the ip/port
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<String> = [ let args: Vec<String> =
"--some", ["--some", "other", "--listen-addr", "/ip4/192.168.100.1/tcp/30333/ws"]
"other", .iter()
"--listen-addr", .map(|x| x.to_string())
"/ip4/192.168.100.1/tcp/30333/ws", .collect();
] let bootnode_addr =
.iter() generate(peer_id, &LOCALHOST, 5678, args.iter().as_ref(), &None).unwrap();
.map(|x| x.to_string()) assert_eq!(
.collect(); &bootnode_addr,
let bootnode_addr = "/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
generate(peer_id, &LOCALHOST, 5678, args.iter().as_ref(), &None).unwrap(); );
assert_eq!( }
&bootnode_addr,
"/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
);
}
#[test] #[test]
fn generate_for_alice_with_listen_addr_without_value_must_fail() { fn generate_for_alice_with_listen_addr_without_value_must_fail() {
// Should override the ip/port // Should override the ip/port
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<String> = ["--some", "other", "--listen-addr"] let args: Vec<String> =
.iter() ["--some", "other", "--listen-addr"].iter().map(|x| x.to_string()).collect();
.map(|x| x.to_string()) let bootnode_addr = generate(peer_id, &LOCALHOST, 5678, args.iter().as_ref(), &None);
.collect();
let bootnode_addr = generate(peer_id, &LOCALHOST, 5678, args.iter().as_ref(), &None);
assert!(bootnode_addr.is_err()); assert!(bootnode_addr.is_err());
assert!(matches!( assert!(matches!(bootnode_addr, Err(GeneratorError::BootnodeAddrGeneration(_))));
bootnode_addr, }
Err(GeneratorError::BootnodeAddrGeneration(_))
));
}
#[test] #[test]
fn generate_for_alice_withcert() { fn generate_for_alice_withcert() {
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<&str> = vec![]; let args: Vec<&str> = vec![];
let bootnode_addr = generate( let bootnode_addr =
peer_id, generate(peer_id, &LOCALHOST, 5678, &args, &Some(String::from("data"))).unwrap();
&LOCALHOST, assert_eq!(
5678,
&args,
&Some(String::from("data")),
)
.unwrap();
assert_eq!(
&bootnode_addr, &bootnode_addr,
"/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm/certhash/data" "/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm/certhash/data"
); );
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,22 +3,22 @@ use support::fs::FileSystemError;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum GeneratorError { pub enum GeneratorError {
#[error("Generating key {0} with input {1}")] #[error("Generating key {0} with input {1}")]
KeyGeneration(String, String), KeyGeneration(String, String),
#[error("Generating port {0}, err {1}")] #[error("Generating port {0}, err {1}")]
PortGeneration(u16, String), PortGeneration(u16, String),
#[error("Chain-spec build error: {0}")] #[error("Chain-spec build error: {0}")]
ChainSpecGeneration(String), ChainSpecGeneration(String),
#[error("Provider error: {0}")] #[error("Provider error: {0}")]
ProviderError(#[from] ProviderError), ProviderError(#[from] ProviderError),
#[error("FileSystem error")] #[error("FileSystem error")]
FileSystemError(#[from] FileSystemError), FileSystemError(#[from] FileSystemError),
#[error("Generating identity, err {0}")] #[error("Generating identity, err {0}")]
IdentityGeneration(String), IdentityGeneration(String),
#[error("Generating bootnode address, err {0}")] #[error("Generating bootnode address, err {0}")]
BootnodeAddrGeneration(String), BootnodeAddrGeneration(String),
#[error("Error overriding wasm on raw chain-spec, err {0}")] #[error("Error overriding wasm on raw chain-spec, err {0}")]
OverridingWasm(String), OverridingWasm(String),
#[error("Error overriding raw chain-spec, err {0}")] #[error("Error overriding raw chain-spec, err {0}")]
OverridingRawSpec(String), OverridingRawSpec(String),
} }
@@ -7,35 +7,29 @@ use super::errors::GeneratorError;
// Generate p2p identity for node // Generate p2p identity for node
// return `node-key` and `peerId` // return `node-key` and `peerId`
pub fn generate(node_name: &str) -> Result<(String, String), GeneratorError> { pub fn generate(node_name: &str) -> Result<(String, String), GeneratorError> {
let key = hex::encode(sha2::Sha256::digest(node_name)); let key = hex::encode(sha2::Sha256::digest(node_name));
let bytes = <[u8; 32]>::from_hex(key.clone()).map_err(|_| { let bytes = <[u8; 32]>::from_hex(key.clone()).map_err(|_| {
GeneratorError::IdentityGeneration("can not transform hex to [u8;32]".into()) GeneratorError::IdentityGeneration("can not transform hex to [u8;32]".into())
})?; })?;
let sk = ed25519::SecretKey::try_from_bytes(bytes) let sk = ed25519::SecretKey::try_from_bytes(bytes)
.map_err(|_| GeneratorError::IdentityGeneration("can not create sk from bytes".into()))?; .map_err(|_| GeneratorError::IdentityGeneration("can not create sk from bytes".into()))?;
let local_identity: Keypair = ed25519::Keypair::from(sk).into(); let local_identity: Keypair = ed25519::Keypair::from(sk).into();
let local_public = local_identity.public(); let local_public = local_identity.public();
let local_peer_id = local_public.to_peer_id(); let local_peer_id = local_public.to_peer_id();
Ok((key, local_peer_id.to_base58())) Ok((key, local_peer_id.to_base58()))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn generate_for_alice() { fn generate_for_alice() {
let s = "alice"; let s = "alice";
let (key, peer_id) = generate(s).unwrap(); let (key, peer_id) = generate(s).unwrap();
assert_eq!( assert_eq!(&key, "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90");
&key, assert_eq!(&peer_id, "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm");
"2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90" }
);
assert_eq!(
&peer_id,
"12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
);
}
} }
@@ -1,151 +1,141 @@
use pezsp_core::{crypto::SecretStringError, ecdsa, ed25519, keccak_256, sr25519, Pair, H160, H256}; use pezsp_core::{
crypto::SecretStringError, ecdsa, ed25519, keccak_256, sr25519, Pair, H160, H256,
};
use super::errors::GeneratorError; use super::errors::GeneratorError;
use crate::shared::types::{Accounts, NodeAccount}; use crate::shared::types::{Accounts, NodeAccount};
const KEYS: [&str; 5] = ["sr", "sr_stash", "ed", "ec", "eth"]; const KEYS: [&str; 5] = ["sr", "sr_stash", "ed", "ec", "eth"];
pub fn generate_pair<T: Pair>(seed: &str) -> Result<T::Pair, SecretStringError> { pub fn generate_pair<T: Pair>(seed: &str) -> Result<T::Pair, SecretStringError> {
let pair = T::Pair::from_string(seed, None)?; let pair = T::Pair::from_string(seed, None)?;
Ok(pair) Ok(pair)
} }
pub fn generate(seed: &str) -> Result<Accounts, GeneratorError> { pub fn generate(seed: &str) -> Result<Accounts, GeneratorError> {
let mut accounts: Accounts = Default::default(); let mut accounts: Accounts = Default::default();
for k in KEYS { for k in KEYS {
let (address, public_key) = match k { let (address, public_key) = match k {
"sr" => { "sr" => {
let pair = generate_pair::<sr25519::Pair>(seed) let pair = generate_pair::<sr25519::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?; .map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public())) (pair.public().to_string(), hex::encode(pair.public()))
}, },
"sr_stash" => { "sr_stash" => {
let pair = generate_pair::<sr25519::Pair>(&format!("{seed}//stash")) let pair = generate_pair::<sr25519::Pair>(&format!("{seed}//stash"))
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?; .map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public())) (pair.public().to_string(), hex::encode(pair.public()))
}, },
"ed" => { "ed" => {
let pair = generate_pair::<ed25519::Pair>(seed) let pair = generate_pair::<ed25519::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?; .map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public())) (pair.public().to_string(), hex::encode(pair.public()))
}, },
"ec" => { "ec" => {
let pair = generate_pair::<ecdsa::Pair>(seed) let pair = generate_pair::<ecdsa::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?; .map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public())) (pair.public().to_string(), hex::encode(pair.public()))
}, },
"eth" => { "eth" => {
let pair = generate_pair::<ecdsa::Pair>(seed) let pair = generate_pair::<ecdsa::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?; .map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
let decompressed = libsecp256k1::PublicKey::parse_compressed(&pair.public().0) let decompressed = libsecp256k1::PublicKey::parse_compressed(&pair.public().0)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))? .map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?
.serialize(); .serialize();
let mut m = [0u8; 64]; let mut m = [0u8; 64];
m.copy_from_slice(&decompressed[1..65]); m.copy_from_slice(&decompressed[1..65]);
let account = H160::from(H256::from(keccak_256(&m))); let account = H160::from(H256::from(keccak_256(&m)));
(hex::encode(account), hex::encode(account)) (hex::encode(account), hex::encode(account))
}, },
_ => unreachable!(), _ => unreachable!(),
}; };
accounts.insert(k.into(), NodeAccount::new(address, public_key)); accounts.insert(k.into(), NodeAccount::new(address, public_key));
} }
Ok(accounts) Ok(accounts)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn generate_for_alice() { fn generate_for_alice() {
use pezsp_core::crypto::Ss58Codec; use pezsp_core::crypto::Ss58Codec;
let s = "Alice"; let s = "Alice";
let seed = format!("//{s}"); let seed = format!("//{s}");
let pair = generate_pair::<sr25519::Pair>(&seed).unwrap(); let pair = generate_pair::<sr25519::Pair>(&seed).unwrap();
assert_eq!( assert_eq!(
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
pair.public().to_ss58check() pair.public().to_ss58check()
); );
let pair = generate_pair::<ecdsa::Pair>(&seed).unwrap(); let pair = generate_pair::<ecdsa::Pair>(&seed).unwrap();
assert_eq!( assert_eq!(
"0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1", "0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
format!("0x{}", hex::encode(pair.public())) format!("0x{}", hex::encode(pair.public()))
); );
let pair = generate_pair::<ed25519::Pair>(&seed).unwrap(); let pair = generate_pair::<ed25519::Pair>(&seed).unwrap();
assert_eq!( assert_eq!(
"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu", "5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu",
pair.public().to_ss58check() pair.public().to_ss58check()
); );
} }
#[test] #[test]
fn generate_for_zombie() { fn generate_for_zombie() {
use pezsp_core::crypto::Ss58Codec; use pezsp_core::crypto::Ss58Codec;
let s = "Zombie"; let s = "Zombie";
let seed = format!("//{s}"); let seed = format!("//{s}");
let pair = generate_pair::<sr25519::Pair>(&seed).unwrap(); let pair = generate_pair::<sr25519::Pair>(&seed).unwrap();
assert_eq!( assert_eq!(
"5FTcLfwFc7ctvqp3RhbEig6UuHLHcHVRujuUm8r21wy4dAR8", "5FTcLfwFc7ctvqp3RhbEig6UuHLHcHVRujuUm8r21wy4dAR8",
pair.public().to_ss58check() pair.public().to_ss58check()
); );
} }
#[test] #[test]
fn generate_pair_invalid_should_fail() { fn generate_pair_invalid_should_fail() {
let s = "Alice"; let s = "Alice";
let seed = s.to_string(); let seed = s.to_string();
let pair = generate_pair::<sr25519::Pair>(&seed); let pair = generate_pair::<sr25519::Pair>(&seed);
assert!(pair.is_err()); assert!(pair.is_err());
} }
#[test] #[test]
fn generate_invalid_should_fail() { fn generate_invalid_should_fail() {
let s = "Alice"; let s = "Alice";
let seed = s.to_string(); let seed = s.to_string();
let pair = generate(&seed); let pair = generate(&seed);
assert!(pair.is_err()); assert!(pair.is_err());
assert!(matches!(pair, Err(GeneratorError::KeyGeneration(_, _)))); assert!(matches!(pair, Err(GeneratorError::KeyGeneration(_, _))));
} }
#[test] #[test]
fn generate_work() { fn generate_work() {
let s = "Alice"; let s = "Alice";
let seed = format!("//{s}"); let seed = format!("//{s}");
let pair = generate(&seed).unwrap(); let pair = generate(&seed).unwrap();
let sr = pair.get("sr").unwrap(); let sr = pair.get("sr").unwrap();
let sr_stash = pair.get("sr_stash").unwrap(); let sr_stash = pair.get("sr_stash").unwrap();
let ed = pair.get("ed").unwrap(); let ed = pair.get("ed").unwrap();
let ec = pair.get("ec").unwrap(); let ec = pair.get("ec").unwrap();
let eth = pair.get("eth").unwrap(); let eth = pair.get("eth").unwrap();
assert_eq!( assert_eq!(sr.address, "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
sr.address, assert_eq!(sr_stash.address, "5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY");
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" assert_eq!(ed.address, "5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu");
); assert_eq!(
assert_eq!( format!("0x{}", ec.public_key),
sr_stash.address, "0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1"
"5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY" );
);
assert_eq!(
ed.address,
"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"
);
assert_eq!(
format!("0x{}", ec.public_key),
"0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1"
);
assert_eq!( assert_eq!(format!("0x{}", eth.public_key), "0xe04cc55ebee1cbce552f250e85c57b70b2e2625b")
format!("0x{}", eth.public_key), }
"0xe04cc55ebee1cbce552f250e85c57b70b2e2625b"
)
}
} }
@@ -1,6 +1,6 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
vec, vec,
}; };
use hex::encode; use hex::encode;
@@ -8,9 +8,9 @@ use support::{constants::THIS_IS_A_BUG, fs::FileSystem};
use super::errors::GeneratorError; use super::errors::GeneratorError;
use crate::{ use crate::{
generators::keystore_key_types::{parse_keystore_key_types, KeystoreKeyType}, generators::keystore_key_types::{parse_keystore_key_types, KeystoreKeyType},
shared::types::NodeAccounts, shared::types::NodeAccounts,
ScopedFilesystem, ScopedFilesystem,
}; };
/// Generates keystore files for a node. /// Generates keystore files for a node.
@@ -25,266 +25,230 @@ use crate::{
/// If `keystore_key_types` is empty, all default key types will be generated. /// If `keystore_key_types` is empty, all default key types will be generated.
/// Otherwise, only the specified key types will be generated. /// Otherwise, only the specified key types will be generated.
pub async fn generate<'a, T>( pub async fn generate<'a, T>(
acc: &NodeAccounts, acc: &NodeAccounts,
node_files_path: impl AsRef<Path>, node_files_path: impl AsRef<Path>,
scoped_fs: &ScopedFilesystem<'a, T>, scoped_fs: &ScopedFilesystem<'a, T>,
asset_hub_polkadot: bool, asset_hub_polkadot: bool,
keystore_key_types: Vec<&str>, keystore_key_types: Vec<&str>,
) -> Result<Vec<PathBuf>, GeneratorError> ) -> Result<Vec<PathBuf>, GeneratorError>
where where
T: FileSystem, T: FileSystem,
{ {
// Create local keystore // Create local keystore
scoped_fs.create_dir_all(node_files_path.as_ref()).await?; scoped_fs.create_dir_all(node_files_path.as_ref()).await?;
let mut filenames = vec![]; let mut filenames = vec![];
// Parse the key type specifications // Parse the key type specifications
let key_types = parse_keystore_key_types(&keystore_key_types, asset_hub_polkadot); let key_types = parse_keystore_key_types(&keystore_key_types, asset_hub_polkadot);
let futures: Vec<_> = key_types let futures: Vec<_> = key_types
.iter() .iter()
.map(|key_type| { .map(|key_type| {
let filename = generate_keystore_filename(key_type, acc); let filename = generate_keystore_filename(key_type, acc);
let file_path = PathBuf::from(format!( let file_path = PathBuf::from(format!(
"{}/{}", "{}/{}",
node_files_path.as_ref().to_string_lossy(), node_files_path.as_ref().to_string_lossy(),
filename filename
)); ));
let content = format!("\"{}\"", acc.seed); let content = format!("\"{}\"", acc.seed);
(filename, scoped_fs.write(file_path, content)) (filename, scoped_fs.write(file_path, content))
}) })
.collect(); .collect();
for (filename, future) in futures { for (filename, future) in futures {
future.await?; future.await?;
filenames.push(PathBuf::from(filename)); filenames.push(PathBuf::from(filename));
} }
Ok(filenames) Ok(filenames)
} }
/// Generates the keystore filename for a given key type. /// Generates the keystore filename for a given key type.
/// ///
/// The filename format is: `{hex_encoded_key_type}{public_key}` /// The filename format is: `{hex_encoded_key_type}{public_key}`
fn generate_keystore_filename(key_type: &KeystoreKeyType, acc: &NodeAccounts) -> String { fn generate_keystore_filename(key_type: &KeystoreKeyType, acc: &NodeAccounts) -> String {
let account_key = key_type.scheme.account_key(); let account_key = key_type.scheme.account_key();
let pk = acc let pk = acc
.accounts .accounts
.get(account_key) .get(account_key)
.expect(&format!( .expect(&format!("Key '{}' should be set for node {THIS_IS_A_BUG}", account_key))
"Key '{}' should be set for node {THIS_IS_A_BUG}", .public_key
account_key .as_str();
))
.public_key
.as_str();
format!("{}{}", encode(&key_type.key_type), pk) format!("{}{}", encode(&key_type.key_type), pk)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{collections::HashMap, ffi::OsString, str::FromStr}; use std::{collections::HashMap, ffi::OsString, str::FromStr};
use support::fs::in_memory::{InMemoryFile, InMemoryFileSystem}; use support::fs::in_memory::{InMemoryFile, InMemoryFileSystem};
use super::*; use super::*;
use crate::shared::types::{NodeAccount, NodeAccounts}; use crate::shared::types::{NodeAccount, NodeAccounts};
fn create_test_accounts() -> NodeAccounts { fn create_test_accounts() -> NodeAccounts {
let mut accounts = HashMap::new(); let mut accounts = HashMap::new();
accounts.insert( accounts.insert("sr".to_string(), NodeAccount::new("sr_address", "sr_public_key"));
"sr".to_string(), accounts.insert("ed".to_string(), NodeAccount::new("ed_address", "ed_public_key"));
NodeAccount::new("sr_address", "sr_public_key"), accounts.insert("ec".to_string(), NodeAccount::new("ec_address", "ec_public_key"));
); NodeAccounts { seed: "//Alice".to_string(), accounts }
accounts.insert( }
"ed".to_string(),
NodeAccount::new("ed_address", "ed_public_key"),
);
accounts.insert(
"ec".to_string(),
NodeAccount::new("ec_address", "ec_public_key"),
);
NodeAccounts {
seed: "//Alice".to_string(),
accounts,
}
}
fn create_test_fs() -> InMemoryFileSystem { fn create_test_fs() -> InMemoryFileSystem {
InMemoryFileSystem::new(HashMap::from([( InMemoryFileSystem::new(HashMap::from([(
OsString::from_str("/").unwrap(), OsString::from_str("/").unwrap(),
InMemoryFile::dir(), InMemoryFile::dir(),
)])) )]))
} }
#[tokio::test] #[tokio::test]
async fn generate_creates_default_keystore_files_when_no_key_types_specified() { async fn generate_creates_default_keystore_files_when_no_key_types_specified() {
let accounts = create_test_accounts(); let accounts = create_test_accounts();
let fs = create_test_fs(); let fs = create_test_fs();
let base_dir = "/tmp/test"; let base_dir = "/tmp/test";
let scoped_fs = ScopedFilesystem { fs: &fs, base_dir }; let scoped_fs = ScopedFilesystem { fs: &fs, base_dir };
let key_types: Vec<&str> = vec![]; let key_types: Vec<&str> = vec![];
let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await; let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await;
assert!(res.is_ok()); assert!(res.is_ok());
let filenames = res.unwrap(); let filenames = res.unwrap();
assert!(filenames.len() > 10); assert!(filenames.len() > 10);
let filename_strs: Vec<String> = filenames let filename_strs: Vec<String> =
.iter() filenames.iter().map(|p| p.to_string_lossy().to_string()).collect();
.map(|p| p.to_string_lossy().to_string())
.collect();
// Check that aura key is generated (hex of "aura" is 61757261) // Check that aura key is generated (hex of "aura" is 61757261)
assert!(filename_strs.iter().any(|f| f.starts_with("61757261"))); assert!(filename_strs.iter().any(|f| f.starts_with("61757261")));
// Check that babe key is generated (hex of "babe" is 62616265) // Check that babe key is generated (hex of "babe" is 62616265)
assert!(filename_strs.iter().any(|f| f.starts_with("62616265"))); assert!(filename_strs.iter().any(|f| f.starts_with("62616265")));
// Check that gran key is generated (hex of "gran" is 6772616e) // Check that gran key is generated (hex of "gran" is 6772616e)
assert!(filename_strs.iter().any(|f| f.starts_with("6772616e"))); assert!(filename_strs.iter().any(|f| f.starts_with("6772616e")));
} }
#[tokio::test] #[tokio::test]
async fn generate_creates_only_specified_keystore_files() { async fn generate_creates_only_specified_keystore_files() {
let accounts = create_test_accounts(); let accounts = create_test_accounts();
let fs = create_test_fs(); let fs = create_test_fs();
let base_dir = "/tmp/test"; let base_dir = "/tmp/test";
let scoped_fs = ScopedFilesystem { fs: &fs, base_dir }; let scoped_fs = ScopedFilesystem { fs: &fs, base_dir };
let key_types = vec!["audi", "gran"]; let key_types = vec!["audi", "gran"];
let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await; let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await;
assert!(res.is_ok()); assert!(res.is_ok());
let filenames = res.unwrap(); let filenames = res.unwrap();
assert_eq!(filenames.len(), 2); assert_eq!(filenames.len(), 2);
let filename_strs: Vec<String> = filenames let filename_strs: Vec<String> =
.iter() filenames.iter().map(|p| p.to_string_lossy().to_string()).collect();
.map(|p| p.to_string_lossy().to_string())
.collect();
// audi uses sr scheme by default // audi uses sr scheme by default
assert!(filename_strs assert!(filename_strs
.iter() .iter()
.any(|f| f.starts_with("61756469") && f.contains("sr_public_key"))); .any(|f| f.starts_with("61756469") && f.contains("sr_public_key")));
// gran uses ed scheme by default // gran uses ed scheme by default
assert!(filename_strs assert!(filename_strs
.iter() .iter()
.any(|f| f.starts_with("6772616e") && f.contains("ed_public_key"))); .any(|f| f.starts_with("6772616e") && f.contains("ed_public_key")));
} }
#[tokio::test] #[tokio::test]
async fn generate_produces_correct_keystore_files() { async fn generate_produces_correct_keystore_files() {
struct TestCase { struct TestCase {
name: &'static str, name: &'static str,
key_types: Vec<&'static str>, key_types: Vec<&'static str>,
asset_hub_polkadot: bool, asset_hub_polkadot: bool,
expected_prefix: &'static str, expected_prefix: &'static str,
expected_public_key: &'static str, expected_public_key: &'static str,
} }
let test_cases = vec![ let test_cases = vec![
TestCase { TestCase {
name: "explicit scheme override (gran_sr)", name: "explicit scheme override (gran_sr)",
key_types: vec!["gran_sr"], key_types: vec!["gran_sr"],
asset_hub_polkadot: false, asset_hub_polkadot: false,
expected_prefix: "6772616e", // "gran" in hex expected_prefix: "6772616e", // "gran" in hex
expected_public_key: "sr_public_key", expected_public_key: "sr_public_key",
}, },
TestCase { TestCase {
name: "aura with asset_hub_polkadot uses ed", name: "aura with asset_hub_polkadot uses ed",
key_types: vec!["aura"], key_types: vec!["aura"],
asset_hub_polkadot: true, asset_hub_polkadot: true,
expected_prefix: "61757261", // "aura" in hex expected_prefix: "61757261", // "aura" in hex
expected_public_key: "ed_public_key", expected_public_key: "ed_public_key",
}, },
TestCase { TestCase {
name: "aura without asset_hub_polkadot uses sr", name: "aura without asset_hub_polkadot uses sr",
key_types: vec!["aura"], key_types: vec!["aura"],
asset_hub_polkadot: false, asset_hub_polkadot: false,
expected_prefix: "61757261", // "aura" in hex expected_prefix: "61757261", // "aura" in hex
expected_public_key: "sr_public_key", expected_public_key: "sr_public_key",
}, },
TestCase { TestCase {
name: "custom key type with explicit ec scheme", name: "custom key type with explicit ec scheme",
key_types: vec!["cust_ec"], key_types: vec!["cust_ec"],
asset_hub_polkadot: false, asset_hub_polkadot: false,
expected_prefix: "63757374", // "cust" in hex expected_prefix: "63757374", // "cust" in hex
expected_public_key: "ec_public_key", expected_public_key: "ec_public_key",
}, },
]; ];
for tc in test_cases { for tc in test_cases {
let accounts = create_test_accounts(); let accounts = create_test_accounts();
let fs = create_test_fs(); let fs = create_test_fs();
let scoped_fs = ScopedFilesystem { let scoped_fs = ScopedFilesystem { fs: &fs, base_dir: "/tmp/test" };
fs: &fs,
base_dir: "/tmp/test",
};
let key_types: Vec<&str> = tc.key_types.clone(); let key_types: Vec<&str> = tc.key_types.clone();
let res = generate( let res =
&accounts, generate(&accounts, "node1", &scoped_fs, tc.asset_hub_polkadot, key_types).await;
"node1",
&scoped_fs,
tc.asset_hub_polkadot,
key_types,
)
.await;
assert!( assert!(res.is_ok(), "[{}] Expected Ok but got: {:?}", tc.name, res.err());
res.is_ok(), let filenames = res.unwrap();
"[{}] Expected Ok but got: {:?}",
tc.name,
res.err()
);
let filenames = res.unwrap();
assert_eq!(filenames.len(), 1, "[{}] Expected 1 file", tc.name); assert_eq!(filenames.len(), 1, "[{}] Expected 1 file", tc.name);
let filename = filenames[0].to_string_lossy().to_string(); let filename = filenames[0].to_string_lossy().to_string();
assert!( assert!(
filename.starts_with(tc.expected_prefix), filename.starts_with(tc.expected_prefix),
"[{}] Expected prefix '{}', got '{}'", "[{}] Expected prefix '{}', got '{}'",
tc.name, tc.name,
tc.expected_prefix, tc.expected_prefix,
filename filename
); );
assert!( assert!(
filename.contains(tc.expected_public_key), filename.contains(tc.expected_public_key),
"[{}] Expected public key '{}' in '{}'", "[{}] Expected public key '{}' in '{}'",
tc.name, tc.name,
tc.expected_public_key, tc.expected_public_key,
filename filename
); );
} }
} }
#[tokio::test] #[tokio::test]
async fn generate_ignores_invalid_key_specs_and_uses_defaults() { async fn generate_ignores_invalid_key_specs_and_uses_defaults() {
let accounts = create_test_accounts(); let accounts = create_test_accounts();
let fs = create_test_fs(); let fs = create_test_fs();
let scoped_fs = ScopedFilesystem { let scoped_fs = ScopedFilesystem { fs: &fs, base_dir: "/tmp/test" };
fs: &fs,
base_dir: "/tmp/test",
};
let key_types = vec![ let key_types = vec![
"invalid", // Too long "invalid", // Too long
"xxx", // Too short "xxx", // Too short
"audi_xx", // Invalid sceme "audi_xx", // Invalid sceme
]; ];
let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await; let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await;
assert!(res.is_ok()); assert!(res.is_ok());
let filenames = res.unwrap(); let filenames = res.unwrap();
// Should fall back to defaults since all specs are invalid // Should fall back to defaults since all specs are invalid
assert!(filenames.len() > 10); assert!(filenames.len() > 10);
} }
} }
@@ -5,94 +5,91 @@ use serde::{Deserialize, Serialize};
/// Supported cryptographic schemes for keystore keys. /// Supported cryptographic schemes for keystore keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum KeyScheme { pub enum KeyScheme {
/// Sr25519 scheme /// Sr25519 scheme
Sr, Sr,
/// Ed25519 scheme /// Ed25519 scheme
Ed, Ed,
/// ECDSA scheme /// ECDSA scheme
Ec, Ec,
} }
impl KeyScheme { impl KeyScheme {
/// Returns the account key suffix used in `NodeAccounts` for this scheme. /// Returns the account key suffix used in `NodeAccounts` for this scheme.
pub fn account_key(&self) -> &'static str { pub fn account_key(&self) -> &'static str {
match self { match self {
KeyScheme::Sr => "sr", KeyScheme::Sr => "sr",
KeyScheme::Ed => "ed", KeyScheme::Ed => "ed",
KeyScheme::Ec => "ec", KeyScheme::Ec => "ec",
} }
} }
} }
impl std::fmt::Display for KeyScheme { impl std::fmt::Display for KeyScheme {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
KeyScheme::Sr => write!(f, "sr"), KeyScheme::Sr => write!(f, "sr"),
KeyScheme::Ed => write!(f, "ed"), KeyScheme::Ed => write!(f, "ed"),
KeyScheme::Ec => write!(f, "ec"), KeyScheme::Ec => write!(f, "ec"),
} }
} }
} }
impl TryFrom<&str> for KeyScheme { impl TryFrom<&str> for KeyScheme {
type Error = String; type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() { match value.to_lowercase().as_str() {
"sr" => Ok(KeyScheme::Sr), "sr" => Ok(KeyScheme::Sr),
"ed" => Ok(KeyScheme::Ed), "ed" => Ok(KeyScheme::Ed),
"ec" => Ok(KeyScheme::Ec), "ec" => Ok(KeyScheme::Ec),
_ => Err(format!("Unsupported key scheme: {}", value)), _ => Err(format!("Unsupported key scheme: {}", value)),
} }
} }
} }
/// A parsed keystore key type. /// A parsed keystore key type.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeystoreKeyType { pub struct KeystoreKeyType {
/// The 4-character key type identifier (e.g., "aura", "babe", "gran"). /// The 4-character key type identifier (e.g., "aura", "babe", "gran").
pub key_type: String, pub key_type: String,
/// The cryptographic scheme to use for this key type. /// The cryptographic scheme to use for this key type.
pub scheme: KeyScheme, pub scheme: KeyScheme,
} }
impl KeystoreKeyType { impl KeystoreKeyType {
pub fn new(key_type: impl Into<String>, scheme: KeyScheme) -> Self { pub fn new(key_type: impl Into<String>, scheme: KeyScheme) -> Self {
Self { Self { key_type: key_type.into(), scheme }
key_type: key_type.into(), }
scheme,
}
}
} }
/// Returns the default predefined key schemes for known key types. /// Returns the default predefined key schemes for known key types.
/// Special handling for `aura` when `is_asset_hub_polkadot` is true. /// Special handling for `aura` when `is_asset_hub_polkadot` is true.
fn get_predefined_schemes(is_asset_hub_polkadot: bool) -> HashMap<&'static str, KeyScheme> { fn get_predefined_schemes(is_asset_hub_polkadot: bool) -> HashMap<&'static str, KeyScheme> {
let mut schemes = HashMap::new(); let mut schemes = HashMap::new();
// aura has special handling for asset-hub-polkadot // aura has special handling for asset-hub-polkadot
if is_asset_hub_polkadot { if is_asset_hub_polkadot {
schemes.insert("aura", KeyScheme::Ed); schemes.insert("aura", KeyScheme::Ed);
} else { } else {
schemes.insert("aura", KeyScheme::Sr); schemes.insert("aura", KeyScheme::Sr);
} }
schemes.insert("babe", KeyScheme::Sr); schemes.insert("babe", KeyScheme::Sr);
schemes.insert("imon", KeyScheme::Sr); schemes.insert("imon", KeyScheme::Sr);
schemes.insert("gran", KeyScheme::Ed); schemes.insert("gran", KeyScheme::Ed);
schemes.insert("audi", KeyScheme::Sr); schemes.insert("audi", KeyScheme::Sr);
schemes.insert("asgn", KeyScheme::Sr); schemes.insert("asgn", KeyScheme::Sr);
schemes.insert("para", KeyScheme::Sr); schemes.insert("para", KeyScheme::Sr);
schemes.insert("beef", KeyScheme::Ec); schemes.insert("beef", KeyScheme::Ec);
schemes.insert("nmbs", KeyScheme::Sr); // Nimbus schemes.insert("nmbs", KeyScheme::Sr); // Nimbus
schemes.insert("rand", KeyScheme::Sr); // Randomness (Moonbeam) schemes.insert("rand", KeyScheme::Sr); // Randomness (Moonbeam)
schemes.insert("rate", KeyScheme::Ed); // Equilibrium rate module schemes.insert("rate", KeyScheme::Ed); // Equilibrium rate module
schemes.insert("acco", KeyScheme::Sr); schemes.insert("acco", KeyScheme::Sr);
schemes.insert("bcsv", KeyScheme::Sr); // BlockchainSrvc (StorageHub) schemes.insert("bcsv", KeyScheme::Sr); // BlockchainSrvc (StorageHub)
schemes.insert("ftsv", KeyScheme::Ed); // FileTransferSrvc (StorageHub) schemes.insert("ftsv", KeyScheme::Ed); // FileTransferSrvc (StorageHub)
schemes.insert("mixn", KeyScheme::Sr); // Mixnet schemes.insert("mixn", KeyScheme::Sr); // Mixnet
schemes schemes
} }
/// Parses a single keystore key type specification string. /// Parses a single keystore key type specification string.
@@ -103,26 +100,26 @@ fn get_predefined_schemes(is_asset_hub_polkadot: bool) -> HashMap<&'static str,
/// ///
/// Returns `None` if the spec is invalid or doesn't match the expected format. /// Returns `None` if the spec is invalid or doesn't match the expected format.
fn parse_key_spec(spec: &str, predefined: &HashMap<&str, KeyScheme>) -> Option<KeystoreKeyType> { fn parse_key_spec(spec: &str, predefined: &HashMap<&str, KeyScheme>) -> Option<KeystoreKeyType> {
let spec = spec.trim(); let spec = spec.trim();
// Try parsing as long form first: key_type_scheme (e.g., "audi_sr") // Try parsing as long form first: key_type_scheme (e.g., "audi_sr")
if let Some((key_type, scheme_str)) = spec.split_once('_') { if let Some((key_type, scheme_str)) = spec.split_once('_') {
if key_type.len() != 4 { if key_type.len() != 4 {
return None; return None;
} }
let scheme = KeyScheme::try_from(scheme_str).ok()?; let scheme = KeyScheme::try_from(scheme_str).ok()?;
return Some(KeystoreKeyType::new(key_type, scheme)); return Some(KeystoreKeyType::new(key_type, scheme));
} }
// Try parsing as short form: key_type only (e.g., "audi") // Try parsing as short form: key_type only (e.g., "audi")
if spec.len() == 4 { if spec.len() == 4 {
// Look up predefined scheme; default to Sr if not found // Look up predefined scheme; default to Sr if not found
let scheme = predefined.get(spec).copied().unwrap_or(KeyScheme::Sr); let scheme = predefined.get(spec).copied().unwrap_or(KeyScheme::Sr);
return Some(KeystoreKeyType::new(spec, scheme)); return Some(KeystoreKeyType::new(spec, scheme));
} }
None None
} }
/// Parses a list of keystore key type specifications. /// Parses a list of keystore key type specifications.
@@ -132,151 +129,149 @@ fn parse_key_spec(spec: &str, predefined: &HashMap<&str, KeyScheme>) -> Option<K
/// ///
/// If the resulting list is empty, returns the default keystore key types. /// If the resulting list is empty, returns the default keystore key types.
pub fn parse_keystore_key_types<T: AsRef<str>>( pub fn parse_keystore_key_types<T: AsRef<str>>(
specs: &[T], specs: &[T],
is_asset_hub_polkadot: bool, is_asset_hub_polkadot: bool,
) -> Vec<KeystoreKeyType> { ) -> Vec<KeystoreKeyType> {
let predefined_schemes = get_predefined_schemes(is_asset_hub_polkadot); let predefined_schemes = get_predefined_schemes(is_asset_hub_polkadot);
let parsed: Vec<KeystoreKeyType> = specs let parsed: Vec<KeystoreKeyType> = specs
.iter() .iter()
.filter_map(|spec| parse_key_spec(spec.as_ref(), &predefined_schemes)) .filter_map(|spec| parse_key_spec(spec.as_ref(), &predefined_schemes))
.collect(); .collect();
if parsed.is_empty() { if parsed.is_empty() {
get_default_keystore_key_types(is_asset_hub_polkadot) get_default_keystore_key_types(is_asset_hub_polkadot)
} else { } else {
parsed parsed
} }
} }
/// Returns the default keystore key types when none are specified. /// Returns the default keystore key types when none are specified.
pub fn get_default_keystore_key_types(is_asset_hub_polkadot: bool) -> Vec<KeystoreKeyType> { pub fn get_default_keystore_key_types(is_asset_hub_polkadot: bool) -> Vec<KeystoreKeyType> {
let predefined_schemes = get_predefined_schemes(is_asset_hub_polkadot); let predefined_schemes = get_predefined_schemes(is_asset_hub_polkadot);
let default_keys = [ let default_keys = [
"aura", "babe", "imon", "gran", "audi", "asgn", "para", "beef", "nmbs", "rand", "rate", "aura", "babe", "imon", "gran", "audi", "asgn", "para", "beef", "nmbs", "rand", "rate",
"mixn", "bcsv", "ftsv", "mixn", "bcsv", "ftsv",
]; ];
default_keys default_keys
.iter() .iter()
.filter_map(|key_type| { .filter_map(|key_type| {
predefined_schemes predefined_schemes.get(*key_type).map(|scheme| KeystoreKeyType::new(*key_type, *scheme))
.get(*key_type) })
.map(|scheme| KeystoreKeyType::new(*key_type, *scheme)) .collect()
})
.collect()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn parse_keystore_key_types_ignores_invalid_specs() { fn parse_keystore_key_types_ignores_invalid_specs() {
let specs = vec![ let specs = vec![
"audi".to_string(), "audi".to_string(),
"invalid".to_string(), // Too long - ignored "invalid".to_string(), // Too long - ignored
"xxx".to_string(), // Too short - ignored "xxx".to_string(), // Too short - ignored
"xxxx".to_string(), // Unknown key - defaults to sr "xxxx".to_string(), // Unknown key - defaults to sr
"audi_xx".to_string(), // Invalid scheme - ignored "audi_xx".to_string(), // Invalid scheme - ignored
"gran".to_string(), "gran".to_string(),
]; ];
let result = parse_keystore_key_types(&specs, false); let result = parse_keystore_key_types(&specs, false);
assert_eq!(result.len(), 3); assert_eq!(result.len(), 3);
assert_eq!(result[1], KeystoreKeyType::new("xxxx", KeyScheme::Sr)); // Unknown defaults to sr assert_eq!(result[1], KeystoreKeyType::new("xxxx", KeyScheme::Sr)); // Unknown defaults to sr
assert_eq!(result[2], KeystoreKeyType::new("gran", KeyScheme::Ed)); assert_eq!(result[2], KeystoreKeyType::new("gran", KeyScheme::Ed));
} }
#[test] #[test]
fn parse_keystore_key_types_returns_specified_keys() { fn parse_keystore_key_types_returns_specified_keys() {
let specs = vec!["audi".to_string(), "gran".to_string()]; let specs = vec!["audi".to_string(), "gran".to_string()];
let res = parse_keystore_key_types(&specs, false); let res = parse_keystore_key_types(&specs, false);
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
assert_eq!(res[0], KeystoreKeyType::new("audi", KeyScheme::Sr)); assert_eq!(res[0], KeystoreKeyType::new("audi", KeyScheme::Sr));
assert_eq!(res[1], KeystoreKeyType::new("gran", KeyScheme::Ed)); assert_eq!(res[1], KeystoreKeyType::new("gran", KeyScheme::Ed));
} }
#[test] #[test]
fn parse_keystore_key_types_mixed_short_and_long_forms() { fn parse_keystore_key_types_mixed_short_and_long_forms() {
let specs = vec![ let specs = vec![
"audi".to_string(), "audi".to_string(),
"gran_sr".to_string(), // Override gran's default ed to sr "gran_sr".to_string(), // Override gran's default ed to sr
"gran".to_string(), "gran".to_string(),
"beef".to_string(), "beef".to_string(),
]; ];
let res = parse_keystore_key_types(&specs, false); let res = parse_keystore_key_types(&specs, false);
assert_eq!(res.len(), 4); assert_eq!(res.len(), 4);
assert_eq!(res[0], KeystoreKeyType::new("audi", KeyScheme::Sr)); assert_eq!(res[0], KeystoreKeyType::new("audi", KeyScheme::Sr));
assert_eq!(res[1], KeystoreKeyType::new("gran", KeyScheme::Sr)); // Overridden assert_eq!(res[1], KeystoreKeyType::new("gran", KeyScheme::Sr)); // Overridden
assert_eq!(res[2], KeystoreKeyType::new("gran", KeyScheme::Ed)); assert_eq!(res[2], KeystoreKeyType::new("gran", KeyScheme::Ed));
assert_eq!(res[3], KeystoreKeyType::new("beef", KeyScheme::Ec)); assert_eq!(res[3], KeystoreKeyType::new("beef", KeyScheme::Ec));
} }
#[test] #[test]
fn parse_keystore_key_types_returns_defaults_when_empty() { fn parse_keystore_key_types_returns_defaults_when_empty() {
let specs: Vec<String> = vec![]; let specs: Vec<String> = vec![];
let res = parse_keystore_key_types(&specs, false); let res = parse_keystore_key_types(&specs, false);
// Should return all default keys // Should return all default keys
assert!(!res.is_empty()); assert!(!res.is_empty());
assert!(res.iter().any(|k| k.key_type == "aura")); assert!(res.iter().any(|k| k.key_type == "aura"));
assert!(res.iter().any(|k| k.key_type == "babe")); assert!(res.iter().any(|k| k.key_type == "babe"));
assert!(res.iter().any(|k| k.key_type == "gran")); assert!(res.iter().any(|k| k.key_type == "gran"));
} }
#[test] #[test]
fn parse_keystore_key_types_allows_custom_key_with_explicit_scheme() { fn parse_keystore_key_types_allows_custom_key_with_explicit_scheme() {
let specs = vec![ let specs = vec![
"cust_sr".to_string(), // Custom key with explicit scheme "cust_sr".to_string(), // Custom key with explicit scheme
"audi".to_string(), "audi".to_string(),
]; ];
let result = parse_keystore_key_types(&specs, false); let result = parse_keystore_key_types(&specs, false);
assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
assert_eq!(result[0], KeystoreKeyType::new("cust", KeyScheme::Sr)); assert_eq!(result[0], KeystoreKeyType::new("cust", KeyScheme::Sr));
assert_eq!(result[1], KeystoreKeyType::new("audi", KeyScheme::Sr)); assert_eq!(result[1], KeystoreKeyType::new("audi", KeyScheme::Sr));
} }
#[test] #[test]
fn full_workflow_asset_hub_polkadot() { fn full_workflow_asset_hub_polkadot() {
// For asset-hub-polkadot, aura should default to ed // For asset-hub-polkadot, aura should default to ed
let specs = vec!["aura".to_string(), "babe".to_string()]; let specs = vec!["aura".to_string(), "babe".to_string()];
let res = parse_keystore_key_types(&specs, true); let res = parse_keystore_key_types(&specs, true);
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
assert_eq!(res[0].key_type, "aura"); assert_eq!(res[0].key_type, "aura");
assert_eq!(res[0].scheme, KeyScheme::Ed); // ed for asset-hub-polkadot assert_eq!(res[0].scheme, KeyScheme::Ed); // ed for asset-hub-polkadot
assert_eq!(res[1].key_type, "babe"); assert_eq!(res[1].key_type, "babe");
assert_eq!(res[1].scheme, KeyScheme::Sr); assert_eq!(res[1].scheme, KeyScheme::Sr);
} }
#[test] #[test]
fn full_workflow_custom_key_types() { fn full_workflow_custom_key_types() {
let specs = vec![ let specs = vec![
"aura".to_string(), // Use default scheme "aura".to_string(), // Use default scheme
"gran_sr".to_string(), // Override gran to use sr instead of ed "gran_sr".to_string(), // Override gran to use sr instead of ed
"cust_ec".to_string(), // Custom key type with ecdsa "cust_ec".to_string(), // Custom key type with ecdsa
]; ];
let res = parse_keystore_key_types(&specs, false); let res = parse_keystore_key_types(&specs, false);
assert_eq!(res.len(), 3); assert_eq!(res.len(), 3);
// aura uses default sr // aura uses default sr
assert_eq!(res[0].key_type, "aura"); assert_eq!(res[0].key_type, "aura");
assert_eq!(res[0].scheme, KeyScheme::Sr); assert_eq!(res[0].scheme, KeyScheme::Sr);
// gran overridden to sr // gran overridden to sr
assert_eq!(res[1].key_type, "gran"); assert_eq!(res[1].key_type, "gran");
assert_eq!(res[1].scheme, KeyScheme::Sr); assert_eq!(res[1].scheme, KeyScheme::Sr);
// custom key with ec // custom key with ec
assert_eq!(res[2].key_type, "cust"); assert_eq!(res[2].key_type, "cust");
assert_eq!(res[2].scheme, KeyScheme::Ec); assert_eq!(res[2].scheme, KeyScheme::Ec);
} }
} }
@@ -2,9 +2,9 @@ use std::path::{Path, PathBuf};
use configuration::types::CommandWithCustomArgs; use configuration::types::CommandWithCustomArgs;
use provider::{ use provider::{
constants::NODE_CONFIG_DIR, constants::NODE_CONFIG_DIR,
types::{GenerateFileCommand, GenerateFilesOptions, TransferedFile}, types::{GenerateFileCommand, GenerateFilesOptions, TransferedFile},
DynNamespace, DynNamespace,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use support::fs::FileSystem; use support::fs::FileSystem;
@@ -15,151 +15,137 @@ use crate::ScopedFilesystem;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ParaArtifactType { pub(crate) enum ParaArtifactType {
Wasm, Wasm,
State, State,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ParaArtifactBuildOption { pub(crate) enum ParaArtifactBuildOption {
Path(String), Path(String),
Command(String), Command(String),
CommandWithCustomArgs(CommandWithCustomArgs), CommandWithCustomArgs(CommandWithCustomArgs),
} }
/// Parachain artifact (could be either the genesis state or genesis wasm) /// Parachain artifact (could be either the genesis state or genesis wasm)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParaArtifact { pub struct ParaArtifact {
artifact_type: ParaArtifactType, artifact_type: ParaArtifactType,
build_option: ParaArtifactBuildOption, build_option: ParaArtifactBuildOption,
artifact_path: Option<PathBuf>, artifact_path: Option<PathBuf>,
// image to use for building the para artifact // image to use for building the para artifact
image: Option<String>, image: Option<String>,
} }
impl ParaArtifact { impl ParaArtifact {
pub(crate) fn new( pub(crate) fn new(
artifact_type: ParaArtifactType, artifact_type: ParaArtifactType,
build_option: ParaArtifactBuildOption, build_option: ParaArtifactBuildOption,
) -> Self { ) -> Self {
Self { Self { artifact_type, build_option, artifact_path: None, image: None }
artifact_type, }
build_option,
artifact_path: None,
image: None,
}
}
pub(crate) fn image(mut self, image: Option<String>) -> Self { pub(crate) fn image(mut self, image: Option<String>) -> Self {
self.image = image; self.image = image;
self self
} }
pub(crate) fn artifact_path(&self) -> Option<&PathBuf> { pub(crate) fn artifact_path(&self) -> Option<&PathBuf> {
self.artifact_path.as_ref() self.artifact_path.as_ref()
} }
pub(crate) async fn build<'a, T>( pub(crate) async fn build<'a, T>(
&mut self, &mut self,
chain_spec_path: Option<impl AsRef<Path>>, chain_spec_path: Option<impl AsRef<Path>>,
artifact_path: impl AsRef<Path>, artifact_path: impl AsRef<Path>,
ns: &DynNamespace, ns: &DynNamespace,
scoped_fs: &ScopedFilesystem<'a, T>, scoped_fs: &ScopedFilesystem<'a, T>,
maybe_output_path: Option<PathBuf>, maybe_output_path: Option<PathBuf>,
) -> Result<(), GeneratorError> ) -> Result<(), GeneratorError>
where where
T: FileSystem, T: FileSystem,
{ {
let (cmd, custom_args) = match &self.build_option { let (cmd, custom_args) = match &self.build_option {
ParaArtifactBuildOption::Path(path) => { ParaArtifactBuildOption::Path(path) => {
let t = TransferedFile::new(PathBuf::from(path), artifact_path.as_ref().into()); let t = TransferedFile::new(PathBuf::from(path), artifact_path.as_ref().into());
scoped_fs.copy_files(vec![&t]).await?; scoped_fs.copy_files(vec![&t]).await?;
self.artifact_path = Some(artifact_path.as_ref().into()); self.artifact_path = Some(artifact_path.as_ref().into());
return Ok(()); // work done! return Ok(()); // work done!
}, },
ParaArtifactBuildOption::Command(cmd) => (cmd, &vec![]), ParaArtifactBuildOption::Command(cmd) => (cmd, &vec![]),
ParaArtifactBuildOption::CommandWithCustomArgs(cmd_with_custom_args) => { ParaArtifactBuildOption::CommandWithCustomArgs(cmd_with_custom_args) => {
( (&cmd_with_custom_args.cmd().as_str().to_string(), cmd_with_custom_args.args())
&cmd_with_custom_args.cmd().as_str().to_string(), // (cmd.cmd_as_str().to_string(), cmd.1)
cmd_with_custom_args.args(), },
) };
// (cmd.cmd_as_str().to_string(), cmd.1)
},
};
let generate_subcmd = match self.artifact_type { let generate_subcmd = match self.artifact_type {
ParaArtifactType::Wasm => "export-genesis-wasm", ParaArtifactType::Wasm => "export-genesis-wasm",
ParaArtifactType::State => "export-genesis-state", ParaArtifactType::State => "export-genesis-state",
}; };
// TODO: replace uuid with para_id-random // TODO: replace uuid with para_id-random
let temp_name = format!("temp-{}-{}", generate_subcmd, Uuid::new_v4()); let temp_name = format!("temp-{}-{}", generate_subcmd, Uuid::new_v4());
let mut args: Vec<String> = vec![generate_subcmd.into()]; let mut args: Vec<String> = vec![generate_subcmd.into()];
let files_to_inject = if let Some(chain_spec_path) = chain_spec_path { let files_to_inject = if let Some(chain_spec_path) = chain_spec_path {
// TODO: we should get the full path from the scoped filesystem // TODO: we should get the full path from the scoped filesystem
let chain_spec_path_local = format!( let chain_spec_path_local = format!(
"{}/{}", "{}/{}",
ns.base_dir().to_string_lossy(), ns.base_dir().to_string_lossy(),
chain_spec_path.as_ref().to_string_lossy() chain_spec_path.as_ref().to_string_lossy()
); );
// Remote path to be injected // Remote path to be injected
let chain_spec_path_in_pod = format!( let chain_spec_path_in_pod =
"{}/{}", format!("{}/{}", NODE_CONFIG_DIR, chain_spec_path.as_ref().to_string_lossy());
NODE_CONFIG_DIR, // Path in the context of the node, this can be different in the context of the providers (e.g native)
chain_spec_path.as_ref().to_string_lossy() let chain_spec_path_in_args = if ns.capabilities().prefix_with_full_path {
); // In native
// Path in the context of the node, this can be different in the context of the providers (e.g native) format!(
let chain_spec_path_in_args = if ns.capabilities().prefix_with_full_path { "{}/{}{}",
// In native ns.base_dir().to_string_lossy(),
format!( &temp_name,
"{}/{}{}", &chain_spec_path_in_pod
ns.base_dir().to_string_lossy(), )
&temp_name, } else {
&chain_spec_path_in_pod chain_spec_path_in_pod.clone()
) };
} else {
chain_spec_path_in_pod.clone()
};
args.push("--chain".into()); args.push("--chain".into());
args.push(chain_spec_path_in_args); args.push(chain_spec_path_in_args);
for custom_arg in custom_args { for custom_arg in custom_args {
match custom_arg { match custom_arg {
configuration::types::Arg::Flag(flag) => { configuration::types::Arg::Flag(flag) => {
args.push(flag.into()); args.push(flag.into());
}, },
configuration::types::Arg::Option(flag, flag_value) => { configuration::types::Arg::Option(flag, flag_value) => {
args.push(flag.into()); args.push(flag.into());
args.push(flag_value.into()); args.push(flag_value.into());
}, },
configuration::types::Arg::Array(flag, values) => { configuration::types::Arg::Array(flag, values) => {
args.push(flag.into()); args.push(flag.into());
values.iter().for_each(|v| args.push(v.into())); values.iter().for_each(|v| args.push(v.into()));
}, },
} }
} }
vec![TransferedFile::new( vec![TransferedFile::new(chain_spec_path_local, chain_spec_path_in_pod)]
chain_spec_path_local, } else {
chain_spec_path_in_pod, vec![]
)] };
} else {
vec![]
};
let artifact_path_ref = artifact_path.as_ref(); let artifact_path_ref = artifact_path.as_ref();
let generate_command = GenerateFileCommand::new(cmd.as_str(), artifact_path_ref).args(args); let generate_command = GenerateFileCommand::new(cmd.as_str(), artifact_path_ref).args(args);
let options = GenerateFilesOptions::with_files( let options = GenerateFilesOptions::with_files(
vec![generate_command], vec![generate_command],
self.image.clone(), self.image.clone(),
&files_to_inject, &files_to_inject,
maybe_output_path, maybe_output_path,
) )
.temp_name(temp_name); .temp_name(temp_name);
ns.generate_files(options).await?; ns.generate_files(options).await?;
self.artifact_path = Some(artifact_path_ref.into()); self.artifact_path = Some(artifact_path_ref.into());
Ok(()) Ok(())
} }
} }
@@ -8,41 +8,39 @@ use crate::shared::types::ParkedPort;
// TODO: (team), we want to continue support ws_port? No // TODO: (team), we want to continue support ws_port? No
enum PortTypes { enum PortTypes {
Rpc, Rpc,
P2P, P2P,
Prometheus, Prometheus,
} }
pub fn generate(port: Option<Port>) -> Result<ParkedPort, GeneratorError> { pub fn generate(port: Option<Port>) -> Result<ParkedPort, GeneratorError> {
let port = port.unwrap_or(0); let port = port.unwrap_or(0);
let listener = TcpListener::bind(format!("0.0.0.0:{port}")) let listener = TcpListener::bind(format!("0.0.0.0:{port}"))
.map_err(|_e| GeneratorError::PortGeneration(port, "Can't bind".into()))?; .map_err(|_e| GeneratorError::PortGeneration(port, "Can't bind".into()))?;
let port = listener let port = listener
.local_addr() .local_addr()
.expect(&format!( .expect(&format!("We should always get the local_addr from the listener {THIS_IS_A_BUG}"))
"We should always get the local_addr from the listener {THIS_IS_A_BUG}" .port();
)) Ok(ParkedPort::new(port, listener))
.port();
Ok(ParkedPort::new(port, listener))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn generate_random() { fn generate_random() {
let port = generate(None).unwrap(); let port = generate(None).unwrap();
let listener = port.1.write().unwrap(); let listener = port.1.write().unwrap();
assert!(listener.is_some()); assert!(listener.is_some());
} }
#[test] #[test]
fn generate_fixed_port() { fn generate_fixed_port() {
let port = generate(Some(33056)).unwrap(); let port = generate(Some(33056)).unwrap();
let listener = port.1.write().unwrap(); let listener = port.1.write().unwrap();
assert!(listener.is_some()); assert!(listener.is_some());
assert_eq!(port.0, 33056); assert_eq!(port.0, 33056);
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -9,33 +9,33 @@ use crate::{shared::types::RuntimeUpgradeOptions, tx_helper};
#[async_trait] #[async_trait]
pub trait ChainUpgrade { pub trait ChainUpgrade {
/// Perform a runtime upgrade (with sudo) /// Perform a runtime upgrade (with sudo)
/// ///
/// This call 'System.set_code_without_checks' wrapped in /// This call 'System.set_code_without_checks' wrapped in
/// 'Sudo.sudo_unchecked_weight' /// 'Sudo.sudo_unchecked_weight'
async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error>; async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error>;
/// Perform a runtime upgrade (with sudo), inner call with the node pass as arg. /// Perform a runtime upgrade (with sudo), inner call with the node pass as arg.
/// ///
/// This call 'System.set_code_without_checks' wrapped in /// This call 'System.set_code_without_checks' wrapped in
/// 'Sudo.sudo_unchecked_weight' /// 'Sudo.sudo_unchecked_weight'
async fn perform_runtime_upgrade( async fn perform_runtime_upgrade(
&self, &self,
node: &NetworkNode, node: &NetworkNode,
options: RuntimeUpgradeOptions, options: RuntimeUpgradeOptions,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let sudo = if let Some(possible_seed) = options.seed { let sudo = if let Some(possible_seed) = options.seed {
Keypair::from_secret_key(possible_seed) Keypair::from_secret_key(possible_seed)
.map_err(|_| anyhow!("seed should return a Keypair"))? .map_err(|_| anyhow!("seed should return a Keypair"))?
} else { } else {
let uri = SecretUri::from_str("//Alice")?; let uri = SecretUri::from_str("//Alice")?;
Keypair::from_uri(&uri).map_err(|_| anyhow!("'//Alice' should return a Keypair"))? Keypair::from_uri(&uri).map_err(|_| anyhow!("'//Alice' should return a Keypair"))?
}; };
let wasm_data = options.wasm.get_asset().await?; let wasm_data = options.wasm.get_asset().await?;
tx_helper::runtime_upgrade::upgrade(node, &wasm_data, &sudo).await?; tx_helper::runtime_upgrade::upgrade(node, &wasm_data, &sudo).await?;
Ok(()) Ok(())
} }
} }
File diff suppressed because it is too large Load Diff
@@ -6,70 +6,61 @@ use serde::{Deserialize, Serialize};
use super::node::NetworkNode; use super::node::NetworkNode;
use crate::{ use crate::{
network::chain_upgrade::ChainUpgrade, shared::types::RuntimeUpgradeOptions, network::chain_upgrade::ChainUpgrade, shared::types::RuntimeUpgradeOptions,
utils::default_as_empty_vec, utils::default_as_empty_vec,
}; };
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Relaychain { pub struct Relaychain {
pub(crate) chain: String, pub(crate) chain: String,
pub(crate) chain_id: String, pub(crate) chain_id: String,
pub(crate) chain_spec_path: PathBuf, pub(crate) chain_spec_path: PathBuf,
#[serde(default, deserialize_with = "default_as_empty_vec")] #[serde(default, deserialize_with = "default_as_empty_vec")]
pub(crate) nodes: Vec<NetworkNode>, pub(crate) nodes: Vec<NetworkNode>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct RawRelaychain { pub(crate) struct RawRelaychain {
#[serde(flatten)] #[serde(flatten)]
pub(crate) inner: Relaychain, pub(crate) inner: Relaychain,
pub(crate) nodes: serde_json::Value, pub(crate) nodes: serde_json::Value,
} }
#[async_trait] #[async_trait]
impl ChainUpgrade for Relaychain { impl ChainUpgrade for Relaychain {
async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> { async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> {
// check if the node is valid first // check if the node is valid first
let node = if let Some(node_name) = &options.node_name { let node = if let Some(node_name) = &options.node_name {
if let Some(node) = self if let Some(node) = self.nodes().into_iter().find(|node| node.name() == node_name) {
.nodes() node
.into_iter() } else {
.find(|node| node.name() == node_name) return Err(anyhow!("Node: {node_name} is not part of the set of nodes"));
{ }
node } else {
} else { // take the first node
return Err(anyhow!("Node: {node_name} is not part of the set of nodes")); if let Some(node) = self.nodes().first() {
} node
} else { } else {
// take the first node return Err(anyhow!("chain doesn't have any node!"));
if let Some(node) = self.nodes().first() { }
node };
} else {
return Err(anyhow!("chain doesn't have any node!"));
}
};
self.perform_runtime_upgrade(node, options).await self.perform_runtime_upgrade(node, options).await
} }
} }
impl Relaychain { impl Relaychain {
pub(crate) fn new(chain: String, chain_id: String, chain_spec_path: PathBuf) -> Self { pub(crate) fn new(chain: String, chain_id: String, chain_spec_path: PathBuf) -> Self {
Self { Self { chain, chain_id, chain_spec_path, nodes: Default::default() }
chain, }
chain_id,
chain_spec_path,
nodes: Default::default(),
}
}
// Public API // Public API
pub fn nodes(&self) -> Vec<&NetworkNode> { pub fn nodes(&self) -> Vec<&NetworkNode> {
self.nodes.iter().collect() self.nodes.iter().collect()
} }
/// Get chain name /// Get chain name
pub fn chain(&self) -> &str { pub fn chain(&self) -> &str {
&self.chain &self.chain
} }
} }
@@ -1,6 +1,6 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use anyhow::anyhow; use anyhow::anyhow;
@@ -14,317 +14,282 @@ use tracing::info;
use super::{chain_upgrade::ChainUpgrade, node::NetworkNode}; use super::{chain_upgrade::ChainUpgrade, node::NetworkNode};
use crate::{ use crate::{
network_spec::teyrchain::TeyrchainSpec, network_spec::teyrchain::TeyrchainSpec,
shared::types::{RegisterParachainOptions, RuntimeUpgradeOptions}, shared::types::{RegisterParachainOptions, RuntimeUpgradeOptions},
tx_helper::client::get_client_from_url, tx_helper::client::get_client_from_url,
utils::default_as_empty_vec, utils::default_as_empty_vec,
ScopedFilesystem, ScopedFilesystem,
}; };
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Parachain { pub struct Parachain {
pub(crate) chain: Option<String>, pub(crate) chain: Option<String>,
pub(crate) para_id: u32, pub(crate) para_id: u32,
// unique_id is internally used to allow multiple parachains with the same id // unique_id is internally used to allow multiple parachains with the same id
// See `ParachainConfig` for more details // See `ParachainConfig` for more details
pub(crate) unique_id: String, pub(crate) unique_id: String,
pub(crate) chain_id: Option<String>, pub(crate) chain_id: Option<String>,
pub(crate) chain_spec_path: Option<PathBuf>, pub(crate) chain_spec_path: Option<PathBuf>,
#[serde(default, deserialize_with = "default_as_empty_vec")] #[serde(default, deserialize_with = "default_as_empty_vec")]
pub(crate) collators: Vec<NetworkNode>, pub(crate) collators: Vec<NetworkNode>,
pub(crate) files_to_inject: Vec<TransferedFile>, pub(crate) files_to_inject: Vec<TransferedFile>,
pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>, pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct RawParachain { pub(crate) struct RawParachain {
#[serde(flatten)] #[serde(flatten)]
pub(crate) inner: Parachain, pub(crate) inner: Parachain,
pub(crate) collators: serde_json::Value, pub(crate) collators: serde_json::Value,
} }
#[async_trait] #[async_trait]
impl ChainUpgrade for Parachain { impl ChainUpgrade for Parachain {
async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> { async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> {
// check if the node is valid first // check if the node is valid first
let node = if let Some(node_name) = &options.node_name { let node = if let Some(node_name) = &options.node_name {
if let Some(node) = self if let Some(node) = self.collators().into_iter().find(|node| node.name() == node_name) {
.collators() node
.into_iter() } else {
.find(|node| node.name() == node_name) return Err(anyhow!("Node: {node_name} is not part of the set of nodes"));
{ }
node } else {
} else { // take the first node
return Err(anyhow!("Node: {node_name} is not part of the set of nodes")); if let Some(node) = self.collators().first() {
} node
} else { } else {
// take the first node return Err(anyhow!("chain doesn't have any node!"));
if let Some(node) = self.collators().first() { }
node };
} else {
return Err(anyhow!("chain doesn't have any node!"));
}
};
self.perform_runtime_upgrade(node, options).await self.perform_runtime_upgrade(node, options).await
} }
} }
impl Parachain { impl Parachain {
pub(crate) fn new(para_id: u32, unique_id: impl Into<String>) -> Self { pub(crate) fn new(para_id: u32, unique_id: impl Into<String>) -> Self {
Self { Self {
chain: None, chain: None,
para_id, para_id,
unique_id: unique_id.into(), unique_id: unique_id.into(),
chain_id: None, chain_id: None,
chain_spec_path: None, chain_spec_path: None,
collators: Default::default(), collators: Default::default(),
files_to_inject: Default::default(), files_to_inject: Default::default(),
bootnodes_addresses: vec![], bootnodes_addresses: vec![],
} }
} }
pub(crate) fn with_chain_spec( pub(crate) fn with_chain_spec(
para_id: u32, para_id: u32,
unique_id: impl Into<String>, unique_id: impl Into<String>,
chain_id: impl Into<String>, chain_id: impl Into<String>,
chain_spec_path: impl AsRef<Path>, chain_spec_path: impl AsRef<Path>,
) -> Self { ) -> Self {
Self { Self {
para_id, para_id,
unique_id: unique_id.into(), unique_id: unique_id.into(),
chain: None, chain: None,
chain_id: Some(chain_id.into()), chain_id: Some(chain_id.into()),
chain_spec_path: Some(chain_spec_path.as_ref().into()), chain_spec_path: Some(chain_spec_path.as_ref().into()),
collators: Default::default(), collators: Default::default(),
files_to_inject: Default::default(), files_to_inject: Default::default(),
bootnodes_addresses: vec![], bootnodes_addresses: vec![],
} }
} }
pub(crate) async fn from_spec( pub(crate) async fn from_spec(
para: &TeyrchainSpec, para: &TeyrchainSpec,
files_to_inject: &[TransferedFile], files_to_inject: &[TransferedFile],
scoped_fs: &ScopedFilesystem<'_, impl FileSystem>, scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
) -> Result<Self, anyhow::Error> { ) -> Result<Self, anyhow::Error> {
let mut para_files_to_inject = files_to_inject.to_owned(); let mut para_files_to_inject = files_to_inject.to_owned();
// parachain id is used for the keystore // parachain id is used for the keystore
let mut parachain = if let Some(chain_spec) = para.chain_spec.as_ref() { let mut parachain = if let Some(chain_spec) = para.chain_spec.as_ref() {
let id = chain_spec.read_chain_id(scoped_fs).await?; let id = chain_spec.read_chain_id(scoped_fs).await?;
// add the spec to global files to inject // add the spec to global files to inject
let spec_name = chain_spec.chain_spec_name(); let spec_name = chain_spec.chain_spec_name();
let base = PathBuf::from_str(scoped_fs.base_dir)?; let base = PathBuf::from_str(scoped_fs.base_dir)?;
para_files_to_inject.push(TransferedFile::new( para_files_to_inject.push(TransferedFile::new(
base.join(format!("{spec_name}.json")), base.join(format!("{spec_name}.json")),
PathBuf::from(format!("/cfg/{}.json", para.id)), PathBuf::from(format!("/cfg/{}.json", para.id)),
)); ));
let raw_path = chain_spec let raw_path = chain_spec
.raw_path() .raw_path()
.ok_or(anyhow::anyhow!("chain-spec path should be set by now.",))?; .ok_or(anyhow::anyhow!("chain-spec path should be set by now.",))?;
let mut running_para = let mut running_para =
Parachain::with_chain_spec(para.id, &para.unique_id, id, raw_path); Parachain::with_chain_spec(para.id, &para.unique_id, id, raw_path);
if let Some(chain_name) = chain_spec.chain_name() { if let Some(chain_name) = chain_spec.chain_name() {
running_para.chain = Some(chain_name.to_string()); running_para.chain = Some(chain_name.to_string());
} }
running_para running_para
} else { } else {
Parachain::new(para.id, &para.unique_id) Parachain::new(para.id, &para.unique_id)
}; };
parachain.bootnodes_addresses = para.bootnodes_addresses().into_iter().cloned().collect(); parachain.bootnodes_addresses = para.bootnodes_addresses().into_iter().cloned().collect();
parachain.files_to_inject = para_files_to_inject; parachain.files_to_inject = para_files_to_inject;
Ok(parachain) Ok(parachain)
} }
pub async fn register( pub async fn register(
options: RegisterParachainOptions, options: RegisterParachainOptions,
scoped_fs: &ScopedFilesystem<'_, impl FileSystem>, scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
info!("Registering parachain: {:?}", options); info!("Registering parachain: {:?}", options);
// get the seed // get the seed
let sudo: Keypair; let sudo: Keypair;
if let Some(possible_seed) = options.seed { if let Some(possible_seed) = options.seed {
sudo = Keypair::from_secret_key(possible_seed) sudo = Keypair::from_secret_key(possible_seed)
.expect(&format!("seed should return a Keypair {THIS_IS_A_BUG}")); .expect(&format!("seed should return a Keypair {THIS_IS_A_BUG}"));
} else { } else {
let uri = SecretUri::from_str("//Alice")?; let uri = SecretUri::from_str("//Alice")?;
sudo = Keypair::from_uri(&uri)?; sudo = Keypair::from_uri(&uri)?;
} }
let genesis_state = scoped_fs let genesis_state = scoped_fs
.read_to_string(options.state_path) .read_to_string(options.state_path)
.await .await
.expect(&format!( .expect(&format!("State Path should be ok by this point {THIS_IS_A_BUG}"));
"State Path should be ok by this point {THIS_IS_A_BUG}" let wasm_data = scoped_fs
)); .read_to_string(options.wasm_path)
let wasm_data = scoped_fs .await
.read_to_string(options.wasm_path) .expect(&format!("Wasm Path should be ok by this point {THIS_IS_A_BUG}"));
.await
.expect(&format!(
"Wasm Path should be ok by this point {THIS_IS_A_BUG}"
));
wait_ws_ready(options.node_ws_url.as_str()) wait_ws_ready(options.node_ws_url.as_str()).await.map_err(|_| {
.await anyhow::anyhow!("Error waiting for ws to be ready, at {}", options.node_ws_url.as_str())
.map_err(|_| { })?;
anyhow::anyhow!(
"Error waiting for ws to be ready, at {}",
options.node_ws_url.as_str()
)
})?;
let api: OnlineClient<BizinikiwConfig> = get_client_from_url(&options.node_ws_url).await?; let api: OnlineClient<BizinikiwConfig> = get_client_from_url(&options.node_ws_url).await?;
let schedule_para = pezkuwi_subxt::dynamic::tx( let schedule_para = pezkuwi_subxt::dynamic::tx(
"ParasSudoWrapper", "ParasSudoWrapper",
"sudo_schedule_para_initialize", "sudo_schedule_para_initialize",
vec![ vec![
Value::primitive(options.id.into()), Value::primitive(options.id.into()),
Value::named_composite([ Value::named_composite([
( ("genesis_head", Value::from_bytes(hex::decode(&genesis_state[2..])?)),
"genesis_head", ("validation_code", Value::from_bytes(hex::decode(&wasm_data[2..])?)),
Value::from_bytes(hex::decode(&genesis_state[2..])?), ("para_kind", Value::bool(options.onboard_as_para)),
), ]),
( ],
"validation_code", );
Value::from_bytes(hex::decode(&wasm_data[2..])?),
),
("para_kind", Value::bool(options.onboard_as_para)),
]),
],
);
let sudo_call = let sudo_call =
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![schedule_para.into_value()]); pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![schedule_para.into_value()]);
// TODO: uncomment below and fix the sign and submit (and follow afterwards until // TODO: uncomment below and fix the sign and submit (and follow afterwards until
// finalized block) to register the parachain // finalized block) to register the parachain
let mut tx = api let mut tx = api.tx().sign_and_submit_then_watch_default(&sudo_call, &sudo).await?;
.tx()
.sign_and_submit_then_watch_default(&sudo_call, &sudo)
.await?;
// Below we use the low level API to replicate the `wait_for_in_block` behaviour // Below we use the low level API to replicate the `wait_for_in_block` behaviour
// which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237. // which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237.
while let Some(status) = tx.next().await { while let Some(status) = tx.next().await {
match status? { match status? {
TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => { TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => {
let _result = tx_in_block.wait_for_success().await?; let _result = tx_in_block.wait_for_success().await?;
info!("In block: {:#?}", tx_in_block.block_hash()); info!("In block: {:#?}", tx_in_block.block_hash());
}, },
TxStatus::Error { message } TxStatus::Error { message }
| TxStatus::Invalid { message } | TxStatus::Invalid { message }
| TxStatus::Dropped { message } => { | TxStatus::Dropped { message } => {
return Err(anyhow::format_err!("Error submitting tx: {message}")); return Err(anyhow::format_err!("Error submitting tx: {message}"));
}, },
_ => continue, _ => continue,
} }
} }
Ok(()) Ok(())
} }
pub fn para_id(&self) -> u32 { pub fn para_id(&self) -> u32 {
self.para_id self.para_id
} }
pub fn unique_id(&self) -> &str { pub fn unique_id(&self) -> &str {
self.unique_id.as_str() self.unique_id.as_str()
} }
pub fn chain_id(&self) -> Option<&str> { pub fn chain_id(&self) -> Option<&str> {
self.chain_id.as_deref() self.chain_id.as_deref()
} }
pub fn collators(&self) -> Vec<&NetworkNode> { pub fn collators(&self) -> Vec<&NetworkNode> {
self.collators.iter().collect() self.collators.iter().collect()
} }
pub fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> { pub fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> {
self.bootnodes_addresses.iter().collect() self.bootnodes_addresses.iter().collect()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use super::*; use super::*;
#[test] #[test]
fn create_with_is_works() { fn create_with_is_works() {
let para = Parachain::new(100, "100"); let para = Parachain::new(100, "100");
// only para_id and unique_id should be set // only para_id and unique_id should be set
assert_eq!(para.para_id, 100); assert_eq!(para.para_id, 100);
assert_eq!(para.unique_id, "100"); assert_eq!(para.unique_id, "100");
assert_eq!(para.chain_id, None); assert_eq!(para.chain_id, None);
assert_eq!(para.chain, None); assert_eq!(para.chain, None);
assert_eq!(para.chain_spec_path, None); assert_eq!(para.chain_spec_path, None);
} }
#[test] #[test]
fn create_with_chain_spec_works() { fn create_with_chain_spec_works() {
let para = Parachain::with_chain_spec(100, "100", "rococo-local", "/tmp/rococo-local.json"); let para = Parachain::with_chain_spec(100, "100", "rococo-local", "/tmp/rococo-local.json");
assert_eq!(para.para_id, 100); assert_eq!(para.para_id, 100);
assert_eq!(para.unique_id, "100"); assert_eq!(para.unique_id, "100");
assert_eq!(para.chain_id, Some("rococo-local".to_string())); assert_eq!(para.chain_id, Some("rococo-local".to_string()));
assert_eq!(para.chain, None); assert_eq!(para.chain, None);
assert_eq!( assert_eq!(para.chain_spec_path, Some(PathBuf::from("/tmp/rococo-local.json")));
para.chain_spec_path, }
Some(PathBuf::from("/tmp/rococo-local.json"))
);
}
#[tokio::test] #[tokio::test]
async fn create_with_para_spec_works() { async fn create_with_para_spec_works() {
use configuration::ParachainConfigBuilder; use configuration::ParachainConfigBuilder;
use crate::network_spec::teyrchain::TeyrchainSpec; use crate::network_spec::teyrchain::TeyrchainSpec;
let bootnode_addresses = vec!["/ip4/10.41.122.55/tcp/45421"]; let bootnode_addresses = vec!["/ip4/10.41.122.55/tcp/45421"];
let para_config = ParachainConfigBuilder::new(Default::default()) let para_config = ParachainConfigBuilder::new(Default::default())
.with_id(100) .with_id(100)
.cumulus_based(false) .cumulus_based(false)
.with_default_command("adder-collator") .with_default_command("adder-collator")
.with_raw_bootnodes_addresses(bootnode_addresses.clone()) .with_raw_bootnodes_addresses(bootnode_addresses.clone())
.with_collator(|c| c.with_name("col")) .with_collator(|c| c.with_name("col"))
.build() .build()
.unwrap(); .unwrap();
let para_spec = let para_spec =
TeyrchainSpec::from_config(&para_config, "rococo-local".try_into().unwrap()).unwrap(); TeyrchainSpec::from_config(&para_config, "rococo-local".try_into().unwrap()).unwrap();
let fs = support::fs::in_memory::InMemoryFileSystem::new(HashMap::default()); let fs = support::fs::in_memory::InMemoryFileSystem::new(HashMap::default());
let scoped_fs = ScopedFilesystem { let scoped_fs = ScopedFilesystem { fs: &fs, base_dir: "/tmp/some" };
fs: &fs,
base_dir: "/tmp/some",
};
let files = vec![TransferedFile::new( let files =
PathBuf::from("/tmp/some"), vec![TransferedFile::new(PathBuf::from("/tmp/some"), PathBuf::from("/tmp/some"))];
PathBuf::from("/tmp/some"), let para = Parachain::from_spec(&para_spec, &files, &scoped_fs).await.unwrap();
)]; println!("{para:#?}");
let para = Parachain::from_spec(&para_spec, &files, &scoped_fs) assert_eq!(para.para_id, 100);
.await assert_eq!(para.unique_id, "100");
.unwrap(); assert_eq!(para.chain_id, None);
println!("{para:#?}"); assert_eq!(para.chain, None);
assert_eq!(para.para_id, 100); // one file should be added.
assert_eq!(para.unique_id, "100"); assert_eq!(para.files_to_inject.len(), 1);
assert_eq!(para.chain_id, None); assert_eq!(
assert_eq!(para.chain, None); para.bootnodes_addresses().iter().map(|addr| addr.to_string()).collect::<Vec<_>>(),
// one file should be added. bootnode_addresses
assert_eq!(para.files_to_inject.len(), 1); );
assert_eq!( }
para.bootnodes_addresses()
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>(),
bootnode_addresses
);
}
} }
@@ -5,58 +5,56 @@ use reqwest::Url;
#[async_trait] #[async_trait]
pub trait MetricsHelper { pub trait MetricsHelper {
async fn metric(&self, metric_name: &str) -> Result<f64, anyhow::Error>; async fn metric(&self, metric_name: &str) -> Result<f64, anyhow::Error>;
async fn metric_with_url( async fn metric_with_url(
metric: impl AsRef<str> + Send, metric: impl AsRef<str> + Send,
endpoint: impl Into<Url> + Send, endpoint: impl Into<Url> + Send,
) -> Result<f64, anyhow::Error>; ) -> Result<f64, anyhow::Error>;
} }
pub struct Metrics { pub struct Metrics {
endpoint: Url, endpoint: Url,
} }
impl Metrics { impl Metrics {
fn new(endpoint: impl Into<Url>) -> Self { fn new(endpoint: impl Into<Url>) -> Self {
Self { Self { endpoint: endpoint.into() }
endpoint: endpoint.into(), }
}
}
async fn fetch_metrics( async fn fetch_metrics(
endpoint: impl AsRef<str>, endpoint: impl AsRef<str>,
) -> Result<HashMap<String, f64>, anyhow::Error> { ) -> Result<HashMap<String, f64>, anyhow::Error> {
let response = reqwest::get(endpoint.as_ref()).await?; let response = reqwest::get(endpoint.as_ref()).await?;
Ok(prom_metrics_parser::parse(&response.text().await?)?) Ok(prom_metrics_parser::parse(&response.text().await?)?)
} }
fn get_metric( fn get_metric(
metrics_map: HashMap<String, f64>, metrics_map: HashMap<String, f64>,
metric_name: &str, metric_name: &str,
) -> Result<f64, anyhow::Error> { ) -> Result<f64, anyhow::Error> {
let treat_not_found_as_zero = true; let treat_not_found_as_zero = true;
if let Some(val) = metrics_map.get(metric_name) { if let Some(val) = metrics_map.get(metric_name) {
Ok(*val) Ok(*val)
} else if treat_not_found_as_zero { } else if treat_not_found_as_zero {
Ok(0_f64) Ok(0_f64)
} else { } else {
Err(anyhow::anyhow!("MetricNotFound: {metric_name}")) Err(anyhow::anyhow!("MetricNotFound: {metric_name}"))
} }
} }
} }
#[async_trait] #[async_trait]
impl MetricsHelper for Metrics { impl MetricsHelper for Metrics {
async fn metric(&self, metric_name: &str) -> Result<f64, anyhow::Error> { async fn metric(&self, metric_name: &str) -> Result<f64, anyhow::Error> {
let metrics_map = Metrics::fetch_metrics(self.endpoint.as_str()).await?; let metrics_map = Metrics::fetch_metrics(self.endpoint.as_str()).await?;
Metrics::get_metric(metrics_map, metric_name) Metrics::get_metric(metrics_map, metric_name)
} }
async fn metric_with_url( async fn metric_with_url(
metric_name: impl AsRef<str> + Send, metric_name: impl AsRef<str> + Send,
endpoint: impl Into<Url> + Send, endpoint: impl Into<Url> + Send,
) -> Result<f64, anyhow::Error> { ) -> Result<f64, anyhow::Error> {
let metrics_map = Metrics::fetch_metrics(endpoint.into()).await?; let metrics_map = Metrics::fetch_metrics(endpoint.into()).await?;
Metrics::get_metric(metrics_map, metric_name.as_ref()) Metrics::get_metric(metrics_map, metric_name.as_ref())
} }
} }
@@ -6,29 +6,29 @@ use tracing::trace;
use crate::network::node::NetworkNode; use crate::network::node::NetworkNode;
pub(crate) async fn verify_nodes(nodes: &[&NetworkNode]) -> Result<(), anyhow::Error> { pub(crate) async fn verify_nodes(nodes: &[&NetworkNode]) -> Result<(), anyhow::Error> {
timeout(Duration::from_secs(90), check_nodes(nodes)) timeout(Duration::from_secs(90), check_nodes(nodes))
.await .await
.map_err(|_| anyhow::anyhow!("one or more nodes are not ready!")) .map_err(|_| anyhow::anyhow!("one or more nodes are not ready!"))
} }
// TODO: we should inject in someway the logic to make the request // TODO: we should inject in someway the logic to make the request
// in order to allow us to `mock` and easily test this. // in order to allow us to `mock` and easily test this.
// maybe moved to the provider with a NodeStatus, and some helpers like wait_running, wait_ready, etc... ? to be discussed // maybe moved to the provider with a NodeStatus, and some helpers like wait_running, wait_ready, etc... ? to be discussed
async fn check_nodes(nodes: &[&NetworkNode]) { async fn check_nodes(nodes: &[&NetworkNode]) {
loop { loop {
let tasks: Vec<_> = nodes let tasks: Vec<_> = nodes
.iter() .iter()
.map(|node| { .map(|node| {
trace!("🔎 checking node: {} ", node.name); trace!("🔎 checking node: {} ", node.name);
reqwest::get(node.prometheus_uri.clone()) reqwest::get(node.prometheus_uri.clone())
}) })
.collect(); .collect();
let all_ready = futures::future::try_join_all(tasks).await; let all_ready = futures::future::try_join_all(tasks).await;
if all_ready.is_ok() { if all_ready.is_ok() {
return; return;
} }
tokio::time::sleep(Duration::from_millis(1000)).await; tokio::time::sleep(Duration::from_millis(1000)).await;
} }
} }
@@ -1,6 +1,6 @@
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{hash_map::Entry, HashMap},
sync::Arc, sync::Arc,
}; };
use configuration::{GlobalSettings, HrmpChannelConfig, NetworkConfig}; use configuration::{GlobalSettings, HrmpChannelConfig, NetworkConfig};
@@ -20,311 +20,289 @@ use self::{node::NodeSpec, relaychain::RelaychainSpec, teyrchain::TeyrchainSpec}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkSpec { pub struct NetworkSpec {
/// Relaychain configuration. /// Relaychain configuration.
pub(crate) relaychain: RelaychainSpec, pub(crate) relaychain: RelaychainSpec,
/// Parachains configurations. /// Parachains configurations.
pub(crate) parachains: Vec<TeyrchainSpec>, pub(crate) parachains: Vec<TeyrchainSpec>,
/// HRMP channels configurations. /// HRMP channels configurations.
pub(crate) hrmp_channels: Vec<HrmpChannelConfig>, pub(crate) hrmp_channels: Vec<HrmpChannelConfig>,
/// Global settings /// Global settings
pub(crate) global_settings: GlobalSettings, pub(crate) global_settings: GlobalSettings,
} }
impl NetworkSpec { impl NetworkSpec {
pub async fn from_config( pub async fn from_config(
network_config: &NetworkConfig, network_config: &NetworkConfig,
) -> Result<NetworkSpec, OrchestratorError> { ) -> Result<NetworkSpec, OrchestratorError> {
let mut errs = vec![]; let mut errs = vec![];
let relaychain = RelaychainSpec::from_config(network_config.relaychain())?; let relaychain = RelaychainSpec::from_config(network_config.relaychain())?;
let mut parachains = vec![]; let mut parachains = vec![];
// TODO: move to `fold` or map+fold // TODO: move to `fold` or map+fold
for para_config in network_config.parachains() { for para_config in network_config.parachains() {
match TeyrchainSpec::from_config(para_config, relaychain.chain.clone()) { match TeyrchainSpec::from_config(para_config, relaychain.chain.clone()) {
Ok(para) => parachains.push(para), Ok(para) => parachains.push(para),
Err(err) => errs.push(err), Err(err) => errs.push(err),
} }
} }
if errs.is_empty() { if errs.is_empty() {
Ok(NetworkSpec { Ok(NetworkSpec {
relaychain, relaychain,
parachains, parachains,
hrmp_channels: network_config hrmp_channels: network_config.hrmp_channels().into_iter().cloned().collect(),
.hrmp_channels() global_settings: network_config.global_settings().clone(),
.into_iter() })
.cloned() } else {
.collect(), let errs_str =
global_settings: network_config.global_settings().clone(), errs.into_iter().map(|e| e.to_string()).collect::<Vec<String>>().join("\n");
}) Err(OrchestratorError::InvalidConfig(errs_str))
} else { }
let errs_str = errs }
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n");
Err(OrchestratorError::InvalidConfig(errs_str))
}
}
pub async fn populate_nodes_available_args( pub async fn populate_nodes_available_args(
&mut self, &mut self,
ns: Arc<dyn ProviderNamespace + Send + Sync>, ns: Arc<dyn ProviderNamespace + Send + Sync>,
) -> Result<(), OrchestratorError> { ) -> Result<(), OrchestratorError> {
let network_nodes = self.collect_network_nodes(); let network_nodes = self.collect_network_nodes();
let mut image_command_to_nodes_mapping = let mut image_command_to_nodes_mapping =
Self::create_image_command_to_nodes_mapping(network_nodes); Self::create_image_command_to_nodes_mapping(network_nodes);
let available_args_outputs = let available_args_outputs =
Self::retrieve_all_nodes_available_args_output(ns, &image_command_to_nodes_mapping) Self::retrieve_all_nodes_available_args_output(ns, &image_command_to_nodes_mapping)
.await?; .await?;
Self::update_nodes_available_args_output( Self::update_nodes_available_args_output(
&mut image_command_to_nodes_mapping, &mut image_command_to_nodes_mapping,
available_args_outputs, available_args_outputs,
); );
Ok(()) Ok(())
} }
// //
pub async fn node_available_args_output( pub async fn node_available_args_output(
&self, &self,
node_spec: &NodeSpec, node_spec: &NodeSpec,
ns: Arc<dyn ProviderNamespace + Send + Sync>, ns: Arc<dyn ProviderNamespace + Send + Sync>,
) -> Result<String, ProviderError> { ) -> Result<String, ProviderError> {
// try to find a node that use the same combination of image/cmd // try to find a node that use the same combination of image/cmd
let cmp_fn = |ad_hoc: &&NodeSpec| -> bool { let cmp_fn = |ad_hoc: &&NodeSpec| -> bool {
ad_hoc.image == node_spec.image && ad_hoc.command == node_spec.command ad_hoc.image == node_spec.image && ad_hoc.command == node_spec.command
}; };
// check if we already had computed the args output for this cmd/[image] // check if we already had computed the args output for this cmd/[image]
let node = self.relaychain.nodes.iter().find(cmp_fn); let node = self.relaychain.nodes.iter().find(cmp_fn);
let node = if let Some(node) = node { let node = if let Some(node) = node {
Some(node) Some(node)
} else { } else {
let node = self let node = self.parachains.iter().find_map(|para| para.collators.iter().find(cmp_fn));
.parachains
.iter()
.find_map(|para| para.collators.iter().find(cmp_fn));
node node
}; };
let output = if let Some(node) = node { let output = if let Some(node) = node {
node.available_args_output.clone().expect(&format!( node.available_args_output
"args_output should be set for running nodes {THIS_IS_A_BUG}" .clone()
)) .expect(&format!("args_output should be set for running nodes {THIS_IS_A_BUG}"))
} else { } else {
// we need to compute the args output // we need to compute the args output
let image = node_spec let image = node_spec.image.as_ref().map(|image| image.as_str().to_string());
.image let command = node_spec.command.as_str().to_string();
.as_ref()
.map(|image| image.as_str().to_string());
let command = node_spec.command.as_str().to_string();
ns.get_node_available_args((command, image)).await? ns.get_node_available_args((command, image)).await?
}; };
Ok(output) Ok(output)
} }
pub fn relaychain(&self) -> &RelaychainSpec { pub fn relaychain(&self) -> &RelaychainSpec {
&self.relaychain &self.relaychain
} }
pub fn relaychain_mut(&mut self) -> &mut RelaychainSpec { pub fn relaychain_mut(&mut self) -> &mut RelaychainSpec {
&mut self.relaychain &mut self.relaychain
} }
pub fn parachains_iter(&self) -> impl Iterator<Item = &TeyrchainSpec> { pub fn parachains_iter(&self) -> impl Iterator<Item = &TeyrchainSpec> {
self.parachains.iter() self.parachains.iter()
} }
pub fn parachains_iter_mut(&mut self) -> impl Iterator<Item = &mut TeyrchainSpec> { pub fn parachains_iter_mut(&mut self) -> impl Iterator<Item = &mut TeyrchainSpec> {
self.parachains.iter_mut() self.parachains.iter_mut()
} }
pub fn set_global_settings(&mut self, global_settings: GlobalSettings) { pub fn set_global_settings(&mut self, global_settings: GlobalSettings) {
self.global_settings = global_settings; self.global_settings = global_settings;
} }
pub async fn build_parachain_artifacts<'a, T: FileSystem>( pub async fn build_parachain_artifacts<'a, T: FileSystem>(
&mut self, &mut self,
ns: DynNamespace, ns: DynNamespace,
scoped_fs: &ScopedFilesystem<'a, T>, scoped_fs: &ScopedFilesystem<'a, T>,
relaychain_id: &str, relaychain_id: &str,
base_dir_exists: bool, base_dir_exists: bool,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
for para in self.parachains.iter_mut() { for para in self.parachains.iter_mut() {
let chain_spec_raw_path = para.build_chain_spec(relaychain_id, &ns, scoped_fs).await?; let chain_spec_raw_path = para.build_chain_spec(relaychain_id, &ns, scoped_fs).await?;
trace!("creating dirs for {}", &para.unique_id); trace!("creating dirs for {}", &para.unique_id);
if base_dir_exists { if base_dir_exists {
scoped_fs.create_dir_all(&para.unique_id).await?; scoped_fs.create_dir_all(&para.unique_id).await?;
} else { } else {
scoped_fs.create_dir(&para.unique_id).await?; scoped_fs.create_dir(&para.unique_id).await?;
}; };
trace!("created dirs for {}", &para.unique_id); trace!("created dirs for {}", &para.unique_id);
// create wasm/state // create wasm/state
para.genesis_state para.genesis_state
.build( .build(
chain_spec_raw_path.clone(), chain_spec_raw_path.clone(),
format!("{}/genesis-state", para.unique_id), format!("{}/genesis-state", para.unique_id),
&ns, &ns,
scoped_fs, scoped_fs,
None, None,
) )
.await?; .await?;
debug!("parachain genesis state built!"); debug!("parachain genesis state built!");
para.genesis_wasm para.genesis_wasm
.build( .build(
chain_spec_raw_path, chain_spec_raw_path,
format!("{}/genesis-wasm", para.unique_id), format!("{}/genesis-wasm", para.unique_id),
&ns, &ns,
scoped_fs, scoped_fs,
None, None,
) )
.await?; .await?;
debug!("parachain genesis wasm built!"); debug!("parachain genesis wasm built!");
} }
Ok(()) Ok(())
} }
// collect mutable references to all nodes from relaychain and parachains // collect mutable references to all nodes from relaychain and parachains
fn collect_network_nodes(&mut self) -> Vec<&mut NodeSpec> { fn collect_network_nodes(&mut self) -> Vec<&mut NodeSpec> {
vec![ vec![
self.relaychain.nodes.iter_mut().collect::<Vec<_>>(), self.relaychain.nodes.iter_mut().collect::<Vec<_>>(),
self.parachains self.parachains.iter_mut().flat_map(|para| para.collators.iter_mut()).collect(),
.iter_mut() ]
.flat_map(|para| para.collators.iter_mut()) .into_iter()
.collect(), .flatten()
] .collect::<Vec<_>>()
.into_iter() }
.flatten()
.collect::<Vec<_>>()
}
// initialize the mapping of all possible node image/commands to corresponding nodes // initialize the mapping of all possible node image/commands to corresponding nodes
fn create_image_command_to_nodes_mapping( fn create_image_command_to_nodes_mapping(
network_nodes: Vec<&mut NodeSpec>, network_nodes: Vec<&mut NodeSpec>,
) -> HashMap<(Option<String>, String), Vec<&mut NodeSpec>> { ) -> HashMap<(Option<String>, String), Vec<&mut NodeSpec>> {
network_nodes.into_iter().fold( network_nodes.into_iter().fold(
HashMap::new(), HashMap::new(),
|mut acc: HashMap<(Option<String>, String), Vec<&mut node::NodeSpec>>, node| { |mut acc: HashMap<(Option<String>, String), Vec<&mut node::NodeSpec>>, node| {
// build mapping key using image and command if image is present or command only // build mapping key using image and command if image is present or command only
let key = node let key = node
.image .image
.as_ref() .as_ref()
.map(|image| { .map(|image| {
( (Some(image.as_str().to_string()), node.command.as_str().to_string())
Some(image.as_str().to_string()), })
node.command.as_str().to_string(), .unwrap_or_else(|| (None, node.command.as_str().to_string()));
)
})
.unwrap_or_else(|| (None, node.command.as_str().to_string()));
// append the node to the vector of nodes for this image/command tuple // append the node to the vector of nodes for this image/command tuple
if let Entry::Vacant(entry) = acc.entry(key.clone()) { if let Entry::Vacant(entry) = acc.entry(key.clone()) {
entry.insert(vec![node]); entry.insert(vec![node]);
} else { } else {
acc.get_mut(&key).unwrap().push(node); acc.get_mut(&key).unwrap().push(node);
} }
acc acc
}, },
) )
} }
async fn retrieve_all_nodes_available_args_output( async fn retrieve_all_nodes_available_args_output(
ns: Arc<dyn ProviderNamespace + Send + Sync>, ns: Arc<dyn ProviderNamespace + Send + Sync>,
image_command_to_nodes_mapping: &HashMap<(Option<String>, String), Vec<&mut NodeSpec>>, image_command_to_nodes_mapping: &HashMap<(Option<String>, String), Vec<&mut NodeSpec>>,
) -> Result<Vec<(Option<String>, String, String)>, OrchestratorError> { ) -> Result<Vec<(Option<String>, String, String)>, OrchestratorError> {
try_join_all( try_join_all(
image_command_to_nodes_mapping image_command_to_nodes_mapping
.keys() .keys()
.map(|(image, command)| { .map(|(image, command)| {
let ns = ns.clone(); let ns = ns.clone();
let image = image.clone(); let image = image.clone();
let command = command.clone(); let command = command.clone();
async move { async move {
// get node available args output from image/command // get node available args output from image/command
let available_args = ns let available_args =
.get_node_available_args((command.clone(), image.clone())) ns.get_node_available_args((command.clone(), image.clone())).await?;
.await?; debug!(
debug!( "retrieved available args for image: {:?}, command: {}",
"retrieved available args for image: {:?}, command: {}", image, command
image, command );
);
// map the result to include image and command // map the result to include image and command
Ok::<_, OrchestratorError>((image, command, available_args)) Ok::<_, OrchestratorError>((image, command, available_args))
} }
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
.await .await
} }
fn update_nodes_available_args_output( fn update_nodes_available_args_output(
image_command_to_nodes_mapping: &mut HashMap<(Option<String>, String), Vec<&mut NodeSpec>>, image_command_to_nodes_mapping: &mut HashMap<(Option<String>, String), Vec<&mut NodeSpec>>,
available_args_outputs: Vec<(Option<String>, String, String)>, available_args_outputs: Vec<(Option<String>, String, String)>,
) { ) {
for (image, command, available_args_output) in available_args_outputs { for (image, command, available_args_output) in available_args_outputs {
let nodes = image_command_to_nodes_mapping let nodes = image_command_to_nodes_mapping
.get_mut(&(image, command)) .get_mut(&(image, command))
.expect(&format!( .expect(&format!("node image/command key should exist {THIS_IS_A_BUG}"));
"node image/command key should exist {THIS_IS_A_BUG}"
));
for node in nodes { for node in nodes {
node.available_args_output = Some(available_args_output.clone()); node.available_args_output = Some(available_args_output.clone());
} }
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[tokio::test] #[tokio::test]
async fn small_network_config_get_spec() { async fn small_network_config_get_spec() {
use configuration::NetworkConfigBuilder; use configuration::NetworkConfigBuilder;
use super::*; use super::*;
let config = NetworkConfigBuilder::new() let config = NetworkConfigBuilder::new()
.with_relaychain(|r| { .with_relaychain(|r| {
r.with_chain("rococo-local") r.with_chain("rococo-local")
.with_default_command("polkadot") .with_default_command("polkadot")
.with_validator(|node| node.with_name("alice")) .with_validator(|node| node.with_name("alice"))
.with_fullnode(|node| node.with_name("bob").with_command("polkadot1")) .with_fullnode(|node| node.with_name("bob").with_command("polkadot1"))
}) })
.with_parachain(|p| { .with_parachain(|p| {
p.with_id(100) p.with_id(100)
.with_default_command("adder-collator") .with_default_command("adder-collator")
.with_collator(|c| c.with_name("collator1")) .with_collator(|c| c.with_name("collator1"))
}) })
.build() .build()
.unwrap(); .unwrap();
let network_spec = NetworkSpec::from_config(&config).await.unwrap(); let network_spec = NetworkSpec::from_config(&config).await.unwrap();
let alice = network_spec.relaychain.nodes.first().unwrap(); let alice = network_spec.relaychain.nodes.first().unwrap();
let bob = network_spec.relaychain.nodes.get(1).unwrap(); let bob = network_spec.relaychain.nodes.get(1).unwrap();
assert_eq!(alice.command.as_str(), "polkadot"); assert_eq!(alice.command.as_str(), "polkadot");
assert_eq!(bob.command.as_str(), "polkadot1"); assert_eq!(bob.command.as_str(), "polkadot1");
assert!(alice.is_validator); assert!(alice.is_validator);
assert!(!bob.is_validator); assert!(!bob.is_validator);
// paras // paras
assert_eq!(network_spec.parachains.len(), 1); assert_eq!(network_spec.parachains.len(), 1);
let para_100 = network_spec.parachains.first().unwrap(); let para_100 = network_spec.parachains.first().unwrap();
assert_eq!(para_100.id, 100); assert_eq!(para_100.id, 100);
} }
} }
@@ -1,9 +1,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use configuration::shared::{ use configuration::shared::{
node::{EnvVar, NodeConfig}, node::{EnvVar, NodeConfig},
resources::Resources, resources::Resources,
types::{Arg, AssetLocation, Command, Image}, types::{Arg, AssetLocation, Command, Image},
}; };
use multiaddr::Multiaddr; use multiaddr::Multiaddr;
use provider::types::Port; use provider::types::Port;
@@ -11,39 +11,39 @@ use serde::{Deserialize, Serialize};
use support::constants::THIS_IS_A_BUG; use support::constants::THIS_IS_A_BUG;
use crate::{ use crate::{
errors::OrchestratorError, errors::OrchestratorError,
generators, generators,
network::AddNodeOptions, network::AddNodeOptions,
shared::{ shared::{
macros, macros,
types::{ChainDefaultContext, NodeAccount, NodeAccounts, ParkedPort}, types::{ChainDefaultContext, NodeAccount, NodeAccounts, ParkedPort},
}, },
AddCollatorOptions, AddCollatorOptions,
}; };
macros::create_add_options!(AddNodeSpecOpts { macros::create_add_options!(AddNodeSpecOpts {
override_eth_key: Option<String> override_eth_key: Option<String>
}); });
macro_rules! impl_from_for_add_node_opts { macro_rules! impl_from_for_add_node_opts {
($struct:ident) => { ($struct:ident) => {
impl From<$struct> for AddNodeSpecOpts { impl From<$struct> for AddNodeSpecOpts {
fn from(value: $struct) -> Self { fn from(value: $struct) -> Self {
Self { Self {
image: value.image, image: value.image,
command: value.command, command: value.command,
subcommand: value.subcommand, subcommand: value.subcommand,
args: value.args, args: value.args,
env: value.env, env: value.env,
is_validator: value.is_validator, is_validator: value.is_validator,
rpc_port: value.rpc_port, rpc_port: value.rpc_port,
prometheus_port: value.prometheus_port, prometheus_port: value.prometheus_port,
p2p_port: value.p2p_port, p2p_port: value.p2p_port,
override_eth_key: value.override_eth_key, override_eth_key: value.override_eth_key,
} }
} }
} }
}; };
} }
impl_from_for_add_node_opts!(AddNodeOptions); impl_from_for_add_node_opts!(AddNodeOptions);
@@ -52,305 +52,281 @@ impl_from_for_add_node_opts!(AddCollatorOptions);
/// A node configuration, with fine-grained configuration options. /// A node configuration, with fine-grained configuration options.
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeSpec { pub struct NodeSpec {
// Node name (should be unique or an index will be appended). // Node name (should be unique or an index will be appended).
pub(crate) name: String, pub(crate) name: String,
/// Node key, used for compute the p2p identity. /// Node key, used for compute the p2p identity.
pub(crate) key: String, pub(crate) key: String,
// libp2p local identity // libp2p local identity
pub(crate) peer_id: String, pub(crate) peer_id: String,
/// Accounts to be injected in the keystore. /// Accounts to be injected in the keystore.
pub(crate) accounts: NodeAccounts, pub(crate) accounts: NodeAccounts,
/// Image to run (only podman/k8s). Override the default. /// Image to run (only podman/k8s). Override the default.
pub(crate) image: Option<Image>, pub(crate) image: Option<Image>,
/// Command to run the node. Override the default. /// Command to run the node. Override the default.
pub(crate) command: Command, pub(crate) command: Command,
/// Optional subcommand for the node. /// Optional subcommand for the node.
pub(crate) subcommand: Option<Command>, pub(crate) subcommand: Option<Command>,
/// Arguments to use for node. Appended to default. /// Arguments to use for node. Appended to default.
pub(crate) args: Vec<Arg>, pub(crate) args: Vec<Arg>,
// The help command output containing the available arguments. // The help command output containing the available arguments.
pub(crate) available_args_output: Option<String>, pub(crate) available_args_output: Option<String>,
/// Wether the node is a validator. /// Wether the node is a validator.
pub(crate) is_validator: bool, pub(crate) is_validator: bool,
/// Whether the node keys must be added to invulnerables. /// Whether the node keys must be added to invulnerables.
pub(crate) is_invulnerable: bool, pub(crate) is_invulnerable: bool,
/// Whether the node is a bootnode. /// Whether the node is a bootnode.
pub(crate) is_bootnode: bool, pub(crate) is_bootnode: bool,
/// Node initial balance present in genesis. /// Node initial balance present in genesis.
pub(crate) initial_balance: u128, pub(crate) initial_balance: u128,
/// Environment variables to set (inside pod for podman/k8s, inside shell for native). /// Environment variables to set (inside pod for podman/k8s, inside shell for native).
pub(crate) env: Vec<EnvVar>, pub(crate) env: Vec<EnvVar>,
/// List of node's bootnodes addresses to use. Appended to default. /// List of node's bootnodes addresses to use. Appended to default.
pub(crate) bootnodes_addresses: Vec<Multiaddr>, pub(crate) bootnodes_addresses: Vec<Multiaddr>,
/// Default resources. Override the default. /// Default resources. Override the default.
pub(crate) resources: Option<Resources>, pub(crate) resources: Option<Resources>,
/// Websocket port to use. /// Websocket port to use.
pub(crate) ws_port: ParkedPort, pub(crate) ws_port: ParkedPort,
/// RPC port to use. /// RPC port to use.
pub(crate) rpc_port: ParkedPort, pub(crate) rpc_port: ParkedPort,
/// Prometheus port to use. /// Prometheus port to use.
pub(crate) prometheus_port: ParkedPort, pub(crate) prometheus_port: ParkedPort,
/// P2P port to use. /// P2P port to use.
pub(crate) p2p_port: ParkedPort, pub(crate) p2p_port: ParkedPort,
/// libp2p cert hash to use with `webrtc` transport. /// libp2p cert hash to use with `webrtc` transport.
pub(crate) p2p_cert_hash: Option<String>, pub(crate) p2p_cert_hash: Option<String>,
/// Database snapshot. Override the default. /// Database snapshot. Override the default.
pub(crate) db_snapshot: Option<AssetLocation>, pub(crate) db_snapshot: Option<AssetLocation>,
/// P2P port to use by full node if this is the case /// P2P port to use by full node if this is the case
pub(crate) full_node_p2p_port: Option<ParkedPort>, pub(crate) full_node_p2p_port: Option<ParkedPort>,
/// Prometheus port to use by full node if this is the case /// Prometheus port to use by full node if this is the case
pub(crate) full_node_prometheus_port: Option<ParkedPort>, pub(crate) full_node_prometheus_port: Option<ParkedPort>,
/// Optionally specify a log path for the node /// Optionally specify a log path for the node
pub(crate) node_log_path: Option<PathBuf>, pub(crate) node_log_path: Option<PathBuf>,
/// Optionally specify a keystore path for the node /// Optionally specify a keystore path for the node
pub(crate) keystore_path: Option<PathBuf>, pub(crate) keystore_path: Option<PathBuf>,
/// Keystore key types to generate. /// Keystore key types to generate.
/// Supports short form (e.g., "audi") using predefined schemas, /// Supports short form (e.g., "audi") using predefined schemas,
/// or long form (e.g., "audi_sr") with explicit schema (sr, ed, ec). /// or long form (e.g., "audi_sr") with explicit schema (sr, ed, ec).
pub(crate) keystore_key_types: Vec<String>, pub(crate) keystore_key_types: Vec<String>,
} }
impl NodeSpec { impl NodeSpec {
pub fn from_config( pub fn from_config(
node_config: &NodeConfig, node_config: &NodeConfig,
chain_context: &ChainDefaultContext, chain_context: &ChainDefaultContext,
full_node_present: bool, full_node_present: bool,
evm_based: bool, evm_based: bool,
) -> Result<Self, OrchestratorError> { ) -> Result<Self, OrchestratorError> {
// Check first if the image is set at node level, then try with the default // Check first if the image is set at node level, then try with the default
let image = node_config.image().or(chain_context.default_image).cloned(); let image = node_config.image().or(chain_context.default_image).cloned();
// Check first if the command is set at node level, then try with the default // Check first if the command is set at node level, then try with the default
let command = if let Some(cmd) = node_config.command() { let command = if let Some(cmd) = node_config.command() {
cmd.clone() cmd.clone()
} else if let Some(cmd) = chain_context.default_command { } else if let Some(cmd) = chain_context.default_command {
cmd.clone() cmd.clone()
} else { } else {
return Err(OrchestratorError::InvalidNodeConfig( return Err(OrchestratorError::InvalidNodeConfig(
node_config.name().into(), node_config.name().into(),
"command".to_string(), "command".to_string(),
)); ));
}; };
let subcommand = node_config.subcommand().cloned(); let subcommand = node_config.subcommand().cloned();
// If `args` is set at `node` level use them // If `args` is set at `node` level use them
// otherwise use the default_args (can be empty). // otherwise use the default_args (can be empty).
let args: Vec<Arg> = if node_config.args().is_empty() { let args: Vec<Arg> = if node_config.args().is_empty() {
chain_context chain_context.default_args.iter().map(|x| x.to_owned().clone()).collect()
.default_args } else {
.iter() node_config.args().into_iter().cloned().collect()
.map(|x| x.to_owned().clone()) };
.collect()
} else {
node_config.args().into_iter().cloned().collect()
};
let (key, peer_id) = generators::generate_node_identity(node_config.name())?; let (key, peer_id) = generators::generate_node_identity(node_config.name())?;
let mut name = node_config.name().to_string(); let mut name = node_config.name().to_string();
let seed = format!("//{}{name}", name.remove(0).to_uppercase()); let seed = format!("//{}{name}", name.remove(0).to_uppercase());
let accounts = generators::generate_node_keys(&seed)?; let accounts = generators::generate_node_keys(&seed)?;
let mut accounts = NodeAccounts { seed, accounts }; let mut accounts = NodeAccounts { seed, accounts };
if evm_based { if evm_based {
if let Some(session_key) = node_config.override_eth_key() { if let Some(session_key) = node_config.override_eth_key() {
accounts accounts.accounts.insert("eth".into(), NodeAccount::new(session_key, session_key));
.accounts }
.insert("eth".into(), NodeAccount::new(session_key, session_key)); }
}
}
let db_snapshot = match (node_config.db_snapshot(), chain_context.default_db_snapshot) { let db_snapshot = match (node_config.db_snapshot(), chain_context.default_db_snapshot) {
(Some(db_snapshot), _) => Some(db_snapshot), (Some(db_snapshot), _) => Some(db_snapshot),
(None, Some(db_snapshot)) => Some(db_snapshot), (None, Some(db_snapshot)) => Some(db_snapshot),
_ => None, _ => None,
}; };
let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present { let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present {
( (
Some(generators::generate_node_port(None)?), Some(generators::generate_node_port(None)?),
Some(generators::generate_node_port(None)?), Some(generators::generate_node_port(None)?),
) )
} else { } else {
(None, None) (None, None)
}; };
Ok(Self { Ok(Self {
name: node_config.name().to_string(), name: node_config.name().to_string(),
key, key,
peer_id, peer_id,
image, image,
command, command,
subcommand, subcommand,
args, args,
available_args_output: None, available_args_output: None,
is_validator: node_config.is_validator(), is_validator: node_config.is_validator(),
is_invulnerable: node_config.is_invulnerable(), is_invulnerable: node_config.is_invulnerable(),
is_bootnode: node_config.is_bootnode(), is_bootnode: node_config.is_bootnode(),
initial_balance: node_config.initial_balance(), initial_balance: node_config.initial_balance(),
env: node_config.env().into_iter().cloned().collect(), env: node_config.env().into_iter().cloned().collect(),
bootnodes_addresses: node_config bootnodes_addresses: node_config.bootnodes_addresses().into_iter().cloned().collect(),
.bootnodes_addresses() resources: node_config.resources().cloned(),
.into_iter() p2p_cert_hash: node_config.p2p_cert_hash().map(str::to_string),
.cloned() db_snapshot: db_snapshot.cloned(),
.collect(), accounts,
resources: node_config.resources().cloned(), ws_port: generators::generate_node_port(node_config.ws_port())?,
p2p_cert_hash: node_config.p2p_cert_hash().map(str::to_string), rpc_port: generators::generate_node_port(node_config.rpc_port())?,
db_snapshot: db_snapshot.cloned(), prometheus_port: generators::generate_node_port(node_config.prometheus_port())?,
accounts, p2p_port: generators::generate_node_port(node_config.p2p_port())?,
ws_port: generators::generate_node_port(node_config.ws_port())?, full_node_p2p_port,
rpc_port: generators::generate_node_port(node_config.rpc_port())?, full_node_prometheus_port,
prometheus_port: generators::generate_node_port(node_config.prometheus_port())?, node_log_path: node_config.node_log_path().cloned(),
p2p_port: generators::generate_node_port(node_config.p2p_port())?, keystore_path: node_config.keystore_path().cloned(),
full_node_p2p_port, keystore_key_types: node_config
full_node_prometheus_port, .keystore_key_types()
node_log_path: node_config.node_log_path().cloned(), .into_iter()
keystore_path: node_config.keystore_path().cloned(), .map(str::to_string)
keystore_key_types: node_config .collect(),
.keystore_key_types() })
.into_iter() }
.map(str::to_string)
.collect(),
})
}
pub fn from_ad_hoc( pub fn from_ad_hoc(
name: impl Into<String>, name: impl Into<String>,
options: AddNodeSpecOpts, options: AddNodeSpecOpts,
chain_context: &ChainDefaultContext, chain_context: &ChainDefaultContext,
full_node_present: bool, full_node_present: bool,
evm_based: bool, evm_based: bool,
) -> Result<Self, OrchestratorError> { ) -> Result<Self, OrchestratorError> {
// Check first if the image is set at node level, then try with the default // Check first if the image is set at node level, then try with the default
let image = if let Some(img) = options.image { let image = if let Some(img) = options.image {
Some(img.clone()) Some(img.clone())
} else { } else {
chain_context.default_image.cloned() chain_context.default_image.cloned()
}; };
let name = name.into(); let name = name.into();
// Check first if the command is set at node level, then try with the default // Check first if the command is set at node level, then try with the default
let command = if let Some(cmd) = options.command { let command = if let Some(cmd) = options.command {
cmd.clone() cmd.clone()
} else if let Some(cmd) = chain_context.default_command { } else if let Some(cmd) = chain_context.default_command {
cmd.clone() cmd.clone()
} else { } else {
return Err(OrchestratorError::InvalidNodeConfig( return Err(OrchestratorError::InvalidNodeConfig(name, "command".to_string()));
name, };
"command".to_string(),
));
};
let subcommand = options.subcommand.clone(); let subcommand = options.subcommand.clone();
// If `args` is set at `node` level use them // If `args` is set at `node` level use them
// otherwise use the default_args (can be empty). // otherwise use the default_args (can be empty).
let args: Vec<Arg> = if options.args.is_empty() { let args: Vec<Arg> = if options.args.is_empty() {
chain_context chain_context.default_args.iter().map(|x| x.to_owned().clone()).collect()
.default_args } else {
.iter() options.args
.map(|x| x.to_owned().clone()) };
.collect()
} else {
options.args
};
let (key, peer_id) = generators::generate_node_identity(&name)?; let (key, peer_id) = generators::generate_node_identity(&name)?;
let mut name_capitalized = name.clone(); let mut name_capitalized = name.clone();
let seed = format!( let seed = format!("//{}{name_capitalized}", name_capitalized.remove(0).to_uppercase());
"//{}{name_capitalized}", let accounts = generators::generate_node_keys(&seed)?;
name_capitalized.remove(0).to_uppercase() let mut accounts = NodeAccounts { seed, accounts };
);
let accounts = generators::generate_node_keys(&seed)?;
let mut accounts = NodeAccounts { seed, accounts };
if evm_based { if evm_based {
if let Some(session_key) = options.override_eth_key.as_ref() { if let Some(session_key) = options.override_eth_key.as_ref() {
accounts accounts.accounts.insert("eth".into(), NodeAccount::new(session_key, session_key));
.accounts }
.insert("eth".into(), NodeAccount::new(session_key, session_key)); }
}
}
let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present { let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present {
( (
Some(generators::generate_node_port(None)?), Some(generators::generate_node_port(None)?),
Some(generators::generate_node_port(None)?), Some(generators::generate_node_port(None)?),
) )
} else { } else {
(None, None) (None, None)
}; };
// //
Ok(Self { Ok(Self {
name, name,
key, key,
peer_id, peer_id,
image, image,
command, command,
subcommand, subcommand,
args, args,
available_args_output: None, available_args_output: None,
is_validator: options.is_validator, is_validator: options.is_validator,
is_invulnerable: false, is_invulnerable: false,
is_bootnode: false, is_bootnode: false,
initial_balance: 0, initial_balance: 0,
env: options.env, env: options.env,
bootnodes_addresses: vec![], bootnodes_addresses: vec![],
resources: None, resources: None,
p2p_cert_hash: None, p2p_cert_hash: None,
db_snapshot: None, db_snapshot: None,
accounts, accounts,
// should be deprecated now! // should be deprecated now!
ws_port: generators::generate_node_port(None)?, ws_port: generators::generate_node_port(None)?,
rpc_port: generators::generate_node_port(options.rpc_port)?, rpc_port: generators::generate_node_port(options.rpc_port)?,
prometheus_port: generators::generate_node_port(options.prometheus_port)?, prometheus_port: generators::generate_node_port(options.prometheus_port)?,
p2p_port: generators::generate_node_port(options.p2p_port)?, p2p_port: generators::generate_node_port(options.p2p_port)?,
full_node_p2p_port, full_node_p2p_port,
full_node_prometheus_port, full_node_prometheus_port,
node_log_path: None, node_log_path: None,
keystore_path: None, keystore_path: None,
keystore_key_types: vec![], keystore_key_types: vec![],
}) })
} }
pub(crate) fn supports_arg(&self, arg: impl AsRef<str>) -> bool { pub(crate) fn supports_arg(&self, arg: impl AsRef<str>) -> bool {
self.available_args_output self.available_args_output
.as_ref() .as_ref()
.expect(&format!( .expect(&format!("available args should be present at this point {THIS_IS_A_BUG}"))
"available args should be present at this point {THIS_IS_A_BUG}" .contains(arg.as_ref())
)) }
.contains(arg.as_ref())
}
pub fn command(&self) -> &str { pub fn command(&self) -> &str {
self.command.as_str() self.command.as_str()
} }
} }
@@ -1,181 +1,181 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use configuration::{ use configuration::{
shared::{ shared::{
helpers::generate_unique_node_name_from_names, helpers::generate_unique_node_name_from_names,
resources::Resources, resources::Resources,
types::{Arg, AssetLocation, Chain, Command, Image}, types::{Arg, AssetLocation, Chain, Command, Image},
}, },
types::JsonOverrides, types::JsonOverrides,
NodeConfig, RelaychainConfig, NodeConfig, RelaychainConfig,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use support::replacer::apply_replacements; use support::replacer::apply_replacements;
use super::node::NodeSpec; use super::node::NodeSpec;
use crate::{ use crate::{
errors::OrchestratorError, errors::OrchestratorError,
generators::chain_spec::{ChainSpec, Context}, generators::chain_spec::{ChainSpec, Context},
shared::{constants::DEFAULT_CHAIN_SPEC_TPL_COMMAND, types::ChainDefaultContext}, shared::{constants::DEFAULT_CHAIN_SPEC_TPL_COMMAND, types::ChainDefaultContext},
}; };
/// A relaychain configuration spec /// A relaychain configuration spec
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelaychainSpec { pub struct RelaychainSpec {
/// Chain to use (e.g. rococo-local). /// Chain to use (e.g. rococo-local).
pub(crate) chain: Chain, pub(crate) chain: Chain,
/// Default command to run the node. Can be overridden on each node. /// Default command to run the node. Can be overridden on each node.
pub(crate) default_command: Option<Command>, pub(crate) default_command: Option<Command>,
/// Default image to use (only podman/k8s). Can be overridden on each node. /// Default image to use (only podman/k8s). Can be overridden on each node.
pub(crate) default_image: Option<Image>, pub(crate) default_image: Option<Image>,
/// Default resources. Can be overridden on each node. /// Default resources. Can be overridden on each node.
pub(crate) default_resources: Option<Resources>, pub(crate) default_resources: Option<Resources>,
/// Default database snapshot. Can be overridden on each node. /// Default database snapshot. Can be overridden on each node.
pub(crate) default_db_snapshot: Option<AssetLocation>, pub(crate) default_db_snapshot: Option<AssetLocation>,
/// Default arguments to use in nodes. Can be overridden on each node. /// Default arguments to use in nodes. Can be overridden on each node.
pub(crate) default_args: Vec<Arg>, pub(crate) default_args: Vec<Arg>,
// chain_spec_path: Option<AssetLocation>, // chain_spec_path: Option<AssetLocation>,
pub(crate) chain_spec: ChainSpec, pub(crate) chain_spec: ChainSpec,
/// Set the count of nominators to generator (used with PoS networks). /// Set the count of nominators to generator (used with PoS networks).
pub(crate) random_nominators_count: u32, pub(crate) random_nominators_count: u32,
/// Set the max nominators value (used with PoS networks). /// Set the max nominators value (used with PoS networks).
pub(crate) max_nominations: u8, pub(crate) max_nominations: u8,
/// Genesis overrides as JSON value. /// Genesis overrides as JSON value.
pub(crate) runtime_genesis_patch: Option<serde_json::Value>, pub(crate) runtime_genesis_patch: Option<serde_json::Value>,
/// Wasm override path/url to use. /// Wasm override path/url to use.
pub(crate) wasm_override: Option<AssetLocation>, pub(crate) wasm_override: Option<AssetLocation>,
/// Nodes to run. /// Nodes to run.
pub(crate) nodes: Vec<NodeSpec>, pub(crate) nodes: Vec<NodeSpec>,
/// Raw chain-spec override path, url or inline json to use. /// Raw chain-spec override path, url or inline json to use.
pub(crate) raw_spec_override: Option<JsonOverrides>, pub(crate) raw_spec_override: Option<JsonOverrides>,
} }
impl RelaychainSpec { impl RelaychainSpec {
pub fn from_config(config: &RelaychainConfig) -> Result<RelaychainSpec, OrchestratorError> { pub fn from_config(config: &RelaychainConfig) -> Result<RelaychainSpec, OrchestratorError> {
// Relaychain main command to use, in order: // Relaychain main command to use, in order:
// set as `default_command` or // set as `default_command` or
// use the command of the first node. // use the command of the first node.
// If non of those is set, return an error. // If non of those is set, return an error.
let main_cmd = config let main_cmd = config
.default_command() .default_command()
.or(config.nodes().first().and_then(|node| node.command())) .or(config.nodes().first().and_then(|node| node.command()))
.ok_or(OrchestratorError::InvalidConfig( .ok_or(OrchestratorError::InvalidConfig(
"Relaychain, either default_command or first node with a command needs to be set." "Relaychain, either default_command or first node with a command needs to be set."
.to_string(), .to_string(),
))?; ))?;
// TODO: internally we use image as String // TODO: internally we use image as String
let main_image = config let main_image = config
.default_image() .default_image()
.or(config.nodes().first().and_then(|node| node.image())) .or(config.nodes().first().and_then(|node| node.image()))
.map(|image| image.as_str().to_string()); .map(|image| image.as_str().to_string());
let replacements = HashMap::from([ let replacements = HashMap::from([
("disableBootnodes", "--disable-default-bootnode"), ("disableBootnodes", "--disable-default-bootnode"),
("mainCommand", main_cmd.as_str()), ("mainCommand", main_cmd.as_str()),
]); ]);
let tmpl = if let Some(tmpl) = config.chain_spec_command() { let tmpl = if let Some(tmpl) = config.chain_spec_command() {
apply_replacements(tmpl, &replacements) apply_replacements(tmpl, &replacements)
} else { } else {
apply_replacements(DEFAULT_CHAIN_SPEC_TPL_COMMAND, &replacements) apply_replacements(DEFAULT_CHAIN_SPEC_TPL_COMMAND, &replacements)
}; };
let chain_spec = ChainSpec::new(config.chain().as_str(), Context::Relay) let chain_spec = ChainSpec::new(config.chain().as_str(), Context::Relay)
.set_chain_name(config.chain().as_str()) .set_chain_name(config.chain().as_str())
.command( .command(
tmpl.as_str(), tmpl.as_str(),
config.chain_spec_command_is_local(), config.chain_spec_command_is_local(),
config.chain_spec_command_output_path(), config.chain_spec_command_output_path(),
) )
.image(main_image.clone()); .image(main_image.clone());
// Add asset location if present // Add asset location if present
let chain_spec = if let Some(chain_spec_path) = config.chain_spec_path() { let chain_spec = if let Some(chain_spec_path) = config.chain_spec_path() {
chain_spec.asset_location(chain_spec_path.clone()) chain_spec.asset_location(chain_spec_path.clone())
} else { } else {
chain_spec chain_spec
}; };
// add chain-spec runtime if present // add chain-spec runtime if present
let chain_spec = if let Some(chain_spec_runtime) = config.chain_spec_runtime() { let chain_spec = if let Some(chain_spec_runtime) = config.chain_spec_runtime() {
chain_spec.runtime(chain_spec_runtime.clone()) chain_spec.runtime(chain_spec_runtime.clone())
} else { } else {
chain_spec chain_spec
}; };
// build the `node_specs` // build the `node_specs`
let chain_context = ChainDefaultContext { let chain_context = ChainDefaultContext {
default_command: config.default_command(), default_command: config.default_command(),
default_image: config.default_image(), default_image: config.default_image(),
default_resources: config.default_resources(), default_resources: config.default_resources(),
default_db_snapshot: config.default_db_snapshot(), default_db_snapshot: config.default_db_snapshot(),
default_args: config.default_args(), default_args: config.default_args(),
}; };
let mut nodes: Vec<NodeConfig> = config.nodes().into_iter().cloned().collect(); let mut nodes: Vec<NodeConfig> = config.nodes().into_iter().cloned().collect();
nodes.extend( nodes.extend(
config config
.group_node_configs() .group_node_configs()
.into_iter() .into_iter()
.flat_map(|node_group| node_group.expand_group_configs()), .flat_map(|node_group| node_group.expand_group_configs()),
); );
let mut names = HashSet::new(); let mut names = HashSet::new();
let (nodes, mut errs) = nodes let (nodes, mut errs) = nodes
.iter() .iter()
.map(|node_config| NodeSpec::from_config(node_config, &chain_context, false, false)) .map(|node_config| NodeSpec::from_config(node_config, &chain_context, false, false))
.fold((vec![], vec![]), |(mut nodes, mut errs), result| { .fold((vec![], vec![]), |(mut nodes, mut errs), result| {
match result { match result {
Ok(mut node) => { Ok(mut node) => {
let unique_name = let unique_name =
generate_unique_node_name_from_names(node.name, &mut names); generate_unique_node_name_from_names(node.name, &mut names);
node.name = unique_name; node.name = unique_name;
nodes.push(node); nodes.push(node);
}, },
Err(err) => errs.push(err), Err(err) => errs.push(err),
} }
(nodes, errs) (nodes, errs)
}); });
if !errs.is_empty() { if !errs.is_empty() {
// TODO: merge errs, maybe return something like Result<Sometype, Vec<OrchestratorError>> // TODO: merge errs, maybe return something like Result<Sometype, Vec<OrchestratorError>>
return Err(errs.swap_remove(0)); return Err(errs.swap_remove(0));
} }
Ok(RelaychainSpec { Ok(RelaychainSpec {
chain: config.chain().clone(), chain: config.chain().clone(),
default_command: config.default_command().cloned(), default_command: config.default_command().cloned(),
default_image: config.default_image().cloned(), default_image: config.default_image().cloned(),
default_resources: config.default_resources().cloned(), default_resources: config.default_resources().cloned(),
default_db_snapshot: config.default_db_snapshot().cloned(), default_db_snapshot: config.default_db_snapshot().cloned(),
wasm_override: config.wasm_override().cloned(), wasm_override: config.wasm_override().cloned(),
default_args: config.default_args().into_iter().cloned().collect(), default_args: config.default_args().into_iter().cloned().collect(),
chain_spec, chain_spec,
random_nominators_count: config.random_nominators_count().unwrap_or(0), random_nominators_count: config.random_nominators_count().unwrap_or(0),
max_nominations: config.max_nominations().unwrap_or(24), max_nominations: config.max_nominations().unwrap_or(24),
runtime_genesis_patch: config.runtime_genesis_patch().cloned(), runtime_genesis_patch: config.runtime_genesis_patch().cloned(),
nodes, nodes,
raw_spec_override: config.raw_spec_override().cloned(), raw_spec_override: config.raw_spec_override().cloned(),
}) })
} }
pub fn chain_spec(&self) -> &ChainSpec { pub fn chain_spec(&self) -> &ChainSpec {
&self.chain_spec &self.chain_spec
} }
pub fn chain_spec_mut(&mut self) -> &mut ChainSpec { pub fn chain_spec_mut(&mut self) -> &mut ChainSpec {
&mut self.chain_spec &mut self.chain_spec
} }
} }
@@ -1,12 +1,12 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::PathBuf, path::PathBuf,
}; };
use configuration::{ use configuration::{
shared::{helpers::generate_unique_node_name_from_names, resources::Resources}, shared::{helpers::generate_unique_node_name_from_names, resources::Resources},
types::{Arg, AssetLocation, Chain, Command, Image, JsonOverrides}, types::{Arg, AssetLocation, Chain, Command, Image, JsonOverrides},
NodeConfig, ParachainConfig, RegistrationStrategy, NodeConfig, ParachainConfig, RegistrationStrategy,
}; };
use provider::DynNamespace; use provider::DynNamespace;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -15,372 +15,348 @@ use tracing::debug;
use super::node::NodeSpec; use super::node::NodeSpec;
use crate::{ use crate::{
errors::OrchestratorError, errors::OrchestratorError,
generators::{ generators::{
chain_spec::{ChainSpec, Context, ParaGenesisConfig}, chain_spec::{ChainSpec, Context, ParaGenesisConfig},
para_artifact::*, para_artifact::*,
}, },
shared::{constants::DEFAULT_CHAIN_SPEC_TPL_COMMAND, types::ChainDefaultContext}, shared::{constants::DEFAULT_CHAIN_SPEC_TPL_COMMAND, types::ChainDefaultContext},
ScopedFilesystem, ScopedFilesystem,
}; };
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeyrchainSpec { pub struct TeyrchainSpec {
// `name` of the parachain (used in some corner cases) // `name` of the parachain (used in some corner cases)
// name: Option<Chain>, // name: Option<Chain>,
/// Parachain id /// Parachain id
pub(crate) id: u32, pub(crate) id: u32,
/// Unique id of the parachain, in the patter of <para_id>-<n> /// Unique id of the parachain, in the patter of <para_id>-<n>
/// where the suffix is only present if more than one parachain is set with the same id /// where the suffix is only present if more than one parachain is set with the same id
pub(crate) unique_id: String, pub(crate) unique_id: String,
/// Default command to run the node. Can be overridden on each node. /// Default command to run the node. Can be overridden on each node.
pub(crate) default_command: Option<Command>, pub(crate) default_command: Option<Command>,
/// Default image to use (only podman/k8s). Can be overridden on each node. /// Default image to use (only podman/k8s). Can be overridden on each node.
pub(crate) default_image: Option<Image>, pub(crate) default_image: Option<Image>,
/// Default resources. Can be overridden on each node. /// Default resources. Can be overridden on each node.
pub(crate) default_resources: Option<Resources>, pub(crate) default_resources: Option<Resources>,
/// Default database snapshot. Can be overridden on each node. /// Default database snapshot. Can be overridden on each node.
pub(crate) default_db_snapshot: Option<AssetLocation>, pub(crate) default_db_snapshot: Option<AssetLocation>,
/// Default arguments to use in nodes. Can be overridden on each node. /// Default arguments to use in nodes. Can be overridden on each node.
pub(crate) default_args: Vec<Arg>, pub(crate) default_args: Vec<Arg>,
/// Chain-spec, only needed by cumulus based paras /// Chain-spec, only needed by cumulus based paras
pub(crate) chain_spec: Option<ChainSpec>, pub(crate) chain_spec: Option<ChainSpec>,
/// Do not automatically assign a bootnode role if no nodes are marked as bootnodes. /// Do not automatically assign a bootnode role if no nodes are marked as bootnodes.
pub(crate) no_default_bootnodes: bool, pub(crate) no_default_bootnodes: bool,
/// Registration strategy to use /// Registration strategy to use
pub(crate) registration_strategy: RegistrationStrategy, pub(crate) registration_strategy: RegistrationStrategy,
/// Onboard as parachain or parathread /// Onboard as parachain or parathread
pub(crate) onboard_as_parachain: bool, pub(crate) onboard_as_parachain: bool,
/// Is the parachain cumulus-based /// Is the parachain cumulus-based
pub(crate) is_cumulus_based: bool, pub(crate) is_cumulus_based: bool,
/// Is the parachain evm-based /// Is the parachain evm-based
pub(crate) is_evm_based: bool, pub(crate) is_evm_based: bool,
/// Initial balance /// Initial balance
pub(crate) initial_balance: u128, pub(crate) initial_balance: u128,
/// Genesis state (head) to register the parachain /// Genesis state (head) to register the parachain
pub(crate) genesis_state: ParaArtifact, pub(crate) genesis_state: ParaArtifact,
/// Genesis WASM to register the parachain /// Genesis WASM to register the parachain
pub(crate) genesis_wasm: ParaArtifact, pub(crate) genesis_wasm: ParaArtifact,
/// Genesis overrides as JSON value. /// Genesis overrides as JSON value.
pub(crate) genesis_overrides: Option<serde_json::Value>, pub(crate) genesis_overrides: Option<serde_json::Value>,
/// Wasm override path/url to use. /// Wasm override path/url to use.
pub(crate) wasm_override: Option<AssetLocation>, pub(crate) wasm_override: Option<AssetLocation>,
/// Collators to spawn /// Collators to spawn
pub(crate) collators: Vec<NodeSpec>, pub(crate) collators: Vec<NodeSpec>,
/// Raw chain-spec override path, url or inline json to use. /// Raw chain-spec override path, url or inline json to use.
pub(crate) raw_spec_override: Option<JsonOverrides>, pub(crate) raw_spec_override: Option<JsonOverrides>,
/// Bootnodes addresses to use for the parachain nodes /// Bootnodes addresses to use for the parachain nodes
pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>, pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>,
} }
impl TeyrchainSpec { impl TeyrchainSpec {
pub fn from_config( pub fn from_config(
config: &ParachainConfig, config: &ParachainConfig,
relay_chain: Chain, relay_chain: Chain,
) -> Result<TeyrchainSpec, OrchestratorError> { ) -> Result<TeyrchainSpec, OrchestratorError> {
let main_cmd = if let Some(cmd) = config.default_command() { let main_cmd = if let Some(cmd) = config.default_command() {
cmd cmd
} else if let Some(first_node) = config.collators().first() { } else if let Some(first_node) = config.collators().first() {
let Some(cmd) = first_node.command() else { let Some(cmd) = first_node.command() else {
return Err(OrchestratorError::InvalidConfig(format!("Parachain {}, either default_command or command in the first node needs to be set.", config.id()))); return Err(OrchestratorError::InvalidConfig(format!("Parachain {}, either default_command or command in the first node needs to be set.", config.id())));
}; };
cmd cmd
} else { } else {
return Err(OrchestratorError::InvalidConfig(format!( return Err(OrchestratorError::InvalidConfig(format!(
"Parachain {}, without nodes and default_command isn't set.", "Parachain {}, without nodes and default_command isn't set.",
config.id() config.id()
))); )));
}; };
// TODO: internally we use image as String // TODO: internally we use image as String
let main_image = config let main_image = config
.default_image() .default_image()
.or(config.collators().first().and_then(|node| node.image())) .or(config.collators().first().and_then(|node| node.image()))
.map(|image| image.as_str().to_string()); .map(|image| image.as_str().to_string());
let chain_spec = if config.is_cumulus_based() { let chain_spec = if config.is_cumulus_based() {
// we need a chain-spec // we need a chain-spec
let chain_name = if let Some(chain_name) = config.chain() { let chain_name =
chain_name.as_str() if let Some(chain_name) = config.chain() { chain_name.as_str() } else { "" };
} else {
""
};
let chain_spec_builder = if chain_name.is_empty() { let chain_spec_builder = if chain_name.is_empty() {
// if the chain don't have name use the unique_id for the name of the file // if the chain don't have name use the unique_id for the name of the file
ChainSpec::new( ChainSpec::new(
config.unique_id().to_string(), config.unique_id().to_string(),
Context::Para { Context::Para { relay_chain, para_id: config.id() },
relay_chain, )
para_id: config.id(), } else {
}, let chain_spec_file_name = if config.unique_id().contains('-') {
) &format!("{}-{}", chain_name, config.unique_id())
} else { } else {
let chain_spec_file_name = if config.unique_id().contains('-') { chain_name
&format!("{}-{}", chain_name, config.unique_id()) };
} else { ChainSpec::new(
chain_name chain_spec_file_name,
}; Context::Para { relay_chain, para_id: config.id() },
ChainSpec::new( )
chain_spec_file_name, };
Context::Para { let chain_spec_builder = chain_spec_builder.set_chain_name(chain_name);
relay_chain,
para_id: config.id(),
},
)
};
let chain_spec_builder = chain_spec_builder.set_chain_name(chain_name);
let replacements = HashMap::from([ let replacements = HashMap::from([
("disableBootnodes", "--disable-default-bootnode"), ("disableBootnodes", "--disable-default-bootnode"),
("mainCommand", main_cmd.as_str()), ("mainCommand", main_cmd.as_str()),
]); ]);
let tmpl = if let Some(tmpl) = config.chain_spec_command() { let tmpl = if let Some(tmpl) = config.chain_spec_command() {
apply_replacements(tmpl, &replacements) apply_replacements(tmpl, &replacements)
} else { } else {
apply_replacements(DEFAULT_CHAIN_SPEC_TPL_COMMAND, &replacements) apply_replacements(DEFAULT_CHAIN_SPEC_TPL_COMMAND, &replacements)
}; };
let chain_spec = chain_spec_builder let chain_spec = chain_spec_builder
.command( .command(
tmpl.as_str(), tmpl.as_str(),
config.chain_spec_command_is_local(), config.chain_spec_command_is_local(),
config.chain_spec_command_output_path(), config.chain_spec_command_output_path(),
) )
.image(main_image.clone()); .image(main_image.clone());
let chain_spec = if let Some(chain_spec_path) = config.chain_spec_path() { let chain_spec = if let Some(chain_spec_path) = config.chain_spec_path() {
chain_spec.asset_location(chain_spec_path.clone()) chain_spec.asset_location(chain_spec_path.clone())
} else { } else {
chain_spec chain_spec
}; };
// add chain-spec runtime if present // add chain-spec runtime if present
let chain_spec = if let Some(chain_spec_runtime) = config.chain_spec_runtime() { let chain_spec = if let Some(chain_spec_runtime) = config.chain_spec_runtime() {
chain_spec.runtime(chain_spec_runtime.clone()) chain_spec.runtime(chain_spec_runtime.clone())
} else { } else {
chain_spec chain_spec
}; };
Some(chain_spec) Some(chain_spec)
} else { } else {
None None
}; };
// build the `node_specs` // build the `node_specs`
let chain_context = ChainDefaultContext { let chain_context = ChainDefaultContext {
default_command: config.default_command(), default_command: config.default_command(),
default_image: config.default_image(), default_image: config.default_image(),
default_resources: config.default_resources(), default_resources: config.default_resources(),
default_db_snapshot: config.default_db_snapshot(), default_db_snapshot: config.default_db_snapshot(),
default_args: config.default_args(), default_args: config.default_args(),
}; };
// We want to track the errors for all the nodes and report them ones // We want to track the errors for all the nodes and report them ones
let mut errs: Vec<OrchestratorError> = Default::default(); let mut errs: Vec<OrchestratorError> = Default::default();
let mut collators: Vec<NodeSpec> = Default::default(); let mut collators: Vec<NodeSpec> = Default::default();
let mut nodes: Vec<NodeConfig> = config.collators().into_iter().cloned().collect(); let mut nodes: Vec<NodeConfig> = config.collators().into_iter().cloned().collect();
nodes.extend( nodes.extend(
config config
.group_collators_configs() .group_collators_configs()
.into_iter() .into_iter()
.flat_map(|node_group| node_group.expand_group_configs()), .flat_map(|node_group| node_group.expand_group_configs()),
); );
let mut names = HashSet::new(); let mut names = HashSet::new();
for node_config in nodes { for node_config in nodes {
match NodeSpec::from_config(&node_config, &chain_context, true, config.is_evm_based()) { match NodeSpec::from_config(&node_config, &chain_context, true, config.is_evm_based()) {
Ok(mut node) => { Ok(mut node) => {
let unique_name = generate_unique_node_name_from_names(node.name, &mut names); let unique_name = generate_unique_node_name_from_names(node.name, &mut names);
node.name = unique_name; node.name = unique_name;
collators.push(node) collators.push(node)
}, },
Err(err) => errs.push(err), Err(err) => errs.push(err),
} }
} }
let genesis_state = if let Some(path) = config.genesis_state_path() { let genesis_state = if let Some(path) = config.genesis_state_path() {
ParaArtifact::new( ParaArtifact::new(
ParaArtifactType::State, ParaArtifactType::State,
ParaArtifactBuildOption::Path(path.to_string()), ParaArtifactBuildOption::Path(path.to_string()),
) )
} else { } else {
let cmd = if let Some(cmd) = config.genesis_state_generator() { let cmd =
cmd.cmd() if let Some(cmd) = config.genesis_state_generator() { cmd.cmd() } else { main_cmd };
} else { ParaArtifact::new(
main_cmd ParaArtifactType::State,
}; ParaArtifactBuildOption::Command(cmd.as_str().into()),
ParaArtifact::new( )
ParaArtifactType::State, .image(main_image.clone())
ParaArtifactBuildOption::Command(cmd.as_str().into()), };
)
.image(main_image.clone())
};
let genesis_wasm = if let Some(path) = config.genesis_wasm_path() { let genesis_wasm = if let Some(path) = config.genesis_wasm_path() {
ParaArtifact::new( ParaArtifact::new(
ParaArtifactType::Wasm, ParaArtifactType::Wasm,
ParaArtifactBuildOption::Path(path.to_string()), ParaArtifactBuildOption::Path(path.to_string()),
) )
} else { } else {
let cmd = if let Some(cmd) = config.genesis_wasm_generator() { let cmd = if let Some(cmd) = config.genesis_wasm_generator() {
cmd.as_str() cmd.as_str()
} else { } else {
main_cmd.as_str() main_cmd.as_str()
}; };
ParaArtifact::new( ParaArtifact::new(ParaArtifactType::Wasm, ParaArtifactBuildOption::Command(cmd.into()))
ParaArtifactType::Wasm, .image(main_image.clone())
ParaArtifactBuildOption::Command(cmd.into()), };
)
.image(main_image.clone())
};
let para_spec = TeyrchainSpec { let para_spec = TeyrchainSpec {
id: config.id(), id: config.id(),
// ensure unique id is set at this point, if not just set to the para_id // ensure unique id is set at this point, if not just set to the para_id
unique_id: if config.unique_id().is_empty() { unique_id: if config.unique_id().is_empty() {
config.id().to_string() config.id().to_string()
} else { } else {
config.unique_id().to_string() config.unique_id().to_string()
}, },
default_command: config.default_command().cloned(), default_command: config.default_command().cloned(),
default_image: config.default_image().cloned(), default_image: config.default_image().cloned(),
default_resources: config.default_resources().cloned(), default_resources: config.default_resources().cloned(),
default_db_snapshot: config.default_db_snapshot().cloned(), default_db_snapshot: config.default_db_snapshot().cloned(),
wasm_override: config.wasm_override().cloned(), wasm_override: config.wasm_override().cloned(),
default_args: config.default_args().into_iter().cloned().collect(), default_args: config.default_args().into_iter().cloned().collect(),
chain_spec, chain_spec,
no_default_bootnodes: config.no_default_bootnodes(), no_default_bootnodes: config.no_default_bootnodes(),
registration_strategy: config registration_strategy: config
.registration_strategy() .registration_strategy()
.unwrap_or(&RegistrationStrategy::InGenesis) .unwrap_or(&RegistrationStrategy::InGenesis)
.clone(), .clone(),
onboard_as_parachain: config.onboard_as_parachain(), onboard_as_parachain: config.onboard_as_parachain(),
is_cumulus_based: config.is_cumulus_based(), is_cumulus_based: config.is_cumulus_based(),
is_evm_based: config.is_evm_based(), is_evm_based: config.is_evm_based(),
initial_balance: config.initial_balance(), initial_balance: config.initial_balance(),
genesis_state, genesis_state,
genesis_wasm, genesis_wasm,
genesis_overrides: config.genesis_overrides().cloned(), genesis_overrides: config.genesis_overrides().cloned(),
collators, collators,
raw_spec_override: config.raw_spec_override().cloned(), raw_spec_override: config.raw_spec_override().cloned(),
bootnodes_addresses: config.bootnodes_addresses().into_iter().cloned().collect(), bootnodes_addresses: config.bootnodes_addresses().into_iter().cloned().collect(),
}; };
Ok(para_spec) Ok(para_spec)
} }
pub fn registration_strategy(&self) -> &RegistrationStrategy { pub fn registration_strategy(&self) -> &RegistrationStrategy {
&self.registration_strategy &self.registration_strategy
} }
pub fn get_genesis_config(&self) -> Result<ParaGenesisConfig<&PathBuf>, OrchestratorError> { pub fn get_genesis_config(&self) -> Result<ParaGenesisConfig<&PathBuf>, OrchestratorError> {
let genesis_config = ParaGenesisConfig { let genesis_config = ParaGenesisConfig {
state_path: self.genesis_state.artifact_path().ok_or( state_path: self.genesis_state.artifact_path().ok_or(
OrchestratorError::InvariantError( OrchestratorError::InvariantError(
"artifact path for state must be set at this point", "artifact path for state must be set at this point",
), ),
)?, )?,
wasm_path: self.genesis_wasm.artifact_path().ok_or( wasm_path: self.genesis_wasm.artifact_path().ok_or(
OrchestratorError::InvariantError( OrchestratorError::InvariantError(
"artifact path for wasm must be set at this point", "artifact path for wasm must be set at this point",
), ),
)?, )?,
id: self.id, id: self.id,
as_parachain: self.onboard_as_parachain, as_parachain: self.onboard_as_parachain,
}; };
Ok(genesis_config) Ok(genesis_config)
} }
pub fn id(&self) -> u32 { pub fn id(&self) -> u32 {
self.id self.id
} }
pub fn chain_spec(&self) -> Option<&ChainSpec> { pub fn chain_spec(&self) -> Option<&ChainSpec> {
self.chain_spec.as_ref() self.chain_spec.as_ref()
} }
pub fn chain_spec_mut(&mut self) -> Option<&mut ChainSpec> { pub fn chain_spec_mut(&mut self) -> Option<&mut ChainSpec> {
self.chain_spec.as_mut() self.chain_spec.as_mut()
} }
/// Build parachain chain-spec /// Build parachain chain-spec
/// ///
/// This function customize the chain-spec (if is possible) and build the raw version /// This function customize the chain-spec (if is possible) and build the raw version
/// of the chain-spec. /// of the chain-spec.
pub(crate) async fn build_chain_spec<'a, T>( pub(crate) async fn build_chain_spec<'a, T>(
&mut self, &mut self,
relay_chain_id: &str, relay_chain_id: &str,
ns: &DynNamespace, ns: &DynNamespace,
scoped_fs: &ScopedFilesystem<'a, T>, scoped_fs: &ScopedFilesystem<'a, T>,
) -> Result<Option<PathBuf>, anyhow::Error> ) -> Result<Option<PathBuf>, anyhow::Error>
where where
T: FileSystem, T: FileSystem,
{ {
let cloned = self.clone(); let cloned = self.clone();
let chain_spec_raw_path = if let Some(chain_spec) = self.chain_spec.as_mut() { let chain_spec_raw_path = if let Some(chain_spec) = self.chain_spec.as_mut() {
debug!("parachain chain-spec building!"); debug!("parachain chain-spec building!");
chain_spec.build(ns, scoped_fs).await?; chain_spec.build(ns, scoped_fs).await?;
debug!("parachain chain-spec built!"); debug!("parachain chain-spec built!");
chain_spec chain_spec.customize_para(&cloned, relay_chain_id, scoped_fs).await?;
.customize_para(&cloned, relay_chain_id, scoped_fs) debug!("parachain chain-spec customized!");
.await?; chain_spec.build_raw(ns, scoped_fs, Some(relay_chain_id.try_into()?)).await?;
debug!("parachain chain-spec customized!"); debug!("parachain chain-spec raw built!");
chain_spec
.build_raw(ns, scoped_fs, Some(relay_chain_id.try_into()?))
.await?;
debug!("parachain chain-spec raw built!");
// override wasm if needed // override wasm if needed
if let Some(ref wasm_override) = self.wasm_override { if let Some(ref wasm_override) = self.wasm_override {
chain_spec.override_code(scoped_fs, wasm_override).await?; chain_spec.override_code(scoped_fs, wasm_override).await?;
} }
// override raw spec if needed // override raw spec if needed
if let Some(ref raw_spec_override) = self.raw_spec_override { if let Some(ref raw_spec_override) = self.raw_spec_override {
chain_spec chain_spec.override_raw_spec(scoped_fs, raw_spec_override).await?;
.override_raw_spec(scoped_fs, raw_spec_override) }
.await?;
}
let chain_spec_raw_path = let chain_spec_raw_path = chain_spec.raw_path().ok_or(
chain_spec OrchestratorError::InvariantError("chain-spec raw path should be set now"),
.raw_path() )?;
.ok_or(OrchestratorError::InvariantError(
"chain-spec raw path should be set now",
))?;
Some(chain_spec_raw_path.to_path_buf()) Some(chain_spec_raw_path.to_path_buf())
} else { } else {
None None
}; };
Ok(chain_spec_raw_path) Ok(chain_spec_raw_path)
} }
/// Get the bootnodes addresses for the parachain spec /// Get the bootnodes addresses for the parachain spec
pub(crate) fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> { pub(crate) fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> {
self.bootnodes_addresses.iter().collect() self.bootnodes_addresses.iter().collect()
} }
} }
@@ -10,7 +10,7 @@ pub const RPC_HTTP_PORT: u16 = 9933;
pub const P2P_PORT: u16 = 30333; pub const P2P_PORT: u16 = 30333;
// default command template to build chain-spec // default command template to build chain-spec
pub const DEFAULT_CHAIN_SPEC_TPL_COMMAND: &str = pub const DEFAULT_CHAIN_SPEC_TPL_COMMAND: &str =
"{{mainCommand}} build-spec --chain {{chainName}} {{disableBootnodes}}"; "{{mainCommand}} build-spec --chain {{chainName}} {{disableBootnodes}}";
// interval to determine how often to run node liveness checks // interval to determine how often to run node liveness checks
pub const NODE_MONITORING_INTERVAL_SECONDS: u64 = 15; pub const NODE_MONITORING_INTERVAL_SECONDS: u64 = 15;
// how long to wait before a node is considered unresponsive // how long to wait before a node is considered unresponsive
@@ -1,13 +1,13 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
net::TcpListener, net::TcpListener,
path::PathBuf, path::PathBuf,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use configuration::shared::{ use configuration::shared::{
resources::Resources, resources::Resources,
types::{Arg, AssetLocation, Command, Image, Port}, types::{Arg, AssetLocation, Command, Image, Port},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -15,85 +15,75 @@ pub type Accounts = HashMap<String, NodeAccount>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NodeAccount { pub struct NodeAccount {
pub address: String, pub address: String,
pub public_key: String, pub public_key: String,
} }
impl NodeAccount { impl NodeAccount {
pub fn new(addr: impl Into<String>, pk: impl Into<String>) -> Self { pub fn new(addr: impl Into<String>, pk: impl Into<String>) -> Self {
Self { Self { address: addr.into(), public_key: pk.into() }
address: addr.into(), }
public_key: pk.into(),
}
}
} }
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct NodeAccounts { pub struct NodeAccounts {
pub seed: String, pub seed: String,
pub accounts: Accounts, pub accounts: Accounts,
} }
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct ParkedPort( pub struct ParkedPort(pub(crate) Port, #[serde(skip)] pub(crate) Arc<RwLock<Option<TcpListener>>>);
pub(crate) Port,
#[serde(skip)] pub(crate) Arc<RwLock<Option<TcpListener>>>,
);
impl ParkedPort { impl ParkedPort {
pub(crate) fn new(port: u16, listener: TcpListener) -> ParkedPort { pub(crate) fn new(port: u16, listener: TcpListener) -> ParkedPort {
let listener = Arc::new(RwLock::new(Some(listener))); let listener = Arc::new(RwLock::new(Some(listener)));
ParkedPort(port, listener) ParkedPort(port, listener)
} }
pub(crate) fn drop_listener(&self) { pub(crate) fn drop_listener(&self) {
// drop the listener will allow the running node to start listenen connections // drop the listener will allow the running node to start listenen connections
let mut l = self.1.write().unwrap(); let mut l = self.1.write().unwrap();
*l = None; *l = None;
} }
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ChainDefaultContext<'a> { pub struct ChainDefaultContext<'a> {
pub default_command: Option<&'a Command>, pub default_command: Option<&'a Command>,
pub default_image: Option<&'a Image>, pub default_image: Option<&'a Image>,
pub default_resources: Option<&'a Resources>, pub default_resources: Option<&'a Resources>,
pub default_db_snapshot: Option<&'a AssetLocation>, pub default_db_snapshot: Option<&'a AssetLocation>,
pub default_args: Vec<&'a Arg>, pub default_args: Vec<&'a Arg>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RegisterParachainOptions { pub struct RegisterParachainOptions {
pub id: u32, pub id: u32,
pub wasm_path: PathBuf, pub wasm_path: PathBuf,
pub state_path: PathBuf, pub state_path: PathBuf,
pub node_ws_url: String, pub node_ws_url: String,
pub onboard_as_para: bool, pub onboard_as_para: bool,
pub seed: Option<[u8; 32]>, pub seed: Option<[u8; 32]>,
pub finalization: bool, pub finalization: bool,
} }
pub struct RuntimeUpgradeOptions { pub struct RuntimeUpgradeOptions {
/// Location of the wasm file (could be either a local file or an url) /// Location of the wasm file (could be either a local file or an url)
pub wasm: AssetLocation, pub wasm: AssetLocation,
/// Name of the node to use as rpc endpoint /// Name of the node to use as rpc endpoint
pub node_name: Option<String>, pub node_name: Option<String>,
/// Seed to use to sign and submit (default to //Alice) /// Seed to use to sign and submit (default to //Alice)
pub seed: Option<[u8; 32]>, pub seed: Option<[u8; 32]>,
} }
impl RuntimeUpgradeOptions { impl RuntimeUpgradeOptions {
pub fn new(wasm: AssetLocation) -> Self { pub fn new(wasm: AssetLocation) -> Self {
Self { Self { wasm, node_name: None, seed: None }
wasm, }
node_name: None,
seed: None,
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ParachainGenesisArgs { pub struct ParachainGenesisArgs {
pub genesis_head: String, pub genesis_head: String,
pub validation_code: String, pub validation_code: String,
pub parachain: bool, pub parachain: bool,
} }
+222 -252
View File
@@ -3,303 +3,273 @@ use std::{collections::HashMap, path::PathBuf};
use anyhow::Context; use anyhow::Context;
use configuration::GlobalSettings; use configuration::GlobalSettings;
use provider::{ use provider::{
constants::{LOCALHOST, NODE_CONFIG_DIR, NODE_DATA_DIR, NODE_RELAY_DATA_DIR, P2P_PORT}, constants::{LOCALHOST, NODE_CONFIG_DIR, NODE_DATA_DIR, NODE_RELAY_DATA_DIR, P2P_PORT},
shared::helpers::running_in_ci, shared::helpers::running_in_ci,
types::{SpawnNodeOptions, TransferedFile}, types::{SpawnNodeOptions, TransferedFile},
DynNamespace, DynNamespace,
}; };
use support::{ use support::{
constants::THIS_IS_A_BUG, fs::FileSystem, replacer::apply_running_network_replacements, constants::THIS_IS_A_BUG, fs::FileSystem, replacer::apply_running_network_replacements,
}; };
use tracing::info; use tracing::info;
use crate::{ use crate::{
generators, generators,
network::node::NetworkNode, network::node::NetworkNode,
network_spec::{node::NodeSpec, teyrchain::TeyrchainSpec}, network_spec::{node::NodeSpec, teyrchain::TeyrchainSpec},
shared::constants::{FULL_NODE_PROMETHEUS_PORT, PROMETHEUS_PORT, RPC_PORT}, shared::constants::{FULL_NODE_PROMETHEUS_PORT, PROMETHEUS_PORT, RPC_PORT},
ScopedFilesystem, ZombieRole, ScopedFilesystem, ZombieRole,
}; };
#[derive(Clone)] #[derive(Clone)]
pub struct SpawnNodeCtx<'a, T: FileSystem> { pub struct SpawnNodeCtx<'a, T: FileSystem> {
/// Relaychain id, from the chain-spec (e.g rococo_local_testnet) /// Relaychain id, from the chain-spec (e.g rococo_local_testnet)
pub(crate) chain_id: &'a str, pub(crate) chain_id: &'a str,
// Parachain id, from the chain-spec (e.g local_testnet) // Parachain id, from the chain-spec (e.g local_testnet)
pub(crate) parachain_id: Option<&'a str>, pub(crate) parachain_id: Option<&'a str>,
/// Relaychain chain name (e.g rococo-local) /// Relaychain chain name (e.g rococo-local)
pub(crate) chain: &'a str, pub(crate) chain: &'a str,
/// Role of the node in the network /// Role of the node in the network
pub(crate) role: ZombieRole, pub(crate) role: ZombieRole,
/// Ref to the namespace /// Ref to the namespace
pub(crate) ns: &'a DynNamespace, pub(crate) ns: &'a DynNamespace,
/// Ref to an scoped filesystem (encapsulate fs actions inside the ns directory) /// Ref to an scoped filesystem (encapsulate fs actions inside the ns directory)
pub(crate) scoped_fs: &'a ScopedFilesystem<'a, T>, pub(crate) scoped_fs: &'a ScopedFilesystem<'a, T>,
/// Ref to a parachain (used to spawn collators) /// Ref to a parachain (used to spawn collators)
pub(crate) parachain: Option<&'a TeyrchainSpec>, pub(crate) parachain: Option<&'a TeyrchainSpec>,
/// The string representation of the bootnode address to pass to nodes /// The string representation of the bootnode address to pass to nodes
pub(crate) bootnodes_addr: &'a Vec<String>, pub(crate) bootnodes_addr: &'a Vec<String>,
/// Flag to wait node is ready or not /// Flag to wait node is ready or not
/// Ready state means we can query Prometheus internal server /// Ready state means we can query Prometheus internal server
pub(crate) wait_ready: bool, pub(crate) wait_ready: bool,
/// A json representation of the running nodes with their names as 'key' /// A json representation of the running nodes with their names as 'key'
pub(crate) nodes_by_name: serde_json::Value, pub(crate) nodes_by_name: serde_json::Value,
/// A ref to the global settings /// A ref to the global settings
pub(crate) global_settings: &'a GlobalSettings, pub(crate) global_settings: &'a GlobalSettings,
} }
pub async fn spawn_node<'a, T>( pub async fn spawn_node<'a, T>(
node: &NodeSpec, node: &NodeSpec,
mut files_to_inject: Vec<TransferedFile>, mut files_to_inject: Vec<TransferedFile>,
ctx: &SpawnNodeCtx<'a, T>, ctx: &SpawnNodeCtx<'a, T>,
) -> Result<NetworkNode, anyhow::Error> ) -> Result<NetworkNode, anyhow::Error>
where where
T: FileSystem, T: FileSystem,
{ {
let mut created_paths = vec![]; let mut created_paths = vec![];
// Create and inject the keystore IFF // Create and inject the keystore IFF
// - The node is validator in the relaychain // - The node is validator in the relaychain
// - The node is collator (encoded as validator) and the parachain is cumulus_based // - The node is collator (encoded as validator) and the parachain is cumulus_based
// (parachain_id) should be set then. // (parachain_id) should be set then.
if node.is_validator && (ctx.parachain.is_none() || ctx.parachain_id.is_some()) { if node.is_validator && (ctx.parachain.is_none() || ctx.parachain_id.is_some()) {
// Generate keystore for node // Generate keystore for node
let node_files_path = if let Some(para) = ctx.parachain { let node_files_path =
para.id.to_string() if let Some(para) = ctx.parachain { para.id.to_string() } else { node.name.clone() };
} else { let asset_hub_polkadot =
node.name.clone() ctx.parachain_id.map(|id| id.starts_with("asset-hub-polkadot")).unwrap_or_default();
}; let keystore_key_types = node.keystore_key_types.iter().map(String::as_str).collect();
let asset_hub_polkadot = ctx let key_filenames = generators::generate_node_keystore(
.parachain_id &node.accounts,
.map(|id| id.starts_with("asset-hub-polkadot")) &node_files_path,
.unwrap_or_default(); ctx.scoped_fs,
let keystore_key_types = node.keystore_key_types.iter().map(String::as_str).collect(); asset_hub_polkadot,
let key_filenames = generators::generate_node_keystore( keystore_key_types,
&node.accounts, )
&node_files_path, .await
ctx.scoped_fs, .unwrap();
asset_hub_polkadot,
keystore_key_types,
)
.await
.unwrap();
// Paths returned are relative to the base dir, we need to convert into // Paths returned are relative to the base dir, we need to convert into
// fullpaths to inject them in the nodes. // fullpaths to inject them in the nodes.
let remote_keystore_chain_id = if let Some(id) = ctx.parachain_id { let remote_keystore_chain_id =
id if let Some(id) = ctx.parachain_id { id } else { ctx.chain_id };
} else {
ctx.chain_id
};
let keystore_path = node.keystore_path.clone().unwrap_or(PathBuf::from(format!( let keystore_path = node
"/data/chains/{remote_keystore_chain_id}/keystore", .keystore_path
))); .clone()
.unwrap_or(PathBuf::from(format!("/data/chains/{remote_keystore_chain_id}/keystore",)));
for key_filename in key_filenames { for key_filename in key_filenames {
let f = TransferedFile::new( let f = TransferedFile::new(
PathBuf::from(format!( PathBuf::from(format!(
"{}/{}/{}", "{}/{}/{}",
ctx.ns.base_dir().to_string_lossy(), ctx.ns.base_dir().to_string_lossy(),
node_files_path, node_files_path,
key_filename.to_string_lossy() key_filename.to_string_lossy()
)), )),
keystore_path.join(key_filename), keystore_path.join(key_filename),
); );
files_to_inject.push(f); files_to_inject.push(f);
} }
created_paths.push(keystore_path); created_paths.push(keystore_path);
} }
let base_dir = format!("{}/{}", ctx.ns.base_dir().to_string_lossy(), &node.name); let base_dir = format!("{}/{}", ctx.ns.base_dir().to_string_lossy(), &node.name);
let (cfg_path, data_path, relay_data_path) = if !ctx.ns.capabilities().prefix_with_full_path { let (cfg_path, data_path, relay_data_path) = if !ctx.ns.capabilities().prefix_with_full_path {
( (NODE_CONFIG_DIR.into(), NODE_DATA_DIR.into(), NODE_RELAY_DATA_DIR.into())
NODE_CONFIG_DIR.into(), } else {
NODE_DATA_DIR.into(), let cfg_path = format!("{}{NODE_CONFIG_DIR}", &base_dir);
NODE_RELAY_DATA_DIR.into(), let data_path = format!("{}{NODE_DATA_DIR}", &base_dir);
) let relay_data_path = format!("{}{NODE_RELAY_DATA_DIR}", &base_dir);
} else { (cfg_path, data_path, relay_data_path)
let cfg_path = format!("{}{NODE_CONFIG_DIR}", &base_dir); };
let data_path = format!("{}{NODE_DATA_DIR}", &base_dir);
let relay_data_path = format!("{}{NODE_RELAY_DATA_DIR}", &base_dir);
(cfg_path, data_path, relay_data_path)
};
let gen_opts = generators::GenCmdOptions { let gen_opts = generators::GenCmdOptions {
relay_chain_name: ctx.chain, relay_chain_name: ctx.chain,
cfg_path: &cfg_path, // TODO: get from provider/ns cfg_path: &cfg_path, // TODO: get from provider/ns
data_path: &data_path, // TODO: get from provider data_path: &data_path, // TODO: get from provider
relay_data_path: &relay_data_path, // TODO: get from provider relay_data_path: &relay_data_path, // TODO: get from provider
use_wrapper: false, // TODO: get from provider use_wrapper: false, // TODO: get from provider
bootnode_addr: ctx.bootnodes_addr.clone(), bootnode_addr: ctx.bootnodes_addr.clone(),
use_default_ports_in_cmd: ctx.ns.capabilities().use_default_ports_in_cmd, use_default_ports_in_cmd: ctx.ns.capabilities().use_default_ports_in_cmd,
// IFF the provider require an image (e.g k8s) we know this is not native // IFF the provider require an image (e.g k8s) we know this is not native
is_native: !ctx.ns.capabilities().requires_image, is_native: !ctx.ns.capabilities().requires_image,
}; };
let mut collator_full_node_prom_port: Option<u16> = None; let mut collator_full_node_prom_port: Option<u16> = None;
let mut collator_full_node_prom_port_external: Option<u16> = None; let mut collator_full_node_prom_port_external: Option<u16> = None;
let (program, args) = match ctx.role { let (program, args) = match ctx.role {
// Collator should be `non-cumulus` one (e.g adder/undying) // Collator should be `non-cumulus` one (e.g adder/undying)
ZombieRole::Node | ZombieRole::Collator => { ZombieRole::Node | ZombieRole::Collator => {
let maybe_para_id = ctx.parachain.map(|para| para.id); let maybe_para_id = ctx.parachain.map(|para| para.id);
generators::generate_node_command(node, gen_opts, maybe_para_id) generators::generate_node_command(node, gen_opts, maybe_para_id)
}, },
ZombieRole::CumulusCollator => { ZombieRole::CumulusCollator => {
let para = ctx.parachain.expect(&format!( let para = ctx
"parachain must be part of the context {THIS_IS_A_BUG}" .parachain
)); .expect(&format!("parachain must be part of the context {THIS_IS_A_BUG}"));
collator_full_node_prom_port = node.full_node_prometheus_port.as_ref().map(|p| p.0); collator_full_node_prom_port = node.full_node_prometheus_port.as_ref().map(|p| p.0);
generators::generate_node_command_cumulus(node, gen_opts, para.id) generators::generate_node_command_cumulus(node, gen_opts, para.id)
}, },
_ => unreachable!(), /* TODO: do we need those? _ => unreachable!(), /* TODO: do we need those?
* ZombieRole::Bootnode => todo!(), * ZombieRole::Bootnode => todo!(),
* ZombieRole::Companion => todo!(), */ * ZombieRole::Companion => todo!(), */
}; };
// apply running networ replacements // apply running networ replacements
let args: Vec<String> = args let args: Vec<String> = args
.iter() .iter()
.map(|arg| apply_running_network_replacements(arg, &ctx.nodes_by_name)) .map(|arg| apply_running_network_replacements(arg, &ctx.nodes_by_name))
.collect(); .collect();
info!( info!("🚀 {}, spawning.... with command: {} {}", node.name, program, args.join(" "));
"🚀 {}, spawning.... with command: {} {}",
node.name,
program,
args.join(" ")
);
let ports = if ctx.ns.capabilities().use_default_ports_in_cmd { let ports = if ctx.ns.capabilities().use_default_ports_in_cmd {
// should use default ports to as internal // should use default ports to as internal
[ [
(P2P_PORT, node.p2p_port.0), (P2P_PORT, node.p2p_port.0),
(RPC_PORT, node.rpc_port.0), (RPC_PORT, node.rpc_port.0),
(PROMETHEUS_PORT, node.prometheus_port.0), (PROMETHEUS_PORT, node.prometheus_port.0),
] ]
} else { } else {
[ [(P2P_PORT, P2P_PORT), (RPC_PORT, RPC_PORT), (PROMETHEUS_PORT, PROMETHEUS_PORT)]
(P2P_PORT, P2P_PORT), };
(RPC_PORT, RPC_PORT),
(PROMETHEUS_PORT, PROMETHEUS_PORT),
]
};
let spawn_ops = SpawnNodeOptions::new(node.name.clone(), program) let spawn_ops = SpawnNodeOptions::new(node.name.clone(), program)
.args(args) .args(args)
.env( .env(node.env.iter().map(|var| (var.name.clone(), var.value.clone())))
node.env .injected_files(files_to_inject)
.iter() .created_paths(created_paths)
.map(|var| (var.name.clone(), var.value.clone())), .db_snapshot(node.db_snapshot.clone())
) .port_mapping(HashMap::from(ports))
.injected_files(files_to_inject) .node_log_path(node.node_log_path.clone());
.created_paths(created_paths)
.db_snapshot(node.db_snapshot.clone())
.port_mapping(HashMap::from(ports))
.node_log_path(node.node_log_path.clone());
let spawn_ops = if let Some(image) = node.image.as_ref() { let spawn_ops = if let Some(image) = node.image.as_ref() {
spawn_ops.image(image.as_str()) spawn_ops.image(image.as_str())
} else { } else {
spawn_ops spawn_ops
}; };
// Drops the port parking listeners before spawn // Drops the port parking listeners before spawn
node.ws_port.drop_listener(); node.ws_port.drop_listener();
node.p2p_port.drop_listener(); node.p2p_port.drop_listener();
node.rpc_port.drop_listener(); node.rpc_port.drop_listener();
node.prometheus_port.drop_listener(); node.prometheus_port.drop_listener();
if let Some(port) = &node.full_node_p2p_port { if let Some(port) = &node.full_node_p2p_port {
port.drop_listener(); port.drop_listener();
} }
if let Some(port) = &node.full_node_prometheus_port { if let Some(port) = &node.full_node_prometheus_port {
port.drop_listener(); port.drop_listener();
} }
let running_node = ctx.ns.spawn_node(&spawn_ops).await.with_context(|| { let running_node = ctx.ns.spawn_node(&spawn_ops).await.with_context(|| {
format!( format!("Failed to spawn node: {} with opts: {:#?}", node.name, spawn_ops)
"Failed to spawn node: {} with opts: {:#?}", })?;
node.name, spawn_ops
)
})?;
let mut ip_to_use = if let Some(local_ip) = ctx.global_settings.local_ip() { let mut ip_to_use =
*local_ip if let Some(local_ip) = ctx.global_settings.local_ip() { *local_ip } else { LOCALHOST };
} else {
LOCALHOST
};
let (rpc_port_external, prometheus_port_external, p2p_external); let (rpc_port_external, prometheus_port_external, p2p_external);
if running_in_ci() && ctx.ns.provider_name() == "k8s" { if running_in_ci() && ctx.ns.provider_name() == "k8s" {
// running kubernets in ci require to use ip and default port // running kubernets in ci require to use ip and default port
(rpc_port_external, prometheus_port_external, p2p_external) = (rpc_port_external, prometheus_port_external, p2p_external) =
(RPC_PORT, PROMETHEUS_PORT, P2P_PORT); (RPC_PORT, PROMETHEUS_PORT, P2P_PORT);
collator_full_node_prom_port_external = Some(FULL_NODE_PROMETHEUS_PORT); collator_full_node_prom_port_external = Some(FULL_NODE_PROMETHEUS_PORT);
ip_to_use = running_node.ip().await?; ip_to_use = running_node.ip().await?;
} else { } else {
// Create port-forward iff we are not in CI or provider doesn't use the default ports (native) // Create port-forward iff we are not in CI or provider doesn't use the default ports (native)
let ports = futures::future::try_join_all(vec![ let ports = futures::future::try_join_all(vec![
running_node.create_port_forward(node.rpc_port.0, RPC_PORT), running_node.create_port_forward(node.rpc_port.0, RPC_PORT),
running_node.create_port_forward(node.prometheus_port.0, PROMETHEUS_PORT), running_node.create_port_forward(node.prometheus_port.0, PROMETHEUS_PORT),
]) ])
.await?; .await?;
(rpc_port_external, prometheus_port_external, p2p_external) = ( (rpc_port_external, prometheus_port_external, p2p_external) = (
ports[0].unwrap_or(node.rpc_port.0), ports[0].unwrap_or(node.rpc_port.0),
ports[1].unwrap_or(node.prometheus_port.0), ports[1].unwrap_or(node.prometheus_port.0),
// p2p don't need port-fwd // p2p don't need port-fwd
node.p2p_port.0, node.p2p_port.0,
); );
if let Some(full_node_prom_port) = collator_full_node_prom_port { if let Some(full_node_prom_port) = collator_full_node_prom_port {
let port_fwd = running_node let port_fwd = running_node
.create_port_forward(full_node_prom_port, FULL_NODE_PROMETHEUS_PORT) .create_port_forward(full_node_prom_port, FULL_NODE_PROMETHEUS_PORT)
.await?; .await?;
collator_full_node_prom_port_external = Some(port_fwd.unwrap_or(full_node_prom_port)); collator_full_node_prom_port_external = Some(port_fwd.unwrap_or(full_node_prom_port));
} }
} }
let multiaddr = generators::generate_node_bootnode_addr( let multiaddr = generators::generate_node_bootnode_addr(
&node.peer_id, &node.peer_id,
&running_node.ip().await?, &running_node.ip().await?,
p2p_external, p2p_external,
running_node.args().as_ref(), running_node.args().as_ref(),
&node.p2p_cert_hash, &node.p2p_cert_hash,
)?; )?;
let ws_uri = format!("ws://{ip_to_use}:{rpc_port_external}"); let ws_uri = format!("ws://{ip_to_use}:{rpc_port_external}");
let prometheus_uri = format!("http://{ip_to_use}:{prometheus_port_external}/metrics"); let prometheus_uri = format!("http://{ip_to_use}:{prometheus_port_external}/metrics");
info!("🚀 {}, should be running now", node.name); info!("🚀 {}, should be running now", node.name);
info!( info!(
"💻 {}: direct link (pjs) https://polkadot.js.org/apps/?rpc={ws_uri}#/explorer", "💻 {}: direct link (pjs) https://polkadot.js.org/apps/?rpc={ws_uri}#/explorer",
node.name node.name
); );
info!( info!(
"💻 {}: direct link (papi) https://dev.papi.how/explorer#networkId=custom&endpoint={ws_uri}", "💻 {}: direct link (papi) https://dev.papi.how/explorer#networkId=custom&endpoint={ws_uri}",
node.name node.name
); );
info!("📊 {}: metrics link {prometheus_uri}", node.name); info!("📊 {}: metrics link {prometheus_uri}", node.name);
if let Some(full_node_prom_port) = collator_full_node_prom_port_external { if let Some(full_node_prom_port) = collator_full_node_prom_port_external {
info!( info!(
"📊 {}: collator full-node metrics link http://{}:{}/metrics", "📊 {}: collator full-node metrics link http://{}:{}/metrics",
node.name, ip_to_use, full_node_prom_port node.name, ip_to_use, full_node_prom_port
); );
} }
info!("📓 logs cmd: {}", running_node.log_cmd()); info!("📓 logs cmd: {}", running_node.log_cmd());
Ok(NetworkNode::new( Ok(NetworkNode::new(
node.name.clone(), node.name.clone(),
ws_uri, ws_uri,
prometheus_uri, prometheus_uri,
multiaddr, multiaddr,
node.clone(), node.clone(),
running_node, running_node,
)) ))
} }
@@ -2,42 +2,38 @@ use pezkuwi_subxt::{backend::rpc::RpcClient, OnlineClient};
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait ClientFromUrl: Sized { pub trait ClientFromUrl: Sized {
async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error>; async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error>;
async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error>; async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error>;
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl<Config: pezkuwi_subxt::Config + Send + Sync> ClientFromUrl for OnlineClient<Config> { impl<Config: pezkuwi_subxt::Config + Send + Sync> ClientFromUrl for OnlineClient<Config> {
async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> { async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_url(url).await.map_err(Into::into) Self::from_url(url).await.map_err(Into::into)
} }
async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> { async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_insecure_url(url).await.map_err(Into::into) Self::from_insecure_url(url).await.map_err(Into::into)
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ClientFromUrl for RpcClient { impl ClientFromUrl for RpcClient {
async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> { async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_url(url) Self::from_url(url).await.map_err(pezkuwi_subxt::Error::from)
.await }
.map_err(pezkuwi_subxt::Error::from)
}
async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> { async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_insecure_url(url) Self::from_insecure_url(url).await.map_err(pezkuwi_subxt::Error::from)
.await }
.map_err(pezkuwi_subxt::Error::from)
}
} }
pub async fn get_client_from_url<T: ClientFromUrl + Send>( pub async fn get_client_from_url<T: ClientFromUrl + Send>(
url: &str, url: &str,
) -> Result<T, pezkuwi_subxt::Error> { ) -> Result<T, pezkuwi_subxt::Error> {
if pezkuwi_subxt::utils::url_is_secure(url)? { if pezkuwi_subxt::utils::url_is_secure(url)? {
T::from_secure_url(url).await T::from_secure_url(url).await
} else { } else {
T::from_insecure_url(url).await T::from_insecure_url(url).await
} }
} }
@@ -5,65 +5,52 @@ use tracing::{debug, info};
use crate::network::node::NetworkNode; use crate::network::node::NetworkNode;
pub async fn upgrade( pub async fn upgrade(
node: &NetworkNode, node: &NetworkNode,
wasm_data: &[u8], wasm_data: &[u8],
sudo: &Keypair, sudo: &Keypair,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
debug!( debug!("Upgrading runtime, using node: {} with endpoting {}", node.name, node.ws_uri);
"Upgrading runtime, using node: {} with endpoting {}", let api: OnlineClient<BizinikiwConfig> = node.wait_client().await?;
node.name, node.ws_uri
);
let api: OnlineClient<BizinikiwConfig> = node.wait_client().await?;
let upgrade = pezkuwi_subxt::dynamic::tx( let upgrade = pezkuwi_subxt::dynamic::tx(
"System", "System",
"set_code_without_checks", "set_code_without_checks",
vec![Value::from_bytes(wasm_data)], vec![Value::from_bytes(wasm_data)],
); );
let sudo_call = pezkuwi_subxt::dynamic::tx( let sudo_call = pezkuwi_subxt::dynamic::tx(
"Sudo", "Sudo",
"sudo_unchecked_weight", "sudo_unchecked_weight",
vec![ vec![
upgrade.into_value(), upgrade.into_value(),
Value::named_composite([ Value::named_composite([
("ref_time", Value::primitive(1.into())), ("ref_time", Value::primitive(1.into())),
("proof_size", Value::primitive(1.into())), ("proof_size", Value::primitive(1.into())),
]), ]),
], ],
); );
let mut tx = api let mut tx = api.tx().sign_and_submit_then_watch_default(&sudo_call, sudo).await?;
.tx()
.sign_and_submit_then_watch_default(&sudo_call, sudo)
.await?;
// Below we use the low level API to replicate the `wait_for_in_block` behaviour // Below we use the low level API to replicate the `wait_for_in_block` behaviour
// which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237. // which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237.
while let Some(status) = tx.next().await { while let Some(status) = tx.next().await {
let status = status?; let status = status?;
match &status { match &status {
TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => { TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => {
let _result = tx_in_block.wait_for_success().await?; let _result = tx_in_block.wait_for_success().await?;
let block_status = if status.as_finalized().is_some() { let block_status =
"Finalized" if status.as_finalized().is_some() { "Finalized" } else { "Best" };
} else { info!("[{}] In block: {:#?}", block_status, tx_in_block.block_hash());
"Best" },
}; TxStatus::Error { message }
info!( | TxStatus::Invalid { message }
"[{}] In block: {:#?}", | TxStatus::Dropped { message } => {
block_status, return Err(anyhow::format_err!("Error submitting tx: {message}"));
tx_in_block.block_hash() },
); _ => continue,
}, }
TxStatus::Error { message } }
| TxStatus::Invalid { message }
| TxStatus::Dropped { message } => {
return Err(anyhow::format_err!("Error submitting tx: {message}"));
},
_ => continue,
}
}
Ok(()) Ok(())
} }
@@ -2,7 +2,7 @@ use serde::Deserializer;
pub fn default_as_empty_vec<'de, D, T>(_deserializer: D) -> Result<Vec<T>, D::Error> pub fn default_as_empty_vec<'de, D, T>(_deserializer: D) -> Result<Vec<T>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
Ok(Vec::new()) Ok(Vec::new())
} }
@@ -6,12 +6,12 @@ use pest_derive::Parser;
/// An error at parsing level. /// An error at parsing level.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum ParserError { pub enum ParserError {
#[error("error parsing input")] #[error("error parsing input")]
ParseError(Box<pest::error::Error<Rule>>), ParseError(Box<pest::error::Error<Rule>>),
#[error("root node should be valid: {0}")] #[error("root node should be valid: {0}")]
ParseRootNodeError(String), ParseRootNodeError(String),
#[error("can't cast metric value as f64: {0}")] #[error("can't cast metric value as f64: {0}")]
CastValueError(#[from] ParseFloatError), CastValueError(#[from] ParseFloatError),
} }
// This include forces recompiling this source file if the grammar file changes. // This include forces recompiling this source file if the grammar file changes.
@@ -25,154 +25,147 @@ pub struct MetricsParser;
pub type MetricMap = HashMap<String, f64>; pub type MetricMap = HashMap<String, f64>;
pub fn parse(input: &str) -> Result<MetricMap, ParserError> { pub fn parse(input: &str) -> Result<MetricMap, ParserError> {
let mut metric_map: MetricMap = Default::default(); let mut metric_map: MetricMap = Default::default();
let mut pairs = MetricsParser::parse(Rule::statement, input) let mut pairs = MetricsParser::parse(Rule::statement, input)
.map_err(|e| ParserError::ParseError(Box::new(e)))?; .map_err(|e| ParserError::ParseError(Box::new(e)))?;
let root = pairs let root = pairs.next().ok_or(ParserError::ParseRootNodeError(pairs.as_str().to_string()))?;
.next() for token in root.into_inner() {
.ok_or(ParserError::ParseRootNodeError(pairs.as_str().to_string()))?; if token.as_rule() == Rule::block {
for token in root.into_inner() { let inner = token.into_inner();
if token.as_rule() == Rule::block { for value in inner {
let inner = token.into_inner(); match value.as_rule() {
for value in inner { Rule::genericomment | Rule::typexpr | Rule::helpexpr => {
match value.as_rule() { // don't need to collect comments/types/helpers blocks.
Rule::genericomment | Rule::typexpr | Rule::helpexpr => { continue;
// don't need to collect comments/types/helpers blocks. },
continue; Rule::promstmt => {
}, let mut key: &str = "";
Rule::promstmt => { let mut labels: Vec<(&str, &str)> = Vec::new();
let mut key: &str = ""; let mut val: f64 = 0_f64;
let mut labels: Vec<(&str, &str)> = Vec::new(); for v in value.clone().into_inner() {
let mut val: f64 = 0_f64; match &v.as_rule() {
for v in value.clone().into_inner() { Rule::key => {
match &v.as_rule() { key = v.as_span().as_str();
Rule::key => { },
key = v.as_span().as_str(); Rule::NaN | Rule::posInf | Rule::negInf => {
}, // noop (not used in substrate metrics)
Rule::NaN | Rule::posInf | Rule::negInf => { },
// noop (not used in substrate metrics) Rule::number => {
}, val = v.as_span().as_str().parse::<f64>()?;
Rule::number => { },
val = v.as_span().as_str().parse::<f64>()?; Rule::labels => {
}, // SAFETY: use unwrap should be safe since we are just
Rule::labels => { // walking the parser struct and if are matching a label
// SAFETY: use unwrap should be safe since we are just // should have a key/vals
// walking the parser struct and if are matching a label for p in v.into_inner() {
// should have a key/vals let mut inner = p.into_inner();
for p in v.into_inner() { let key = inner.next().unwrap().as_span().as_str();
let mut inner = p.into_inner(); let value = inner
let key = inner.next().unwrap().as_span().as_str(); .next()
let value = inner .unwrap()
.next() .into_inner()
.unwrap() .next()
.into_inner() .unwrap()
.next() .as_span()
.unwrap() .as_str();
.as_span()
.as_str();
labels.push((key, value)); labels.push((key, value));
} }
}, },
_ => { _ => {
todo!("not implemented"); todo!("not implemented");
}, },
} }
} }
// we should store to make it compatible with zombienet v1: // we should store to make it compatible with zombienet v1:
// key_without_prefix // key_without_prefix
// key_without_prefix_and_without_chain // key_without_prefix_and_without_chain
// key_with_prefix_with_chain // key_with_prefix_with_chain
// key_with_prefix_and_without_chain // key_with_prefix_and_without_chain
let key_with_out_prefix = let key_with_out_prefix =
key.split('_').collect::<Vec<&str>>()[1..].join("_"); key.split('_').collect::<Vec<&str>>()[1..].join("_");
let (labels_without_chain, labels_with_chain) = let (labels_without_chain, labels_with_chain) =
labels.iter().fold((vec![], vec![]), |mut acc, item| { labels.iter().fold((vec![], vec![]), |mut acc, item| {
if item.0.eq("chain") { if item.0.eq("chain") {
acc.1.push(format!("{}=\"{}\"", item.0, item.1)); acc.1.push(format!("{}=\"{}\"", item.0, item.1));
} else { } else {
acc.0.push(format!("{}=\"{}\"", item.0, item.1)); acc.0.push(format!("{}=\"{}\"", item.0, item.1));
acc.1.push(format!("{}=\"{}\"", item.0, item.1)); acc.1.push(format!("{}=\"{}\"", item.0, item.1));
} }
acc acc
}); });
let labels_with_chain_str = if labels_with_chain.is_empty() { let labels_with_chain_str = if labels_with_chain.is_empty() {
String::from("") String::from("")
} else { } else {
format!("{{{}}}", labels_with_chain.join(",")) format!("{{{}}}", labels_with_chain.join(","))
}; };
let labels_without_chain_str = if labels_without_chain.is_empty() { let labels_without_chain_str = if labels_without_chain.is_empty() {
String::from("") String::from("")
} else { } else {
format!("{{{}}}", labels_without_chain.join(",")) format!("{{{}}}", labels_without_chain.join(","))
}; };
metric_map.insert(format!("{key}{labels_without_chain_str}"), val); metric_map.insert(format!("{key}{labels_without_chain_str}"), val);
metric_map.insert( metric_map.insert(
format!("{key_with_out_prefix}{labels_without_chain_str}"), format!("{key_with_out_prefix}{labels_without_chain_str}"),
val, val,
); );
metric_map.insert(format!("{key}{labels_with_chain_str}"), val); metric_map.insert(format!("{key}{labels_with_chain_str}"), val);
metric_map metric_map
.insert(format!("{key_with_out_prefix}{labels_with_chain_str}"), val); .insert(format!("{key_with_out_prefix}{labels_with_chain_str}"), val);
}, },
_ => {}, _ => {},
} }
} }
} }
} }
Ok(metric_map) Ok(metric_map)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs; use std::fs;
use super::*; use super::*;
#[test] #[test]
fn parse_metrics_works() { fn parse_metrics_works() {
let metrics_raw = fs::read_to_string("./testing/metrics.txt").unwrap(); let metrics_raw = fs::read_to_string("./testing/metrics.txt").unwrap();
let metrics = parse(&metrics_raw).unwrap(); let metrics = parse(&metrics_raw).unwrap();
// full key // full key
assert_eq!( assert_eq!(
metrics metrics
.get("polkadot_node_is_active_validator{chain=\"rococo_local_testnet\"}") .get("polkadot_node_is_active_validator{chain=\"rococo_local_testnet\"}")
.unwrap(), .unwrap(),
&1_f64 &1_f64
); );
// with prefix and no chain // with prefix and no chain
assert_eq!( assert_eq!(metrics.get("polkadot_node_is_active_validator").unwrap(), &1_f64);
metrics.get("polkadot_node_is_active_validator").unwrap(), // no prefix with chain
&1_f64 assert_eq!(
); metrics.get("node_is_active_validator{chain=\"rococo_local_testnet\"}").unwrap(),
// no prefix with chain &1_f64
assert_eq!( );
metrics // no prefix without chain
.get("node_is_active_validator{chain=\"rococo_local_testnet\"}") assert_eq!(metrics.get("node_is_active_validator").unwrap(), &1_f64);
.unwrap(), }
&1_f64
);
// no prefix without chain
assert_eq!(metrics.get("node_is_active_validator").unwrap(), &1_f64);
}
#[test] #[test]
fn parse_invalid_metrics_str_should_fail() { fn parse_invalid_metrics_str_should_fail() {
let metrics_raw = r" let metrics_raw = r"
# HELP polkadot_node_is_active_validator Tracks if the validator is in the active set. Updates at session boundary. # HELP polkadot_node_is_active_validator Tracks if the validator is in the active set. Updates at session boundary.
# TYPE polkadot_node_is_active_validator gauge # TYPE polkadot_node_is_active_validator gauge
polkadot_node_is_active_validator{chain=} 1 polkadot_node_is_active_validator{chain=} 1
"; ";
let metrics = parse(metrics_raw); let metrics = parse(metrics_raw);
assert!(metrics.is_err()); assert!(metrics.is_err());
assert!(matches!(metrics, Err(ParserError::ParseError(_)))); assert!(matches!(metrics, Err(ParserError::ParseError(_))));
} }
} }
@@ -16,581 +16,540 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Clone)] #[derive(Clone)]
pub struct DockerClient { pub struct DockerClient {
using_podman: bool, using_podman: bool,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ContainerRunOptions { pub struct ContainerRunOptions {
image: String, image: String,
command: Vec<String>, command: Vec<String>,
env: Option<Vec<(String, String)>>, env: Option<Vec<(String, String)>>,
volume_mounts: Option<HashMap<String, String>>, volume_mounts: Option<HashMap<String, String>>,
name: Option<String>, name: Option<String>,
entrypoint: Option<String>, entrypoint: Option<String>,
port_mapping: HashMap<Port, Port>, port_mapping: HashMap<Port, Port>,
rm: bool, rm: bool,
detach: bool, detach: bool,
} }
enum Container { enum Container {
Docker(DockerContainer), Docker(DockerContainer),
Podman(PodmanContainer), Podman(PodmanContainer),
} }
// TODO: we may don't need this // TODO: we may don't need this
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct DockerContainer { struct DockerContainer {
#[serde(alias = "Names", deserialize_with = "deserialize_list")] #[serde(alias = "Names", deserialize_with = "deserialize_list")]
names: Vec<String>, names: Vec<String>,
#[serde(alias = "Ports", deserialize_with = "deserialize_list")] #[serde(alias = "Ports", deserialize_with = "deserialize_list")]
ports: Vec<String>, ports: Vec<String>,
#[serde(alias = "State")] #[serde(alias = "State")]
state: String, state: String,
} }
// TODO: we may don't need this // TODO: we may don't need this
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct PodmanPort { struct PodmanPort {
host_ip: String, host_ip: String,
container_port: u16, container_port: u16,
host_port: u16, host_port: u16,
range: u16, range: u16,
protocol: String, protocol: String,
} }
// TODO: we may don't need this // TODO: we may don't need this
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct PodmanContainer { struct PodmanContainer {
#[serde(alias = "Id")] #[serde(alias = "Id")]
id: String, id: String,
#[serde(alias = "Image")] #[serde(alias = "Image")]
image: String, image: String,
#[serde(alias = "Mounts")] #[serde(alias = "Mounts")]
mounts: Vec<String>, mounts: Vec<String>,
#[serde(alias = "Names")] #[serde(alias = "Names")]
names: Vec<String>, names: Vec<String>,
#[serde(alias = "Ports", deserialize_with = "deserialize_null_as_default")] #[serde(alias = "Ports", deserialize_with = "deserialize_null_as_default")]
ports: Vec<PodmanPort>, ports: Vec<PodmanPort>,
#[serde(alias = "State")] #[serde(alias = "State")]
state: String, state: String,
} }
fn deserialize_list<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error> fn deserialize_list<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let str_sequence = String::deserialize(deserializer)?; let str_sequence = String::deserialize(deserializer)?;
Ok(str_sequence Ok(str_sequence
.split(',') .split(',')
.filter(|item| !item.is_empty()) .filter(|item| !item.is_empty())
.map(|item| item.to_owned()) .map(|item| item.to_owned())
.collect()) .collect())
} }
fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error> fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where where
T: Default + Deserialize<'de>, T: Default + Deserialize<'de>,
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let opt = Option::deserialize(deserializer)?; let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default()) Ok(opt.unwrap_or_default())
} }
impl ContainerRunOptions { impl ContainerRunOptions {
pub fn new<S>(image: &str, command: Vec<S>) -> Self pub fn new<S>(image: &str, command: Vec<S>) -> Self
where where
S: Into<String> + std::fmt::Debug + Send + Clone, S: Into<String> + std::fmt::Debug + Send + Clone,
{ {
ContainerRunOptions { ContainerRunOptions {
image: image.to_string(), image: image.to_string(),
command: command command: command.clone().into_iter().map(|s| s.into()).collect::<Vec<_>>(),
.clone() env: None,
.into_iter() volume_mounts: None,
.map(|s| s.into()) name: None,
.collect::<Vec<_>>(), entrypoint: None,
env: None, port_mapping: HashMap::default(),
volume_mounts: None, rm: false,
name: None, detach: true, // add -d flag by default
entrypoint: None, }
port_mapping: HashMap::default(), }
rm: false,
detach: true, // add -d flag by default
}
}
pub fn env<S>(mut self, env: Vec<(S, S)>) -> Self pub fn env<S>(mut self, env: Vec<(S, S)>) -> Self
where where
S: Into<String> + std::fmt::Debug + Send + Clone, S: Into<String> + std::fmt::Debug + Send + Clone,
{ {
self.env = Some( self.env = Some(env.into_iter().map(|(name, value)| (name.into(), value.into())).collect());
env.into_iter() self
.map(|(name, value)| (name.into(), value.into())) }
.collect(),
);
self
}
pub fn volume_mounts<S>(mut self, volume_mounts: HashMap<S, S>) -> Self pub fn volume_mounts<S>(mut self, volume_mounts: HashMap<S, S>) -> Self
where where
S: Into<String> + std::fmt::Debug + Send + Clone, S: Into<String> + std::fmt::Debug + Send + Clone,
{ {
self.volume_mounts = Some( self.volume_mounts = Some(
volume_mounts volume_mounts
.into_iter() .into_iter()
.map(|(source, target)| (source.into(), target.into())) .map(|(source, target)| (source.into(), target.into()))
.collect(), .collect(),
); );
self self
} }
pub fn name<S>(mut self, name: S) -> Self pub fn name<S>(mut self, name: S) -> Self
where where
S: Into<String> + std::fmt::Debug + Send + Clone, S: Into<String> + std::fmt::Debug + Send + Clone,
{ {
self.name = Some(name.into()); self.name = Some(name.into());
self self
} }
pub fn entrypoint<S>(mut self, entrypoint: S) -> Self pub fn entrypoint<S>(mut self, entrypoint: S) -> Self
where where
S: Into<String> + std::fmt::Debug + Send + Clone, S: Into<String> + std::fmt::Debug + Send + Clone,
{ {
self.entrypoint = Some(entrypoint.into()); self.entrypoint = Some(entrypoint.into());
self self
} }
pub fn port_mapping(mut self, port_mapping: &HashMap<Port, Port>) -> Self { pub fn port_mapping(mut self, port_mapping: &HashMap<Port, Port>) -> Self {
self.port_mapping.clone_from(port_mapping); self.port_mapping.clone_from(port_mapping);
self self
} }
pub fn rm(mut self) -> Self { pub fn rm(mut self) -> Self {
self.rm = true; self.rm = true;
self self
} }
pub fn detach(mut self, choice: bool) -> Self { pub fn detach(mut self, choice: bool) -> Self {
self.detach = choice; self.detach = choice;
self self
} }
} }
impl DockerClient { impl DockerClient {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let using_podman = Self::is_using_podman().await?; let using_podman = Self::is_using_podman().await?;
Ok(DockerClient { using_podman }) Ok(DockerClient { using_podman })
} }
pub fn client_binary(&self) -> String { pub fn client_binary(&self) -> String {
String::from(if self.using_podman { String::from(if self.using_podman { "podman" } else { "docker" })
"podman" }
} else {
"docker"
})
}
async fn is_using_podman() -> Result<bool> { async fn is_using_podman() -> Result<bool> {
if let Ok(output) = tokio::process::Command::new("docker") if let Ok(output) = tokio::process::Command::new("docker").arg("version").output().await {
.arg("version") // detect whether we're actually running podman with docker emulation
.output() return Ok(String::from_utf8_lossy(&output.stdout).to_lowercase().contains("podman"));
.await }
{
// detect whether we're actually running podman with docker emulation
return Ok(String::from_utf8_lossy(&output.stdout)
.to_lowercase()
.contains("podman"));
}
tokio::process::Command::new("podman") tokio::process::Command::new("podman")
.arg("--version") .arg("--version")
.output() .output()
.await .await
.map_err(|err| anyhow!("Failed to detect container engine: {err}"))?; .map_err(|err| anyhow!("Failed to detect container engine: {err}"))?;
Ok(true) Ok(true)
} }
} }
impl DockerClient { impl DockerClient {
fn client_command(&self) -> tokio::process::Command { fn client_command(&self) -> tokio::process::Command {
tokio::process::Command::new(self.client_binary()) tokio::process::Command::new(self.client_binary())
} }
pub async fn create_volume(&self, name: &str) -> Result<()> { pub async fn create_volume(&self, name: &str) -> Result<()> {
let result = self let result = self
.client_command() .client_command()
.args(["volume", "create", name]) .args(["volume", "create", name])
.output() .output()
.await .await
.map_err(|err| anyhow!("Failed to create volume '{name}': {err}"))?; .map_err(|err| anyhow!("Failed to create volume '{name}': {err}"))?;
if !result.status.success() { if !result.status.success() {
return Err(anyhow!( return Err(anyhow!(
"Failed to create volume '{name}': {}", "Failed to create volume '{name}': {}",
String::from_utf8_lossy(&result.stderr) String::from_utf8_lossy(&result.stderr)
) )
.into()); .into());
} }
Ok(()) Ok(())
} }
pub async fn container_run(&self, options: ContainerRunOptions) -> Result<String> { pub async fn container_run(&self, options: ContainerRunOptions) -> Result<String> {
let mut cmd = self.client_command(); let mut cmd = self.client_command();
cmd.args(["run", "--platform", "linux/amd64"]); cmd.args(["run", "--platform", "linux/amd64"]);
if options.detach { if options.detach {
cmd.arg("-d"); cmd.arg("-d");
} }
Self::apply_cmd_options(&mut cmd, &options); Self::apply_cmd_options(&mut cmd, &options);
trace!("cmd: {:?}", cmd); trace!("cmd: {:?}", cmd);
let result = cmd.output().await.map_err(|err| { let result = cmd.output().await.map_err(|err| {
anyhow!( anyhow!(
"Failed to run container with image '{image}' and command '{command}': {err}", "Failed to run container with image '{image}' and command '{command}': {err}",
image = options.image, image = options.image,
command = options.command.join(" "), command = options.command.join(" "),
) )
})?; })?;
if !result.status.success() { if !result.status.success() {
return Err(anyhow!( return Err(anyhow!(
"Failed to run container with image '{image}' and command '{command}': {err}", "Failed to run container with image '{image}' and command '{command}': {err}",
image = options.image, image = options.image,
command = options.command.join(" "), command = options.command.join(" "),
err = String::from_utf8_lossy(&result.stderr) err = String::from_utf8_lossy(&result.stderr)
) )
.into()); .into());
} }
Ok(String::from_utf8_lossy(&result.stdout).to_string()) Ok(String::from_utf8_lossy(&result.stdout).to_string())
} }
pub async fn container_create(&self, options: ContainerRunOptions) -> Result<String> { pub async fn container_create(&self, options: ContainerRunOptions) -> Result<String> {
let mut cmd = self.client_command(); let mut cmd = self.client_command();
cmd.args(["container", "create"]); cmd.args(["container", "create"]);
Self::apply_cmd_options(&mut cmd, &options); Self::apply_cmd_options(&mut cmd, &options);
trace!("cmd: {:?}", cmd); trace!("cmd: {:?}", cmd);
let result = cmd.output().await.map_err(|err| { let result = cmd.output().await.map_err(|err| {
anyhow!( anyhow!(
"Failed to run container with image '{image}' and command '{command}': {err}", "Failed to run container with image '{image}' and command '{command}': {err}",
image = options.image, image = options.image,
command = options.command.join(" "), command = options.command.join(" "),
) )
})?; })?;
if !result.status.success() { if !result.status.success() {
return Err(anyhow!( return Err(anyhow!(
"Failed to run container with image '{image}' and command '{command}': {err}", "Failed to run container with image '{image}' and command '{command}': {err}",
image = options.image, image = options.image,
command = options.command.join(" "), command = options.command.join(" "),
err = String::from_utf8_lossy(&result.stderr) err = String::from_utf8_lossy(&result.stderr)
) )
.into()); .into());
} }
Ok(String::from_utf8_lossy(&result.stdout).to_string()) Ok(String::from_utf8_lossy(&result.stdout).to_string())
} }
pub async fn container_exec<S>( pub async fn container_exec<S>(
&self, &self,
name: &str, name: &str,
command: Vec<S>, command: Vec<S>,
env: Option<Vec<(S, S)>>, env: Option<Vec<(S, S)>>,
as_user: Option<S>, as_user: Option<S>,
) -> Result<ExecutionResult> ) -> Result<ExecutionResult>
where where
S: Into<String> + std::fmt::Debug + Send + Clone, S: Into<String> + std::fmt::Debug + Send + Clone,
{ {
let mut cmd = self.client_command(); let mut cmd = self.client_command();
cmd.arg("exec"); cmd.arg("exec");
if let Some(env) = env { if let Some(env) = env {
for env_var in env { for env_var in env {
cmd.args(["-e", &format!("{}={}", env_var.0.into(), env_var.1.into())]); cmd.args(["-e", &format!("{}={}", env_var.0.into(), env_var.1.into())]);
} }
} }
if let Some(user) = as_user { if let Some(user) = as_user {
cmd.args(["-u", user.into().as_ref()]); cmd.args(["-u", user.into().as_ref()]);
} }
cmd.arg(name); cmd.arg(name);
cmd.args( cmd.args(command.clone().into_iter().map(|s| <S as Into<String>>::into(s)));
command
.clone()
.into_iter()
.map(|s| <S as Into<String>>::into(s)),
);
trace!("cmd is : {:?}", cmd); trace!("cmd is : {:?}", cmd);
let result = cmd.output().await.map_err(|err| { let result = cmd.output().await.map_err(|err| {
anyhow!( anyhow!(
"Failed to exec '{}' on '{}': {err}", "Failed to exec '{}' on '{}': {err}",
command command
.into_iter() .into_iter()
.map(|s| <S as Into<String>>::into(s)) .map(|s| <S as Into<String>>::into(s))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "), .join(" "),
name, name,
) )
})?; })?;
if !result.status.success() { if !result.status.success() {
return Ok(Err(( return Ok(Err((result.status, String::from_utf8_lossy(&result.stderr).to_string())));
result.status, }
String::from_utf8_lossy(&result.stderr).to_string(),
)));
}
Ok(Ok(String::from_utf8_lossy(&result.stdout).to_string())) Ok(Ok(String::from_utf8_lossy(&result.stdout).to_string()))
} }
pub async fn container_cp( pub async fn container_cp(
&self, &self,
name: &str, name: &str,
local_path: &Path, local_path: &Path,
remote_path: &Path, remote_path: &Path,
) -> Result<()> { ) -> Result<()> {
let result = self let result = self
.client_command() .client_command()
.args([ .args([
"cp", "cp",
local_path.to_string_lossy().as_ref(), local_path.to_string_lossy().as_ref(),
&format!("{name}:{}", remote_path.to_string_lossy().as_ref()), &format!("{name}:{}", remote_path.to_string_lossy().as_ref()),
]) ])
.output() .output()
.await .await
.map_err(|err| { .map_err(|err| {
anyhow!( anyhow!(
"Failed copy file '{file}' to container '{name}': {err}", "Failed copy file '{file}' to container '{name}': {err}",
file = local_path.to_string_lossy(), file = local_path.to_string_lossy(),
) )
})?; })?;
if !result.status.success() { if !result.status.success() {
return Err(anyhow!( return Err(anyhow!(
"Failed to copy file '{file}' to container '{name}': {err}", "Failed to copy file '{file}' to container '{name}': {err}",
file = local_path.to_string_lossy(), file = local_path.to_string_lossy(),
err = String::from_utf8_lossy(&result.stderr) err = String::from_utf8_lossy(&result.stderr)
) )
.into()); .into());
} }
Ok(()) Ok(())
} }
pub async fn container_rm(&self, name: &str) -> Result<()> { pub async fn container_rm(&self, name: &str) -> Result<()> {
let result = self let result = self
.client_command() .client_command()
.args(["rm", "--force", "--volumes", name]) .args(["rm", "--force", "--volumes", name])
.output() .output()
.await .await
.map_err(|err| anyhow!("Failed do remove container '{name}: {err}"))?; .map_err(|err| anyhow!("Failed do remove container '{name}: {err}"))?;
if !result.status.success() { if !result.status.success() {
return Err(anyhow!( return Err(anyhow!(
"Failed to remove container '{name}': {err}", "Failed to remove container '{name}': {err}",
err = String::from_utf8_lossy(&result.stderr) err = String::from_utf8_lossy(&result.stderr)
) )
.into()); .into());
} }
Ok(()) Ok(())
} }
pub async fn namespaced_containers_rm(&self, namespace: &str) -> Result<()> { pub async fn namespaced_containers_rm(&self, namespace: &str) -> Result<()> {
let container_names: Vec<String> = self let container_names: Vec<String> = self
.get_containers() .get_containers()
.await? .await?
.into_iter() .into_iter()
.filter_map(|container| match container { .filter_map(|container| match container {
Container::Docker(container) => { Container::Docker(container) => {
if let Some(name) = container.names.first() { if let Some(name) = container.names.first() {
if name.starts_with(namespace) { if name.starts_with(namespace) {
return Some(name.to_string()); return Some(name.to_string());
} }
} }
None None
}, },
Container::Podman(container) => { Container::Podman(container) => {
if let Some(name) = container.names.first() { if let Some(name) = container.names.first() {
if name.starts_with(namespace) { if name.starts_with(namespace) {
return Some(name.to_string()); return Some(name.to_string());
} }
} }
None None
}, },
}) })
.collect(); .collect();
info!("{:?}", container_names); info!("{:?}", container_names);
let futures = container_names let futures =
.iter() container_names.iter().map(|name| self.container_rm(name)).collect::<Vec<_>>();
.map(|name| self.container_rm(name)) try_join_all(futures).await?;
.collect::<Vec<_>>();
try_join_all(futures).await?;
Ok(()) Ok(())
} }
pub async fn container_ip(&self, container_name: &str) -> Result<String> { pub async fn container_ip(&self, container_name: &str) -> Result<String> {
let ip = if self.using_podman { let ip = if self.using_podman {
"127.0.0.1".into() "127.0.0.1".into()
} else { } else {
let mut cmd = tokio::process::Command::new("docker"); let mut cmd = tokio::process::Command::new("docker");
cmd.args(vec![ cmd.args(vec!["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", container_name]);
"inspect",
"-f",
"{{ .NetworkSettings.IPAddress }}",
container_name,
]);
trace!("CMD: {cmd:?}"); trace!("CMD: {cmd:?}");
let res = cmd let res = cmd
.output() .output()
.await .await
.map_err(|err| anyhow!("Failed to get docker container ip, output: {err}"))?; .map_err(|err| anyhow!("Failed to get docker container ip, output: {err}"))?;
String::from_utf8(res.stdout) String::from_utf8(res.stdout)
.map_err(|err| anyhow!("Failed to get docker container ip, output: {err}"))? .map_err(|err| anyhow!("Failed to get docker container ip, output: {err}"))?
.trim() .trim()
.into() .into()
}; };
trace!("IP: {ip}"); trace!("IP: {ip}");
Ok(ip) Ok(ip)
} }
async fn get_containers(&self) -> Result<Vec<Container>> { async fn get_containers(&self) -> Result<Vec<Container>> {
let containers = if self.using_podman { let containers = if self.using_podman {
self.get_podman_containers() self.get_podman_containers().await?.into_iter().map(Container::Podman).collect()
.await? } else {
.into_iter() self.get_docker_containers().await?.into_iter().map(Container::Docker).collect()
.map(Container::Podman) };
.collect()
} else {
self.get_docker_containers()
.await?
.into_iter()
.map(Container::Docker)
.collect()
};
Ok(containers) Ok(containers)
} }
async fn get_podman_containers(&self) -> Result<Vec<PodmanContainer>> { async fn get_podman_containers(&self) -> Result<Vec<PodmanContainer>> {
let res = tokio::process::Command::new("podman") let res = tokio::process::Command::new("podman")
.args(vec!["ps", "--all", "--no-trunc", "--format", "json"]) .args(vec!["ps", "--all", "--no-trunc", "--format", "json"])
.output() .output()
.await .await
.map_err(|err| anyhow!("Failed to get podman containers output: {err}"))?; .map_err(|err| anyhow!("Failed to get podman containers output: {err}"))?;
let stdout = String::from_utf8_lossy(&res.stdout); let stdout = String::from_utf8_lossy(&res.stdout);
let containers = serde_json::from_str(&stdout) let containers = serde_json::from_str(&stdout)
.map_err(|err| anyhow!("Failed to parse podman containers output: {err}"))?; .map_err(|err| anyhow!("Failed to parse podman containers output: {err}"))?;
Ok(containers) Ok(containers)
} }
async fn get_docker_containers(&self) -> Result<Vec<DockerContainer>> { async fn get_docker_containers(&self) -> Result<Vec<DockerContainer>> {
let res = tokio::process::Command::new("docker") let res = tokio::process::Command::new("docker")
.args(vec!["ps", "--all", "--no-trunc", "--format", "json"]) .args(vec!["ps", "--all", "--no-trunc", "--format", "json"])
.output() .output()
.await .await
.unwrap(); .unwrap();
let stdout = String::from_utf8_lossy(&res.stdout); let stdout = String::from_utf8_lossy(&res.stdout);
let mut containers = vec![]; let mut containers = vec![];
for line in stdout.lines() { for line in stdout.lines() {
containers.push( containers.push(
serde_json::from_str::<DockerContainer>(line) serde_json::from_str::<DockerContainer>(line)
.map_err(|err| anyhow!("Failed to parse docker container output: {err}"))?, .map_err(|err| anyhow!("Failed to parse docker container output: {err}"))?,
); );
} }
Ok(containers) Ok(containers)
} }
pub(crate) async fn container_logs(&self, container_name: &str) -> Result<String> { pub(crate) async fn container_logs(&self, container_name: &str) -> Result<String> {
let output = Command::new("sh") let output = Command::new("sh")
.arg("-c") .arg("-c")
.arg(format!("docker logs -t '{container_name}' 2>&1")) .arg(format!("docker logs -t '{container_name}' 2>&1"))
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.output() .output()
.await .await
.map_err(|err| { .map_err(|err| {
anyhow!( anyhow!(
"Failed to spawn docker logs command for container '{container_name}': {err}" "Failed to spawn docker logs command for container '{container_name}': {err}"
) )
})?; })?;
let logs = String::from_utf8_lossy(&output.stdout).to_string(); let logs = String::from_utf8_lossy(&output.stdout).to_string();
if !output.status.success() { if !output.status.success() {
// stderr was redirected to stdout, so logs should contain the error message if any // stderr was redirected to stdout, so logs should contain the error message if any
return Err(anyhow!( return Err(anyhow!(
"Failed to get logs for container '{name}': {logs}", "Failed to get logs for container '{name}': {logs}",
name = container_name, name = container_name,
logs = &logs logs = &logs
) )
.into()); .into());
} }
Ok(logs) Ok(logs)
} }
fn apply_cmd_options(cmd: &mut Command, options: &ContainerRunOptions) { fn apply_cmd_options(cmd: &mut Command, options: &ContainerRunOptions) {
if options.rm { if options.rm {
cmd.arg("--rm"); cmd.arg("--rm");
} }
if let Some(entrypoint) = options.entrypoint.as_ref() { if let Some(entrypoint) = options.entrypoint.as_ref() {
cmd.args(["--entrypoint", entrypoint]); cmd.args(["--entrypoint", entrypoint]);
} }
if let Some(volume_mounts) = options.volume_mounts.as_ref() { if let Some(volume_mounts) = options.volume_mounts.as_ref() {
for (source, target) in volume_mounts { for (source, target) in volume_mounts {
cmd.args(["-v", &format!("{source}:{target}")]); cmd.args(["-v", &format!("{source}:{target}")]);
} }
} }
if let Some(env) = options.env.as_ref() { if let Some(env) = options.env.as_ref() {
for env_var in env { for env_var in env {
cmd.args(["-e", &format!("{}={}", env_var.0, env_var.1)]); cmd.args(["-e", &format!("{}={}", env_var.0, env_var.1)]);
} }
} }
// add published ports // add published ports
for (container_port, host_port) in options.port_mapping.iter() { for (container_port, host_port) in options.port_mapping.iter() {
cmd.args(["-p", &format!("{host_port}:{container_port}")]); cmd.args(["-p", &format!("{host_port}:{container_port}")]);
} }
if let Some(name) = options.name.as_ref() { if let Some(name) = options.name.as_ref() {
cmd.args(["--name", name]); cmd.args(["--name", name]);
} }
cmd.arg(&options.image); cmd.arg(&options.image);
for arg in &options.command { for arg in &options.command {
cmd.arg(arg); cmd.arg(arg);
} }
} }
} }
@@ -1,8 +1,8 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Weak}, sync::{Arc, Weak},
thread, thread,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -12,212 +12,207 @@ use tracing::{debug, trace, warn};
use uuid::Uuid; use uuid::Uuid;
use super::{ use super::{
client::{ContainerRunOptions, DockerClient}, client::{ContainerRunOptions, DockerClient},
node::DockerNode, node::DockerNode,
DockerProvider, DockerProvider,
}; };
use crate::{ use crate::{
constants::NAMESPACE_PREFIX, constants::NAMESPACE_PREFIX,
docker::{ docker::{
node::{DeserializableDockerNodeOptions, DockerNodeOptions}, node::{DeserializableDockerNodeOptions, DockerNodeOptions},
provider, provider,
}, },
shared::helpers::extract_execution_result, shared::helpers::extract_execution_result,
types::{ types::{
GenerateFileCommand, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions, GenerateFileCommand, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions,
SpawnNodeOptions, SpawnNodeOptions,
}, },
DynNode, ProviderError, ProviderNamespace, ProviderNode, DynNode, ProviderError, ProviderNamespace, ProviderNode,
}; };
pub struct DockerNamespace<FS> pub struct DockerNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
weak: Weak<DockerNamespace<FS>>, weak: Weak<DockerNamespace<FS>>,
#[allow(dead_code)] #[allow(dead_code)]
provider: Weak<DockerProvider<FS>>, provider: Weak<DockerProvider<FS>>,
name: String, name: String,
base_dir: PathBuf, base_dir: PathBuf,
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
docker_client: DockerClient, docker_client: DockerClient,
filesystem: FS, filesystem: FS,
delete_on_drop: Arc<Mutex<bool>>, delete_on_drop: Arc<Mutex<bool>>,
pub(super) nodes: RwLock<HashMap<String, Arc<DockerNode<FS>>>>, pub(super) nodes: RwLock<HashMap<String, Arc<DockerNode<FS>>>>,
} }
impl<FS> DockerNamespace<FS> impl<FS> DockerNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
pub(super) async fn new( pub(super) async fn new(
provider: &Weak<DockerProvider<FS>>, provider: &Weak<DockerProvider<FS>>,
tmp_dir: &PathBuf, tmp_dir: &PathBuf,
capabilities: &ProviderCapabilities, capabilities: &ProviderCapabilities,
docker_client: &DockerClient, docker_client: &DockerClient,
filesystem: &FS, filesystem: &FS,
custom_base_dir: Option<&Path>, custom_base_dir: Option<&Path>,
) -> Result<Arc<Self>, ProviderError> { ) -> Result<Arc<Self>, ProviderError> {
let name = format!("{}{}", NAMESPACE_PREFIX, Uuid::new_v4()); let name = format!("{}{}", NAMESPACE_PREFIX, Uuid::new_v4());
let base_dir = if let Some(custom_base_dir) = custom_base_dir { let base_dir = if let Some(custom_base_dir) = custom_base_dir {
if !filesystem.exists(custom_base_dir).await { if !filesystem.exists(custom_base_dir).await {
filesystem.create_dir(custom_base_dir).await?; filesystem.create_dir(custom_base_dir).await?;
} else { } else {
warn!( warn!(
"⚠️ Using and existing directory {} as base dir", "⚠️ Using and existing directory {} as base dir",
custom_base_dir.to_string_lossy() custom_base_dir.to_string_lossy()
); );
} }
PathBuf::from(custom_base_dir) PathBuf::from(custom_base_dir)
} else { } else {
let base_dir = PathBuf::from_iter([tmp_dir, &PathBuf::from(&name)]); let base_dir = PathBuf::from_iter([tmp_dir, &PathBuf::from(&name)]);
filesystem.create_dir(&base_dir).await?; filesystem.create_dir(&base_dir).await?;
base_dir base_dir
}; };
let namespace = Arc::new_cyclic(|weak| DockerNamespace { let namespace = Arc::new_cyclic(|weak| DockerNamespace {
weak: weak.clone(), weak: weak.clone(),
provider: provider.clone(), provider: provider.clone(),
name, name,
base_dir, base_dir,
capabilities: capabilities.clone(), capabilities: capabilities.clone(),
filesystem: filesystem.clone(), filesystem: filesystem.clone(),
docker_client: docker_client.clone(), docker_client: docker_client.clone(),
nodes: RwLock::new(HashMap::new()), nodes: RwLock::new(HashMap::new()),
delete_on_drop: Arc::new(Mutex::new(true)), delete_on_drop: Arc::new(Mutex::new(true)),
}); });
namespace.initialize().await?; namespace.initialize().await?;
Ok(namespace) Ok(namespace)
} }
pub(super) async fn attach_to_live( pub(super) async fn attach_to_live(
provider: &Weak<DockerProvider<FS>>, provider: &Weak<DockerProvider<FS>>,
capabilities: &ProviderCapabilities, capabilities: &ProviderCapabilities,
docker_client: &DockerClient, docker_client: &DockerClient,
filesystem: &FS, filesystem: &FS,
custom_base_dir: &Path, custom_base_dir: &Path,
name: &str, name: &str,
) -> Result<Arc<Self>, ProviderError> { ) -> Result<Arc<Self>, ProviderError> {
let base_dir = custom_base_dir.to_path_buf(); let base_dir = custom_base_dir.to_path_buf();
let namespace = Arc::new_cyclic(|weak| DockerNamespace { let namespace = Arc::new_cyclic(|weak| DockerNamespace {
weak: weak.clone(), weak: weak.clone(),
provider: provider.clone(), provider: provider.clone(),
name: name.to_owned(), name: name.to_owned(),
base_dir, base_dir,
capabilities: capabilities.clone(), capabilities: capabilities.clone(),
filesystem: filesystem.clone(), filesystem: filesystem.clone(),
docker_client: docker_client.clone(), docker_client: docker_client.clone(),
nodes: RwLock::new(HashMap::new()), nodes: RwLock::new(HashMap::new()),
delete_on_drop: Arc::new(Mutex::new(false)), delete_on_drop: Arc::new(Mutex::new(false)),
}); });
Ok(namespace) Ok(namespace)
} }
async fn initialize(&self) -> Result<(), ProviderError> { async fn initialize(&self) -> Result<(), ProviderError> {
// let ns_scripts_shared = PathBuf::from_iter([&self.base_dir, &PathBuf::from("shared-scripts")]); // let ns_scripts_shared = PathBuf::from_iter([&self.base_dir, &PathBuf::from("shared-scripts")]);
// self.filesystem.create_dir(&ns_scripts_shared).await?; // self.filesystem.create_dir(&ns_scripts_shared).await?;
self.initialize_zombie_scripts_volume().await?; self.initialize_zombie_scripts_volume().await?;
self.initialize_helper_binaries_volume().await?; self.initialize_helper_binaries_volume().await?;
Ok(()) Ok(())
} }
async fn initialize_zombie_scripts_volume(&self) -> Result<(), ProviderError> { async fn initialize_zombie_scripts_volume(&self) -> Result<(), ProviderError> {
let local_zombie_wrapper_path = let local_zombie_wrapper_path =
PathBuf::from_iter([&self.base_dir, &PathBuf::from("zombie-wrapper.sh")]); PathBuf::from_iter([&self.base_dir, &PathBuf::from("zombie-wrapper.sh")]);
self.filesystem self.filesystem
.write( .write(&local_zombie_wrapper_path, include_str!("../shared/scripts/zombie-wrapper.sh"))
&local_zombie_wrapper_path, .await?;
include_str!("../shared/scripts/zombie-wrapper.sh"),
)
.await?;
let local_helper_binaries_downloader_path = PathBuf::from_iter([ let local_helper_binaries_downloader_path =
&self.base_dir, PathBuf::from_iter([&self.base_dir, &PathBuf::from("helper-binaries-downloader.sh")]);
&PathBuf::from("helper-binaries-downloader.sh"),
]);
self.filesystem self.filesystem
.write( .write(
&local_helper_binaries_downloader_path, &local_helper_binaries_downloader_path,
include_str!("../shared/scripts/helper-binaries-downloader.sh"), include_str!("../shared/scripts/helper-binaries-downloader.sh"),
) )
.await?; .await?;
let zombie_wrapper_volume_name = format!("{}-zombie-wrapper", self.name); let zombie_wrapper_volume_name = format!("{}-zombie-wrapper", self.name);
let zombie_wrapper_container_name = format!("{}-scripts", self.name); let zombie_wrapper_container_name = format!("{}-scripts", self.name);
self.docker_client self.docker_client
.create_volume(&zombie_wrapper_volume_name) .create_volume(&zombie_wrapper_volume_name)
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
self.docker_client self.docker_client
.container_create( .container_create(
ContainerRunOptions::new("alpine:latest", vec!["tail", "-f", "/dev/null"]) ContainerRunOptions::new("alpine:latest", vec!["tail", "-f", "/dev/null"])
.volume_mounts(HashMap::from([( .volume_mounts(HashMap::from([(
zombie_wrapper_volume_name.as_str(), zombie_wrapper_volume_name.as_str(),
"/scripts", "/scripts",
)])) )]))
.name(&zombie_wrapper_container_name) .name(&zombie_wrapper_container_name)
.rm(), .rm(),
) )
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
// copy the scripts // copy the scripts
self.docker_client self.docker_client
.container_cp( .container_cp(
&zombie_wrapper_container_name, &zombie_wrapper_container_name,
&local_zombie_wrapper_path, &local_zombie_wrapper_path,
&PathBuf::from("/scripts/zombie-wrapper.sh"), &PathBuf::from("/scripts/zombie-wrapper.sh"),
) )
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
self.docker_client self.docker_client
.container_cp( .container_cp(
&zombie_wrapper_container_name, &zombie_wrapper_container_name,
&local_helper_binaries_downloader_path, &local_helper_binaries_downloader_path,
&PathBuf::from("/scripts/helper-binaries-downloader.sh"), &PathBuf::from("/scripts/helper-binaries-downloader.sh"),
) )
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
// set permissions for rwx on whole volume recursively // set permissions for rwx on whole volume recursively
self.docker_client self.docker_client
.container_run( .container_run(
ContainerRunOptions::new("alpine:latest", vec!["chmod", "-R", "777", "/scripts"]) ContainerRunOptions::new("alpine:latest", vec!["chmod", "-R", "777", "/scripts"])
.volume_mounts(HashMap::from([( .volume_mounts(HashMap::from([(
zombie_wrapper_volume_name.as_ref(), zombie_wrapper_volume_name.as_ref(),
"/scripts", "/scripts",
)])) )]))
.rm(), .rm(),
) )
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
Ok(()) Ok(())
} }
async fn initialize_helper_binaries_volume(&self) -> Result<(), ProviderError> { async fn initialize_helper_binaries_volume(&self) -> Result<(), ProviderError> {
let helper_binaries_volume_name = format!("{}-helper-binaries", self.name); let helper_binaries_volume_name = format!("{}-helper-binaries", self.name);
let zombie_wrapper_volume_name = format!("{}-zombie-wrapper", self.name); let zombie_wrapper_volume_name = format!("{}-zombie-wrapper", self.name);
self.docker_client self.docker_client
.create_volume(&helper_binaries_volume_name) .create_volume(&helper_binaries_volume_name)
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
// download binaries to volume // download binaries to volume
self.docker_client self.docker_client
.container_run( .container_run(
ContainerRunOptions::new( ContainerRunOptions::new(
"alpine:latest", "alpine:latest",
vec!["ash", "/scripts/helper-binaries-downloader.sh"], vec!["ash", "/scripts/helper-binaries-downloader.sh"],
) )
@@ -234,261 +229,242 @@ where
// wait until complete // wait until complete
.detach(false) .detach(false)
.rm(), .rm(),
) )
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
// set permissions for rwx on whole volume recursively // set permissions for rwx on whole volume recursively
self.docker_client self.docker_client
.container_run( .container_run(
ContainerRunOptions::new("alpine:latest", vec!["chmod", "-R", "777", "/helpers"]) ContainerRunOptions::new("alpine:latest", vec!["chmod", "-R", "777", "/helpers"])
.volume_mounts(HashMap::from([( .volume_mounts(HashMap::from([(
helper_binaries_volume_name.as_ref(), helper_binaries_volume_name.as_ref(),
"/helpers", "/helpers",
)])) )]))
.rm(), .rm(),
) )
.await .await
.map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?; .map_err(|err| ProviderError::CreateNamespaceFailed(self.name.clone(), err.into()))?;
Ok(()) Ok(())
} }
pub async fn set_delete_on_drop(&self, delete_on_drop: bool) { pub async fn set_delete_on_drop(&self, delete_on_drop: bool) {
*self.delete_on_drop.lock().await = delete_on_drop; *self.delete_on_drop.lock().await = delete_on_drop;
} }
pub async fn delete_on_drop(&self) -> bool { pub async fn delete_on_drop(&self) -> bool {
if let Ok(delete_on_drop) = self.delete_on_drop.try_lock() { if let Ok(delete_on_drop) = self.delete_on_drop.try_lock() {
*delete_on_drop *delete_on_drop
} else { } else {
// if we can't lock just remove the ns // if we can't lock just remove the ns
true true
} }
} }
} }
#[async_trait] #[async_trait]
impl<FS> ProviderNamespace for DockerNamespace<FS> impl<FS> ProviderNamespace for DockerNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
fn name(&self) -> &str { fn name(&self) -> &str {
&self.name &self.name
} }
fn base_dir(&self) -> &PathBuf { fn base_dir(&self) -> &PathBuf {
&self.base_dir &self.base_dir
} }
fn capabilities(&self) -> &ProviderCapabilities { fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities &self.capabilities
} }
fn provider_name(&self) -> &str { fn provider_name(&self) -> &str {
provider::PROVIDER_NAME provider::PROVIDER_NAME
} }
async fn detach(&self) { async fn detach(&self) {
self.set_delete_on_drop(false).await; self.set_delete_on_drop(false).await;
} }
async fn is_detached(&self) -> bool { async fn is_detached(&self) -> bool {
self.delete_on_drop().await self.delete_on_drop().await
} }
async fn nodes(&self) -> HashMap<String, DynNode> { async fn nodes(&self) -> HashMap<String, DynNode> {
self.nodes self.nodes
.read() .read()
.await .await
.iter() .iter()
.map(|(name, node)| (name.clone(), node.clone() as DynNode)) .map(|(name, node)| (name.clone(), node.clone() as DynNode))
.collect() .collect()
} }
async fn get_node_available_args( async fn get_node_available_args(
&self, &self,
(command, image): (String, Option<String>), (command, image): (String, Option<String>),
) -> Result<String, ProviderError> { ) -> Result<String, ProviderError> {
let node_image = image.expect(&format!("image should be present when getting node available args with docker provider {THIS_IS_A_BUG}")); let node_image = image.expect(&format!("image should be present when getting node available args with docker provider {THIS_IS_A_BUG}"));
let temp_node = self let temp_node = self
.spawn_node( .spawn_node(
&SpawnNodeOptions::new(format!("temp-{}", Uuid::new_v4()), "cat".to_string()) &SpawnNodeOptions::new(format!("temp-{}", Uuid::new_v4()), "cat".to_string())
.image(node_image.clone()), .image(node_image.clone()),
) )
.await?; .await?;
let available_args_output = temp_node let available_args_output = temp_node
.run_command(RunCommandOptions::new(command.clone()).args(vec!["--help"])) .run_command(RunCommandOptions::new(command.clone()).args(vec!["--help"]))
.await? .await?
.map_err(|(_exit, status)| { .map_err(|(_exit, status)| {
ProviderError::NodeAvailableArgsError(node_image, command, status) ProviderError::NodeAvailableArgsError(node_image, command, status)
})?; })?;
temp_node.destroy().await?; temp_node.destroy().await?;
Ok(available_args_output) Ok(available_args_output)
} }
async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError> { async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError> {
debug!("spawn option {:?}", options); debug!("spawn option {:?}", options);
let node = DockerNode::new(DockerNodeOptions { let node = DockerNode::new(DockerNodeOptions {
namespace: &self.weak, namespace: &self.weak,
namespace_base_dir: &self.base_dir, namespace_base_dir: &self.base_dir,
name: &options.name, name: &options.name,
image: options.image.as_ref(), image: options.image.as_ref(),
program: &options.program, program: &options.program,
args: &options.args, args: &options.args,
env: &options.env, env: &options.env,
startup_files: &options.injected_files, startup_files: &options.injected_files,
db_snapshot: options.db_snapshot.as_ref(), db_snapshot: options.db_snapshot.as_ref(),
docker_client: &self.docker_client, docker_client: &self.docker_client,
container_name: format!("{}-{}", self.name, options.name), container_name: format!("{}-{}", self.name, options.name),
filesystem: &self.filesystem, filesystem: &self.filesystem,
port_mapping: options.port_mapping.as_ref().unwrap_or(&HashMap::default()), port_mapping: options.port_mapping.as_ref().unwrap_or(&HashMap::default()),
}) })
.await?; .await?;
self.nodes self.nodes.write().await.insert(node.name().to_string(), node.clone());
.write()
.await
.insert(node.name().to_string(), node.clone());
Ok(node) Ok(node)
} }
async fn spawn_node_from_json( async fn spawn_node_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNode, ProviderError> { ) -> Result<DynNode, ProviderError> {
let deserializable: DeserializableDockerNodeOptions = let deserializable: DeserializableDockerNodeOptions =
serde_json::from_value(json_value.clone())?; serde_json::from_value(json_value.clone())?;
let options = DockerNodeOptions::from_deserializable( let options = DockerNodeOptions::from_deserializable(
&deserializable, &deserializable,
&self.weak, &self.weak,
&self.base_dir, &self.base_dir,
&self.docker_client, &self.docker_client,
&self.filesystem, &self.filesystem,
); );
let node = DockerNode::attach_to_live(options).await?; let node = DockerNode::attach_to_live(options).await?;
self.nodes self.nodes.write().await.insert(node.name().to_string(), node.clone());
.write()
.await
.insert(node.name().to_string(), node.clone());
Ok(node) Ok(node)
} }
async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError> { async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError> {
debug!("generate files options {options:#?}"); debug!("generate files options {options:#?}");
let node_name = options let node_name = options.temp_name.unwrap_or_else(|| format!("temp-{}", Uuid::new_v4()));
.temp_name let node_image = options.image.expect(&format!(
.unwrap_or_else(|| format!("temp-{}", Uuid::new_v4())); "image should be present when generating files with docker provider {THIS_IS_A_BUG}"
let node_image = options.image.expect(&format!( ));
"image should be present when generating files with docker provider {THIS_IS_A_BUG}"
));
// run dummy command in a new container // run dummy command in a new container
let temp_node = self let temp_node = self
.spawn_node( .spawn_node(
&SpawnNodeOptions::new(node_name, "cat".to_string()) &SpawnNodeOptions::new(node_name, "cat".to_string())
.injected_files(options.injected_files) .injected_files(options.injected_files)
.image(node_image), .image(node_image),
) )
.await?; .await?;
for GenerateFileCommand { for GenerateFileCommand { program, args, env, local_output_path } in options.commands {
program, let local_output_full_path = format!(
args, "{}{}{}",
env, self.base_dir.to_string_lossy(),
local_output_path, if local_output_path.starts_with("/") { "" } else { "/" },
} in options.commands local_output_path.to_string_lossy()
{ );
let local_output_full_path = format!(
"{}{}{}",
self.base_dir.to_string_lossy(),
if local_output_path.starts_with("/") {
""
} else {
"/"
},
local_output_path.to_string_lossy()
);
let contents = extract_execution_result( let contents = extract_execution_result(
&temp_node, &temp_node,
RunCommandOptions { program, args, env }, RunCommandOptions { program, args, env },
options.expected_path.as_ref(), options.expected_path.as_ref(),
) )
.await?; .await?;
self.filesystem self.filesystem
.write(local_output_full_path, contents) .write(local_output_full_path, contents)
.await .await
.map_err(|err| ProviderError::FileGenerationFailed(err.into()))?; .map_err(|err| ProviderError::FileGenerationFailed(err.into()))?;
} }
temp_node.destroy().await temp_node.destroy().await
} }
async fn static_setup(&self) -> Result<(), ProviderError> { async fn static_setup(&self) -> Result<(), ProviderError> {
todo!() todo!()
} }
async fn destroy(&self) -> Result<(), ProviderError> { async fn destroy(&self) -> Result<(), ProviderError> {
let _ = self let _ =
.docker_client self.docker_client.namespaced_containers_rm(&self.name).await.map_err(|err| {
.namespaced_containers_rm(&self.name) ProviderError::DeleteNamespaceFailed(self.name.clone(), err.into())
.await })?;
.map_err(|err| ProviderError::DeleteNamespaceFailed(self.name.clone(), err.into()))?;
if let Some(provider) = self.provider.upgrade() { if let Some(provider) = self.provider.upgrade() {
provider.namespaces.write().await.remove(&self.name); provider.namespaces.write().await.remove(&self.name);
} }
Ok(()) Ok(())
} }
} }
impl<FS> Drop for DockerNamespace<FS> impl<FS> Drop for DockerNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
fn drop(&mut self) { fn drop(&mut self) {
let ns_name = self.name.clone(); let ns_name = self.name.clone();
if let Ok(delete_on_drop) = self.delete_on_drop.try_lock() { if let Ok(delete_on_drop) = self.delete_on_drop.try_lock() {
if *delete_on_drop { if *delete_on_drop {
let client = self.docker_client.clone(); let client = self.docker_client.clone();
let provider = self.provider.upgrade(); let provider = self.provider.upgrade();
let handler = thread::spawn(move || { let handler = thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move { rt.block_on(async move {
trace!("🧟 deleting ns {ns_name} from cluster"); trace!("🧟 deleting ns {ns_name} from cluster");
let _ = client.namespaced_containers_rm(&ns_name).await; let _ = client.namespaced_containers_rm(&ns_name).await;
trace!("✅ deleted"); trace!("✅ deleted");
}); });
}); });
if handler.join().is_ok() { if handler.join().is_ok() {
if let Some(provider) = provider { if let Some(provider) = provider {
if let Ok(mut p) = provider.namespaces.try_write() { if let Ok(mut p) = provider.namespaces.try_write() {
p.remove(&self.name); p.remove(&self.name);
} else { } else {
warn!( warn!(
"⚠️ Can not acquire write lock to the provider, ns {} not removed", "⚠️ Can not acquire write lock to the provider, ns {} not removed",
self.name self.name
); );
} }
} }
} }
} else { } else {
trace!("⚠️ leaking ns {ns_name} in cluster"); trace!("⚠️ leaking ns {ns_name} in cluster");
} }
}; };
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -10,152 +10,143 @@ use tokio::sync::RwLock;
use super::{client::DockerClient, namespace::DockerNamespace}; use super::{client::DockerClient, namespace::DockerNamespace};
use crate::{ use crate::{
shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider, shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider,
ProviderError, ProviderNamespace, ProviderError, ProviderNamespace,
}; };
pub const PROVIDER_NAME: &str = "docker"; pub const PROVIDER_NAME: &str = "docker";
pub struct DockerProvider<FS> pub struct DockerProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
weak: Weak<DockerProvider<FS>>, weak: Weak<DockerProvider<FS>>,
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
tmp_dir: PathBuf, tmp_dir: PathBuf,
docker_client: DockerClient, docker_client: DockerClient,
filesystem: FS, filesystem: FS,
pub(super) namespaces: RwLock<HashMap<String, Arc<DockerNamespace<FS>>>>, pub(super) namespaces: RwLock<HashMap<String, Arc<DockerNamespace<FS>>>>,
} }
impl<FS> DockerProvider<FS> impl<FS> DockerProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
pub async fn new(filesystem: FS) -> Arc<Self> { pub async fn new(filesystem: FS) -> Arc<Self> {
let docker_client = DockerClient::new().await.unwrap(); let docker_client = DockerClient::new().await.unwrap();
let provider = Arc::new_cyclic(|weak| DockerProvider { let provider = Arc::new_cyclic(|weak| DockerProvider {
weak: weak.clone(), weak: weak.clone(),
capabilities: ProviderCapabilities { capabilities: ProviderCapabilities {
requires_image: true, requires_image: true,
has_resources: false, has_resources: false,
prefix_with_full_path: false, prefix_with_full_path: false,
use_default_ports_in_cmd: true, use_default_ports_in_cmd: true,
}, },
tmp_dir: std::env::temp_dir(), tmp_dir: std::env::temp_dir(),
docker_client, docker_client,
filesystem, filesystem,
namespaces: RwLock::new(HashMap::new()), namespaces: RwLock::new(HashMap::new()),
}); });
let cloned_provider = provider.clone(); let cloned_provider = provider.clone();
tokio::spawn(async move { tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap(); tokio::signal::ctrl_c().await.unwrap();
for (_, ns) in cloned_provider.namespaces().await { for (_, ns) in cloned_provider.namespaces().await {
if ns.is_detached().await { if ns.is_detached().await {
// best effort // best effort
let _ = ns.destroy().await; let _ = ns.destroy().await;
} }
} }
// exit the process (130, SIGINT) // exit the process (130, SIGINT)
std::process::exit(130) std::process::exit(130)
}); });
provider provider
} }
pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self { pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self {
self.tmp_dir = tmp_dir.into(); self.tmp_dir = tmp_dir.into();
self self
} }
} }
#[async_trait] #[async_trait]
impl<FS> Provider for DockerProvider<FS> impl<FS> Provider for DockerProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
fn name(&self) -> &str { fn name(&self) -> &str {
PROVIDER_NAME PROVIDER_NAME
} }
fn capabilities(&self) -> &ProviderCapabilities { fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities &self.capabilities
} }
async fn namespaces(&self) -> HashMap<String, DynNamespace> { async fn namespaces(&self) -> HashMap<String, DynNamespace> {
self.namespaces self.namespaces
.read() .read()
.await .await
.iter() .iter()
.map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace)) .map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace))
.collect() .collect()
} }
async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> { async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> {
let namespace = DockerNamespace::new( let namespace = DockerNamespace::new(
&self.weak, &self.weak,
&self.tmp_dir, &self.tmp_dir,
&self.capabilities, &self.capabilities,
&self.docker_client, &self.docker_client,
&self.filesystem, &self.filesystem,
None, None,
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
async fn create_namespace_with_base_dir( async fn create_namespace_with_base_dir(
&self, &self,
base_dir: &Path, base_dir: &Path,
) -> Result<DynNamespace, ProviderError> { ) -> Result<DynNamespace, ProviderError> {
let namespace = DockerNamespace::new( let namespace = DockerNamespace::new(
&self.weak, &self.weak,
&self.tmp_dir, &self.tmp_dir,
&self.capabilities, &self.capabilities,
&self.docker_client, &self.docker_client,
&self.filesystem, &self.filesystem,
Some(base_dir), Some(base_dir),
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
async fn create_namespace_from_json( async fn create_namespace_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNamespace, ProviderError> { ) -> Result<DynNamespace, ProviderError> {
let (base_dir, name) = extract_namespace_info(json_value)?; let (base_dir, name) = extract_namespace_info(json_value)?;
let namespace = DockerNamespace::attach_to_live( let namespace = DockerNamespace::attach_to_live(
&self.weak, &self.weak,
&self.capabilities, &self.capabilities,
&self.docker_client, &self.docker_client,
&self.filesystem, &self.filesystem,
&base_dir, &base_dir,
&name, &name,
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,76 +2,73 @@ use std::collections::BTreeMap;
use configuration::shared::resources::{ResourceQuantity, Resources}; use configuration::shared::resources::{ResourceQuantity, Resources};
use k8s_openapi::{ use k8s_openapi::{
api::core::v1::{ api::core::v1::{
ConfigMapVolumeSource, Container, EnvVar, PodSpec, ResourceRequirements, Volume, ConfigMapVolumeSource, Container, EnvVar, PodSpec, ResourceRequirements, Volume,
VolumeMount, VolumeMount,
}, },
apimachinery::pkg::api::resource::Quantity, apimachinery::pkg::api::resource::Quantity,
}; };
pub(super) struct PodSpecBuilder; pub(super) struct PodSpecBuilder;
impl PodSpecBuilder { impl PodSpecBuilder {
pub(super) fn build( pub(super) fn build(
name: &str, name: &str,
image: &str, image: &str,
resources: Option<&Resources>, resources: Option<&Resources>,
program: &str, program: &str,
args: &[String], args: &[String],
env: &[(String, String)], env: &[(String, String)],
) -> PodSpec { ) -> PodSpec {
PodSpec { PodSpec {
hostname: Some(name.to_string()), hostname: Some(name.to_string()),
init_containers: Some(vec![Self::build_helper_binaries_setup_container()]), init_containers: Some(vec![Self::build_helper_binaries_setup_container()]),
containers: vec![Self::build_main_container( containers: vec![Self::build_main_container(
name, image, resources, program, args, env, name, image, resources, program, args, env,
)], )],
volumes: Some(Self::build_volumes()), volumes: Some(Self::build_volumes()),
..Default::default() ..Default::default()
} }
} }
fn build_main_container( fn build_main_container(
name: &str, name: &str,
image: &str, image: &str,
resources: Option<&Resources>, resources: Option<&Resources>,
program: &str, program: &str,
args: &[String], args: &[String],
env: &[(String, String)], env: &[(String, String)],
) -> Container { ) -> Container {
Container { Container {
name: name.to_string(), name: name.to_string(),
image: Some(image.to_string()), image: Some(image.to_string()),
image_pull_policy: Some("Always".to_string()), image_pull_policy: Some("Always".to_string()),
command: Some( command: Some(
[ [vec!["/zombie-wrapper.sh".to_string(), program.to_string()], args.to_vec()]
vec!["/zombie-wrapper.sh".to_string(), program.to_string()], .concat(),
args.to_vec(), ),
] env: Some(
.concat(), env.iter()
), .map(|(name, value)| EnvVar {
env: Some( name: name.clone(),
env.iter() value: Some(value.clone()),
.map(|(name, value)| EnvVar { value_from: None,
name: name.clone(), })
value: Some(value.clone()), .collect(),
value_from: None, ),
}) volume_mounts: Some(Self::build_volume_mounts(vec![VolumeMount {
.collect(), name: "zombie-wrapper-volume".to_string(),
), mount_path: "/zombie-wrapper.sh".to_string(),
volume_mounts: Some(Self::build_volume_mounts(vec![VolumeMount { sub_path: Some("zombie-wrapper.sh".to_string()),
name: "zombie-wrapper-volume".to_string(), ..Default::default()
mount_path: "/zombie-wrapper.sh".to_string(), }])),
sub_path: Some("zombie-wrapper.sh".to_string()), resources: Self::build_resources_requirements(resources),
..Default::default() ..Default::default()
}])), }
resources: Self::build_resources_requirements(resources), }
..Default::default()
}
}
fn build_helper_binaries_setup_container() -> Container { fn build_helper_binaries_setup_container() -> Container {
Container { Container {
name: "helper-binaries-setup".to_string(), name: "helper-binaries-setup".to_string(),
image: Some("europe-west3-docker.pkg.dev/parity-zombienet/zombienet-public-images/alpine:latest".to_string()), image: Some("europe-west3-docker.pkg.dev/parity-zombienet/zombienet-public-images/alpine:latest".to_string()),
image_pull_policy: Some("IfNotPresent".to_string()), image_pull_policy: Some("IfNotPresent".to_string()),
@@ -87,102 +84,93 @@ impl PodSpecBuilder {
]), ]),
..Default::default() ..Default::default()
} }
} }
fn build_volumes() -> Vec<Volume> { fn build_volumes() -> Vec<Volume> {
vec![ vec![
Volume { Volume { name: "cfg".to_string(), ..Default::default() },
name: "cfg".to_string(), Volume { name: "data".to_string(), ..Default::default() },
..Default::default() Volume { name: "relay-data".to_string(), ..Default::default() },
}, Volume {
Volume { name: "zombie-wrapper-volume".to_string(),
name: "data".to_string(), config_map: Some(ConfigMapVolumeSource {
..Default::default() name: Some("zombie-wrapper".to_string()),
}, default_mode: Some(0o755),
Volume { ..Default::default()
name: "relay-data".to_string(), }),
..Default::default() ..Default::default()
}, },
Volume { Volume {
name: "zombie-wrapper-volume".to_string(), name: "helper-binaries-downloader-volume".to_string(),
config_map: Some(ConfigMapVolumeSource { config_map: Some(ConfigMapVolumeSource {
name: Some("zombie-wrapper".to_string()), name: Some("helper-binaries-downloader".to_string()),
default_mode: Some(0o755), default_mode: Some(0o755),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
}, },
Volume { ]
name: "helper-binaries-downloader-volume".to_string(), }
config_map: Some(ConfigMapVolumeSource {
name: Some("helper-binaries-downloader".to_string()),
default_mode: Some(0o755),
..Default::default()
}),
..Default::default()
},
]
}
fn build_volume_mounts(non_default_mounts: Vec<VolumeMount>) -> Vec<VolumeMount> { fn build_volume_mounts(non_default_mounts: Vec<VolumeMount>) -> Vec<VolumeMount> {
[ [
vec![ vec![
VolumeMount { VolumeMount {
name: "cfg".to_string(), name: "cfg".to_string(),
mount_path: "/cfg".to_string(), mount_path: "/cfg".to_string(),
read_only: Some(false), read_only: Some(false),
..Default::default() ..Default::default()
}, },
VolumeMount { VolumeMount {
name: "data".to_string(), name: "data".to_string(),
mount_path: "/data".to_string(), mount_path: "/data".to_string(),
read_only: Some(false), read_only: Some(false),
..Default::default() ..Default::default()
}, },
VolumeMount { VolumeMount {
name: "relay-data".to_string(), name: "relay-data".to_string(),
mount_path: "/relay-data".to_string(), mount_path: "/relay-data".to_string(),
read_only: Some(false), read_only: Some(false),
..Default::default() ..Default::default()
}, },
], ],
non_default_mounts, non_default_mounts,
] ]
.concat() .concat()
} }
fn build_resources_requirements(resources: Option<&Resources>) -> Option<ResourceRequirements> { fn build_resources_requirements(resources: Option<&Resources>) -> Option<ResourceRequirements> {
resources.map(|resources| ResourceRequirements { resources.map(|resources| ResourceRequirements {
limits: Self::build_resources_requirements_quantities( limits: Self::build_resources_requirements_quantities(
resources.limit_cpu(), resources.limit_cpu(),
resources.limit_memory(), resources.limit_memory(),
), ),
requests: Self::build_resources_requirements_quantities( requests: Self::build_resources_requirements_quantities(
resources.request_cpu(), resources.request_cpu(),
resources.request_memory(), resources.request_memory(),
), ),
..Default::default() ..Default::default()
}) })
} }
fn build_resources_requirements_quantities( fn build_resources_requirements_quantities(
cpu: Option<&ResourceQuantity>, cpu: Option<&ResourceQuantity>,
memory: Option<&ResourceQuantity>, memory: Option<&ResourceQuantity>,
) -> Option<BTreeMap<String, Quantity>> { ) -> Option<BTreeMap<String, Quantity>> {
let mut quantities = BTreeMap::new(); let mut quantities = BTreeMap::new();
if let Some(cpu) = cpu { if let Some(cpu) = cpu {
quantities.insert("cpu".to_string(), Quantity(cpu.as_str().to_string())); quantities.insert("cpu".to_string(), Quantity(cpu.as_str().to_string()));
} }
if let Some(memory) = memory { if let Some(memory) = memory {
quantities.insert("memory".to_string(), Quantity(memory.as_str().to_string())); quantities.insert("memory".to_string(), Quantity(memory.as_str().to_string()));
} }
if !quantities.is_empty() { if !quantities.is_empty() {
Some(quantities) Some(quantities)
} else { } else {
None None
} }
} }
} }
@@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -10,136 +10,127 @@ use tokio::sync::RwLock;
use super::{client::KubernetesClient, namespace::KubernetesNamespace}; use super::{client::KubernetesClient, namespace::KubernetesNamespace};
use crate::{ use crate::{
shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider, shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider,
ProviderError, ProviderNamespace, ProviderError, ProviderNamespace,
}; };
pub const PROVIDER_NAME: &str = "k8s"; pub const PROVIDER_NAME: &str = "k8s";
pub struct KubernetesProvider<FS> pub struct KubernetesProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
weak: Weak<KubernetesProvider<FS>>, weak: Weak<KubernetesProvider<FS>>,
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
tmp_dir: PathBuf, tmp_dir: PathBuf,
k8s_client: KubernetesClient, k8s_client: KubernetesClient,
filesystem: FS, filesystem: FS,
pub(super) namespaces: RwLock<HashMap<String, Arc<KubernetesNamespace<FS>>>>, pub(super) namespaces: RwLock<HashMap<String, Arc<KubernetesNamespace<FS>>>>,
} }
impl<FS> KubernetesProvider<FS> impl<FS> KubernetesProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
pub async fn new(filesystem: FS) -> Arc<Self> { pub async fn new(filesystem: FS) -> Arc<Self> {
let k8s_client = KubernetesClient::new().await.unwrap(); let k8s_client = KubernetesClient::new().await.unwrap();
Arc::new_cyclic(|weak| KubernetesProvider { Arc::new_cyclic(|weak| KubernetesProvider {
weak: weak.clone(), weak: weak.clone(),
capabilities: ProviderCapabilities { capabilities: ProviderCapabilities {
requires_image: true, requires_image: true,
has_resources: true, has_resources: true,
prefix_with_full_path: false, prefix_with_full_path: false,
use_default_ports_in_cmd: true, use_default_ports_in_cmd: true,
}, },
tmp_dir: std::env::temp_dir(), tmp_dir: std::env::temp_dir(),
k8s_client, k8s_client,
filesystem, filesystem,
namespaces: RwLock::new(HashMap::new()), namespaces: RwLock::new(HashMap::new()),
}) })
} }
pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self { pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self {
self.tmp_dir = tmp_dir.into(); self.tmp_dir = tmp_dir.into();
self self
} }
} }
#[async_trait] #[async_trait]
impl<FS> Provider for KubernetesProvider<FS> impl<FS> Provider for KubernetesProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
fn name(&self) -> &str { fn name(&self) -> &str {
PROVIDER_NAME PROVIDER_NAME
} }
fn capabilities(&self) -> &ProviderCapabilities { fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities &self.capabilities
} }
async fn namespaces(&self) -> HashMap<String, DynNamespace> { async fn namespaces(&self) -> HashMap<String, DynNamespace> {
self.namespaces self.namespaces
.read() .read()
.await .await
.iter() .iter()
.map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace)) .map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace))
.collect() .collect()
} }
async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> { async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> {
let namespace = KubernetesNamespace::new( let namespace = KubernetesNamespace::new(
&self.weak, &self.weak,
&self.tmp_dir, &self.tmp_dir,
&self.capabilities, &self.capabilities,
&self.k8s_client, &self.k8s_client,
&self.filesystem, &self.filesystem,
None, None,
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
async fn create_namespace_with_base_dir( async fn create_namespace_with_base_dir(
&self, &self,
base_dir: &Path, base_dir: &Path,
) -> Result<DynNamespace, ProviderError> { ) -> Result<DynNamespace, ProviderError> {
let namespace = KubernetesNamespace::new( let namespace = KubernetesNamespace::new(
&self.weak, &self.weak,
&self.tmp_dir, &self.tmp_dir,
&self.capabilities, &self.capabilities,
&self.k8s_client, &self.k8s_client,
&self.filesystem, &self.filesystem,
Some(base_dir), Some(base_dir),
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
async fn create_namespace_from_json( async fn create_namespace_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNamespace, ProviderError> { ) -> Result<DynNamespace, ProviderError> {
let (base_dir, name) = extract_namespace_info(json_value)?; let (base_dir, name) = extract_namespace_info(json_value)?;
let namespace = KubernetesNamespace::attach_to_live( let namespace = KubernetesNamespace::attach_to_live(
&self.weak, &self.weak,
&self.capabilities, &self.capabilities,
&self.k8s_client, &self.k8s_client,
&self.filesystem, &self.filesystem,
&base_dir, &base_dir,
&name, &name,
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
} }
+154 -154
View File
@@ -5,253 +5,253 @@ mod native;
pub mod shared; pub mod shared;
use std::{ use std::{
collections::HashMap, collections::HashMap,
net::IpAddr, net::IpAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use shared::{ use shared::{
constants::LOCALHOST, constants::LOCALHOST,
types::{ types::{
ExecutionResult, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions, ExecutionResult, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions,
RunScriptOptions, SpawnNodeOptions, RunScriptOptions, SpawnNodeOptions,
}, },
}; };
use support::fs::FileSystemError; use support::fs::FileSystemError;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub enum ProviderError { pub enum ProviderError {
#[error("Failed to create client '{0}': {1}")] #[error("Failed to create client '{0}': {1}")]
CreateClientFailed(String, anyhow::Error), CreateClientFailed(String, anyhow::Error),
#[error("Failed to create namespace '{0}': {1}")] #[error("Failed to create namespace '{0}': {1}")]
CreateNamespaceFailed(String, anyhow::Error), CreateNamespaceFailed(String, anyhow::Error),
#[error("Failed to spawn node '{0}': {1}")] #[error("Failed to spawn node '{0}': {1}")]
NodeSpawningFailed(String, anyhow::Error), NodeSpawningFailed(String, anyhow::Error),
#[error("Error running command '{0}' {1}: {2}")] #[error("Error running command '{0}' {1}: {2}")]
RunCommandError(String, String, anyhow::Error), RunCommandError(String, String, anyhow::Error),
#[error("Error running script'{0}': {1}")] #[error("Error running script'{0}': {1}")]
RunScriptError(String, anyhow::Error), RunScriptError(String, anyhow::Error),
#[error("Invalid network configuration field {0}")] #[error("Invalid network configuration field {0}")]
InvalidConfig(String), InvalidConfig(String),
#[error("Failed to retrieve node available args using image {0} and command {1}: {2}")] #[error("Failed to retrieve node available args using image {0} and command {1}: {2}")]
NodeAvailableArgsError(String, String, String), NodeAvailableArgsError(String, String, String),
#[error("Can not recover node: {0}")] #[error("Can not recover node: {0}")]
MissingNode(String), MissingNode(String),
#[error("Can not recover node: {0} info, field: {1}")] #[error("Can not recover node: {0} info, field: {1}")]
MissingNodeInfo(String, String), MissingNodeInfo(String, String),
#[error("File generation failed: {0}")] #[error("File generation failed: {0}")]
FileGenerationFailed(anyhow::Error), FileGenerationFailed(anyhow::Error),
#[error(transparent)] #[error(transparent)]
FileSystemError(#[from] FileSystemError), FileSystemError(#[from] FileSystemError),
#[error("Invalid script path for {0}")] #[error("Invalid script path for {0}")]
InvalidScriptPath(anyhow::Error), InvalidScriptPath(anyhow::Error),
#[error("Script with path {0} not found")] #[error("Script with path {0} not found")]
ScriptNotFound(PathBuf), ScriptNotFound(PathBuf),
#[error("Failed to retrieve process ID for node '{0}'")] #[error("Failed to retrieve process ID for node '{0}'")]
ProcessIdRetrievalFailed(String), ProcessIdRetrievalFailed(String),
#[error("Failed to pause node '{0}': {1}")] #[error("Failed to pause node '{0}': {1}")]
PauseNodeFailed(String, anyhow::Error), PauseNodeFailed(String, anyhow::Error),
#[error("Failed to resume node '{0}': {1}")] #[error("Failed to resume node '{0}': {1}")]
ResumeNodeFailed(String, anyhow::Error), ResumeNodeFailed(String, anyhow::Error),
#[error("Failed to kill node '{0}': {1}")] #[error("Failed to kill node '{0}': {1}")]
KillNodeFailed(String, anyhow::Error), KillNodeFailed(String, anyhow::Error),
#[error("Failed to restart node '{0}': {1}")] #[error("Failed to restart node '{0}': {1}")]
RestartNodeFailed(String, anyhow::Error), RestartNodeFailed(String, anyhow::Error),
#[error("Failed to destroy node '{0}': {1}")] #[error("Failed to destroy node '{0}': {1}")]
DestroyNodeFailed(String, anyhow::Error), DestroyNodeFailed(String, anyhow::Error),
#[error("Failed to get logs for node '{0}': {1}")] #[error("Failed to get logs for node '{0}': {1}")]
GetLogsFailed(String, anyhow::Error), GetLogsFailed(String, anyhow::Error),
#[error("Failed to dump logs for node '{0}': {1}")] #[error("Failed to dump logs for node '{0}': {1}")]
DumpLogsFailed(String, anyhow::Error), DumpLogsFailed(String, anyhow::Error),
#[error("Failed to copy file from node '{0}': {1}")] #[error("Failed to copy file from node '{0}': {1}")]
CopyFileFromNodeError(String, anyhow::Error), CopyFileFromNodeError(String, anyhow::Error),
#[error("Failed to setup fileserver: {0}")] #[error("Failed to setup fileserver: {0}")]
FileServerSetupError(anyhow::Error), FileServerSetupError(anyhow::Error),
#[error("Error uploading file: '{0}': {1}")] #[error("Error uploading file: '{0}': {1}")]
UploadFile(String, anyhow::Error), UploadFile(String, anyhow::Error),
#[error("Error downloading file: '{0}': {1}")] #[error("Error downloading file: '{0}': {1}")]
DownloadFile(String, anyhow::Error), DownloadFile(String, anyhow::Error),
#[error("Error sending file '{0}' to {1}: {2}")] #[error("Error sending file '{0}' to {1}: {2}")]
SendFile(String, String, anyhow::Error), SendFile(String, String, anyhow::Error),
#[error("Error creating port-forward '{0}:{1}': {2}")] #[error("Error creating port-forward '{0}:{1}': {2}")]
PortForwardError(u16, u16, anyhow::Error), PortForwardError(u16, u16, anyhow::Error),
#[error("Failed to delete namespace '{0}': {1}")] #[error("Failed to delete namespace '{0}': {1}")]
DeleteNamespaceFailed(String, anyhow::Error), DeleteNamespaceFailed(String, anyhow::Error),
#[error("Serialization error")] #[error("Serialization error")]
SerializationError(#[from] serde_json::Error), SerializationError(#[from] serde_json::Error),
#[error("Failed to acquire lock: {0}")] #[error("Failed to acquire lock: {0}")]
FailedToAcquireLock(String), FailedToAcquireLock(String),
} }
#[async_trait] #[async_trait]
pub trait Provider { pub trait Provider {
fn name(&self) -> &str; fn name(&self) -> &str;
fn capabilities(&self) -> &ProviderCapabilities; fn capabilities(&self) -> &ProviderCapabilities;
async fn namespaces(&self) -> HashMap<String, DynNamespace>; async fn namespaces(&self) -> HashMap<String, DynNamespace>;
async fn create_namespace(&self) -> Result<DynNamespace, ProviderError>; async fn create_namespace(&self) -> Result<DynNamespace, ProviderError>;
async fn create_namespace_with_base_dir( async fn create_namespace_with_base_dir(
&self, &self,
base_dir: &Path, base_dir: &Path,
) -> Result<DynNamespace, ProviderError>; ) -> Result<DynNamespace, ProviderError>;
async fn create_namespace_from_json( async fn create_namespace_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNamespace, ProviderError>; ) -> Result<DynNamespace, ProviderError>;
} }
pub type DynProvider = Arc<dyn Provider + Send + Sync>; pub type DynProvider = Arc<dyn Provider + Send + Sync>;
#[async_trait] #[async_trait]
pub trait ProviderNamespace { pub trait ProviderNamespace {
fn name(&self) -> &str; fn name(&self) -> &str;
fn base_dir(&self) -> &PathBuf; fn base_dir(&self) -> &PathBuf;
fn capabilities(&self) -> &ProviderCapabilities; fn capabilities(&self) -> &ProviderCapabilities;
fn provider_name(&self) -> &str; fn provider_name(&self) -> &str;
async fn detach(&self) { async fn detach(&self) {
// noop by default // noop by default
warn!("Detach is not implemented for {}", self.name()); warn!("Detach is not implemented for {}", self.name());
} }
async fn is_detached(&self) -> bool { async fn is_detached(&self) -> bool {
// false by default // false by default
false false
} }
async fn nodes(&self) -> HashMap<String, DynNode>; async fn nodes(&self) -> HashMap<String, DynNode>;
async fn get_node_available_args( async fn get_node_available_args(
&self, &self,
options: (String, Option<String>), options: (String, Option<String>),
) -> Result<String, ProviderError>; ) -> Result<String, ProviderError>;
async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError>; async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError>;
async fn spawn_node_from_json( async fn spawn_node_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNode, ProviderError>; ) -> Result<DynNode, ProviderError>;
async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError>; async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError>;
async fn destroy(&self) -> Result<(), ProviderError>; async fn destroy(&self) -> Result<(), ProviderError>;
async fn static_setup(&self) -> Result<(), ProviderError>; async fn static_setup(&self) -> Result<(), ProviderError>;
} }
pub type DynNamespace = Arc<dyn ProviderNamespace + Send + Sync>; pub type DynNamespace = Arc<dyn ProviderNamespace + Send + Sync>;
#[async_trait] #[async_trait]
pub trait ProviderNode: erased_serde::Serialize { pub trait ProviderNode: erased_serde::Serialize {
fn name(&self) -> &str; fn name(&self) -> &str;
fn args(&self) -> Vec<&str>; fn args(&self) -> Vec<&str>;
fn base_dir(&self) -> &PathBuf; fn base_dir(&self) -> &PathBuf;
fn config_dir(&self) -> &PathBuf; fn config_dir(&self) -> &PathBuf;
fn data_dir(&self) -> &PathBuf; fn data_dir(&self) -> &PathBuf;
fn relay_data_dir(&self) -> &PathBuf; fn relay_data_dir(&self) -> &PathBuf;
fn scripts_dir(&self) -> &PathBuf; fn scripts_dir(&self) -> &PathBuf;
fn log_path(&self) -> &PathBuf; fn log_path(&self) -> &PathBuf;
fn log_cmd(&self) -> String; fn log_cmd(&self) -> String;
// Return the absolute path to the file in the `node` perspective // Return the absolute path to the file in the `node` perspective
// TODO: purpose? // TODO: purpose?
fn path_in_node(&self, file: &Path) -> PathBuf; fn path_in_node(&self, file: &Path) -> PathBuf;
async fn logs(&self) -> Result<String, ProviderError>; async fn logs(&self) -> Result<String, ProviderError>;
async fn dump_logs(&self, local_dest: PathBuf) -> Result<(), ProviderError>; async fn dump_logs(&self, local_dest: PathBuf) -> Result<(), ProviderError>;
// By default return localhost, should be overrided for k8s // By default return localhost, should be overrided for k8s
async fn ip(&self) -> Result<IpAddr, ProviderError> { async fn ip(&self) -> Result<IpAddr, ProviderError> {
Ok(LOCALHOST) Ok(LOCALHOST)
} }
// Noop by default (native/docker provider) // Noop by default (native/docker provider)
async fn create_port_forward( async fn create_port_forward(
&self, &self,
_local_port: u16, _local_port: u16,
_remote_port: u16, _remote_port: u16,
) -> Result<Option<u16>, ProviderError> { ) -> Result<Option<u16>, ProviderError> {
Ok(None) Ok(None)
} }
async fn run_command( async fn run_command(
&self, &self,
options: RunCommandOptions, options: RunCommandOptions,
) -> Result<ExecutionResult, ProviderError>; ) -> Result<ExecutionResult, ProviderError>;
async fn run_script(&self, options: RunScriptOptions) async fn run_script(&self, options: RunScriptOptions)
-> Result<ExecutionResult, ProviderError>; -> Result<ExecutionResult, ProviderError>;
async fn send_file( async fn send_file(
&self, &self,
local_file_path: &Path, local_file_path: &Path,
remote_file_path: &Path, remote_file_path: &Path,
mode: &str, mode: &str,
) -> Result<(), ProviderError>; ) -> Result<(), ProviderError>;
async fn receive_file( async fn receive_file(
&self, &self,
remote_file_path: &Path, remote_file_path: &Path,
local_file_path: &Path, local_file_path: &Path,
) -> Result<(), ProviderError>; ) -> Result<(), ProviderError>;
async fn pause(&self) -> Result<(), ProviderError>; async fn pause(&self) -> Result<(), ProviderError>;
async fn resume(&self) -> Result<(), ProviderError>; async fn resume(&self) -> Result<(), ProviderError>;
async fn restart(&self, after: Option<Duration>) -> Result<(), ProviderError>; async fn restart(&self, after: Option<Duration>) -> Result<(), ProviderError>;
async fn destroy(&self) -> Result<(), ProviderError>; async fn destroy(&self) -> Result<(), ProviderError>;
} }
pub type DynNode = Arc<dyn ProviderNode + Send + Sync>; pub type DynNode = Arc<dyn ProviderNode + Send + Sync>;
@@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -12,363 +12,337 @@ use uuid::Uuid;
use super::node::{NativeNode, NativeNodeOptions}; use super::node::{NativeNode, NativeNodeOptions};
use crate::{ use crate::{
constants::NAMESPACE_PREFIX, constants::NAMESPACE_PREFIX,
native::{node::DeserializableNativeNodeOptions, provider}, native::{node::DeserializableNativeNodeOptions, provider},
shared::helpers::extract_execution_result, shared::helpers::extract_execution_result,
types::{ types::{
GenerateFileCommand, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions, GenerateFileCommand, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions,
SpawnNodeOptions, SpawnNodeOptions,
}, },
DynNode, NativeProvider, ProviderError, ProviderNamespace, ProviderNode, DynNode, NativeProvider, ProviderError, ProviderNamespace, ProviderNode,
}; };
pub(super) struct NativeNamespace<FS> pub(super) struct NativeNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
weak: Weak<NativeNamespace<FS>>, weak: Weak<NativeNamespace<FS>>,
name: String, name: String,
provider: Weak<NativeProvider<FS>>, provider: Weak<NativeProvider<FS>>,
base_dir: PathBuf, base_dir: PathBuf,
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
filesystem: FS, filesystem: FS,
pub(super) nodes: RwLock<HashMap<String, Arc<NativeNode<FS>>>>, pub(super) nodes: RwLock<HashMap<String, Arc<NativeNode<FS>>>>,
} }
impl<FS> NativeNamespace<FS> impl<FS> NativeNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
pub(super) async fn new( pub(super) async fn new(
provider: &Weak<NativeProvider<FS>>, provider: &Weak<NativeProvider<FS>>,
tmp_dir: &PathBuf, tmp_dir: &PathBuf,
capabilities: &ProviderCapabilities, capabilities: &ProviderCapabilities,
filesystem: &FS, filesystem: &FS,
custom_base_dir: Option<&Path>, custom_base_dir: Option<&Path>,
) -> Result<Arc<Self>, ProviderError> { ) -> Result<Arc<Self>, ProviderError> {
let name = format!("{}{}", NAMESPACE_PREFIX, Uuid::new_v4()); let name = format!("{}{}", NAMESPACE_PREFIX, Uuid::new_v4());
let base_dir = if let Some(custom_base_dir) = custom_base_dir { let base_dir = if let Some(custom_base_dir) = custom_base_dir {
if !filesystem.exists(custom_base_dir).await { if !filesystem.exists(custom_base_dir).await {
filesystem.create_dir_all(custom_base_dir).await?; filesystem.create_dir_all(custom_base_dir).await?;
} else { } else {
warn!( warn!(
"⚠️ Using and existing directory {} as base dir", "⚠️ Using and existing directory {} as base dir",
custom_base_dir.to_string_lossy() custom_base_dir.to_string_lossy()
); );
} }
PathBuf::from(custom_base_dir) PathBuf::from(custom_base_dir)
} else { } else {
let base_dir = PathBuf::from_iter([tmp_dir, &PathBuf::from(&name)]); let base_dir = PathBuf::from_iter([tmp_dir, &PathBuf::from(&name)]);
filesystem.create_dir(&base_dir).await?; filesystem.create_dir(&base_dir).await?;
base_dir base_dir
}; };
Ok(Arc::new_cyclic(|weak| NativeNamespace { Ok(Arc::new_cyclic(|weak| NativeNamespace {
weak: weak.clone(), weak: weak.clone(),
provider: provider.clone(), provider: provider.clone(),
name, name,
base_dir, base_dir,
capabilities: capabilities.clone(), capabilities: capabilities.clone(),
filesystem: filesystem.clone(), filesystem: filesystem.clone(),
nodes: RwLock::new(HashMap::new()), nodes: RwLock::new(HashMap::new()),
})) }))
} }
pub(super) async fn attach_to_live( pub(super) async fn attach_to_live(
provider: &Weak<NativeProvider<FS>>, provider: &Weak<NativeProvider<FS>>,
capabilities: &ProviderCapabilities, capabilities: &ProviderCapabilities,
filesystem: &FS, filesystem: &FS,
custom_base_dir: &Path, custom_base_dir: &Path,
name: &str, name: &str,
) -> Result<Arc<Self>, ProviderError> { ) -> Result<Arc<Self>, ProviderError> {
let base_dir = custom_base_dir.to_path_buf(); let base_dir = custom_base_dir.to_path_buf();
Ok(Arc::new_cyclic(|weak| NativeNamespace { Ok(Arc::new_cyclic(|weak| NativeNamespace {
weak: weak.clone(), weak: weak.clone(),
provider: provider.clone(), provider: provider.clone(),
name: name.to_string(), name: name.to_string(),
base_dir, base_dir,
capabilities: capabilities.clone(), capabilities: capabilities.clone(),
filesystem: filesystem.clone(), filesystem: filesystem.clone(),
nodes: RwLock::new(HashMap::new()), nodes: RwLock::new(HashMap::new()),
})) }))
} }
} }
#[async_trait] #[async_trait]
impl<FS> ProviderNamespace for NativeNamespace<FS> impl<FS> ProviderNamespace for NativeNamespace<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
fn name(&self) -> &str { fn name(&self) -> &str {
&self.name &self.name
} }
fn base_dir(&self) -> &PathBuf { fn base_dir(&self) -> &PathBuf {
&self.base_dir &self.base_dir
} }
fn capabilities(&self) -> &ProviderCapabilities { fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities &self.capabilities
} }
fn provider_name(&self) -> &str { fn provider_name(&self) -> &str {
provider::PROVIDER_NAME provider::PROVIDER_NAME
} }
async fn nodes(&self) -> HashMap<String, DynNode> { async fn nodes(&self) -> HashMap<String, DynNode> {
self.nodes self.nodes
.read() .read()
.await .await
.iter() .iter()
.map(|(name, node)| (name.clone(), node.clone() as DynNode)) .map(|(name, node)| (name.clone(), node.clone() as DynNode))
.collect() .collect()
} }
async fn get_node_available_args( async fn get_node_available_args(
&self, &self,
(command, _image): (String, Option<String>), (command, _image): (String, Option<String>),
) -> Result<String, ProviderError> { ) -> Result<String, ProviderError> {
let temp_node = self let temp_node = self
.spawn_node( .spawn_node(
&SpawnNodeOptions::new(format!("temp-{}", Uuid::new_v4()), "bash".to_string()) &SpawnNodeOptions::new(format!("temp-{}", Uuid::new_v4()), "bash".to_string())
.args(vec!["-c", "while :; do sleep 1; done"]), .args(vec!["-c", "while :; do sleep 1; done"]),
) )
.await?; .await?;
let available_args_output = temp_node let available_args_output = temp_node
.run_command(RunCommandOptions::new(command.clone()).args(vec!["--help"])) .run_command(RunCommandOptions::new(command.clone()).args(vec!["--help"]))
.await? .await?
.map_err(|(_exit, status)| { .map_err(|(_exit, status)| {
ProviderError::NodeAvailableArgsError("".to_string(), command, status) ProviderError::NodeAvailableArgsError("".to_string(), command, status)
})?; })?;
temp_node.destroy().await?; temp_node.destroy().await?;
Ok(available_args_output) Ok(available_args_output)
} }
async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError> { async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError> {
trace!("spawn node options {options:?}"); trace!("spawn node options {options:?}");
let node = NativeNode::new(NativeNodeOptions { let node = NativeNode::new(NativeNodeOptions {
namespace: &self.weak, namespace: &self.weak,
namespace_base_dir: &self.base_dir, namespace_base_dir: &self.base_dir,
name: &options.name, name: &options.name,
program: &options.program, program: &options.program,
args: &options.args, args: &options.args,
env: &options.env, env: &options.env,
startup_files: &options.injected_files, startup_files: &options.injected_files,
created_paths: &options.created_paths, created_paths: &options.created_paths,
db_snapshot: options.db_snapshot.as_ref(), db_snapshot: options.db_snapshot.as_ref(),
filesystem: &self.filesystem, filesystem: &self.filesystem,
node_log_path: options.node_log_path.as_ref(), node_log_path: options.node_log_path.as_ref(),
}) })
.await?; .await?;
self.nodes self.nodes.write().await.insert(options.name.clone(), node.clone());
.write()
.await
.insert(options.name.clone(), node.clone());
Ok(node) Ok(node)
} }
async fn spawn_node_from_json( async fn spawn_node_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNode, ProviderError> { ) -> Result<DynNode, ProviderError> {
let deserializable: DeserializableNativeNodeOptions = let deserializable: DeserializableNativeNodeOptions =
serde_json::from_value(json_value.clone())?; serde_json::from_value(json_value.clone())?;
let options = NativeNodeOptions::from_deserializable( let options = NativeNodeOptions::from_deserializable(
&deserializable, &deserializable,
&self.weak, &self.weak,
&self.base_dir, &self.base_dir,
&self.filesystem, &self.filesystem,
); );
let pid = json_value let pid = json_value
.get("process_handle") .get("process_handle")
.and_then(|v| v.as_i64()) .and_then(|v| v.as_i64())
.ok_or_else(|| ProviderError::InvalidConfig("Missing pid field".to_string()))? .ok_or_else(|| ProviderError::InvalidConfig("Missing pid field".to_string()))?
as i32; as i32;
let node = NativeNode::attach_to_live(options, pid).await?; let node = NativeNode::attach_to_live(options, pid).await?;
self.nodes self.nodes.write().await.insert(node.name().to_string(), node.clone());
.write()
.await
.insert(node.name().to_string(), node.clone());
Ok(node) Ok(node)
} }
async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError> { async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError> {
let node_name = if let Some(name) = options.temp_name { let node_name = if let Some(name) = options.temp_name {
name name
} else { } else {
format!("temp-{}", Uuid::new_v4()) format!("temp-{}", Uuid::new_v4())
}; };
// we spawn a node doing nothing but looping so we can execute our commands // we spawn a node doing nothing but looping so we can execute our commands
let temp_node = self let temp_node = self
.spawn_node( .spawn_node(
&SpawnNodeOptions::new(node_name, "bash".to_string()) &SpawnNodeOptions::new(node_name, "bash".to_string())
.args(vec!["-c", "while :; do sleep 1; done"]) .args(vec!["-c", "while :; do sleep 1; done"])
.injected_files(options.injected_files), .injected_files(options.injected_files),
) )
.await?; .await?;
for GenerateFileCommand { for GenerateFileCommand { program, args, env, local_output_path } in options.commands {
program, trace!(
args, "🏗 building file {:?} in path {} with command {} {}",
env, local_output_path.as_os_str(),
local_output_path, self.base_dir.to_string_lossy(),
} in options.commands program,
{ args.join(" ")
trace!( );
"🏗 building file {:?} in path {} with command {} {}", let local_output_full_path = format!(
local_output_path.as_os_str(), "{}{}{}",
self.base_dir.to_string_lossy(), self.base_dir.to_string_lossy(),
program, if local_output_path.starts_with("/") { "" } else { "/" },
args.join(" ") local_output_path.to_string_lossy()
); );
let local_output_full_path = format!(
"{}{}{}",
self.base_dir.to_string_lossy(),
if local_output_path.starts_with("/") {
""
} else {
"/"
},
local_output_path.to_string_lossy()
);
let contents = extract_execution_result( let contents = extract_execution_result(
&temp_node, &temp_node,
RunCommandOptions { program, args, env }, RunCommandOptions { program, args, env },
options.expected_path.as_ref(), options.expected_path.as_ref(),
) )
.await?; .await?;
self.filesystem self.filesystem
.write(local_output_full_path, contents) .write(local_output_full_path, contents)
.await .await
.map_err(|err| ProviderError::FileGenerationFailed(err.into()))?; .map_err(|err| ProviderError::FileGenerationFailed(err.into()))?;
} }
temp_node.destroy().await temp_node.destroy().await
} }
async fn static_setup(&self) -> Result<(), ProviderError> { async fn static_setup(&self) -> Result<(), ProviderError> {
// no static setup exists for native provider // no static setup exists for native provider
todo!() todo!()
} }
async fn destroy(&self) -> Result<(), ProviderError> { async fn destroy(&self) -> Result<(), ProviderError> {
let mut names = vec![]; let mut names = vec![];
for node in self.nodes.read().await.values() { for node in self.nodes.read().await.values() {
node.abort() node.abort()
.await .await
.map_err(|err| ProviderError::DestroyNodeFailed(node.name().to_string(), err))?; .map_err(|err| ProviderError::DestroyNodeFailed(node.name().to_string(), err))?;
names.push(node.name().to_string()); names.push(node.name().to_string());
} }
let mut nodes = self.nodes.write().await; let mut nodes = self.nodes.write().await;
for name in names { for name in names {
nodes.remove(&name); nodes.remove(&name);
} }
if let Some(provider) = self.provider.upgrade() { if let Some(provider) = self.provider.upgrade() {
provider.namespaces.write().await.remove(&self.name); provider.namespaces.write().await.remove(&self.name);
} }
Ok(()) Ok(())
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use support::fs::local::LocalFileSystem; use support::fs::local::LocalFileSystem;
use super::*; use super::*;
use crate::{ use crate::{
types::{GenerateFileCommand, GenerateFilesOptions}, types::{GenerateFileCommand, GenerateFilesOptions},
NativeProvider, Provider, NativeProvider, Provider,
}; };
fn unique_temp_dir() -> PathBuf { fn unique_temp_dir() -> PathBuf {
let mut base = std::env::temp_dir(); let mut base = std::env::temp_dir();
base.push(format!("znet_native_ns_test_{}", uuid::Uuid::new_v4())); base.push(format!("znet_native_ns_test_{}", uuid::Uuid::new_v4()));
base base
} }
#[tokio::test] #[tokio::test]
async fn generate_files_uses_expected_path_when_provided() { async fn generate_files_uses_expected_path_when_provided() {
let fs = LocalFileSystem; let fs = LocalFileSystem;
let provider = NativeProvider::new(fs.clone()); let provider = NativeProvider::new(fs.clone());
let base_dir = unique_temp_dir(); let base_dir = unique_temp_dir();
// Namespace builder will create directory if needed // Namespace builder will create directory if needed
let ns = provider let ns = provider
.create_namespace_with_base_dir(&base_dir) .create_namespace_with_base_dir(&base_dir)
.await .await
.expect("namespace should be created"); .expect("namespace should be created");
// Create a unique on-host path that the native node will write to // Create a unique on-host path that the native node will write to
let expected_path = let expected_path =
std::env::temp_dir().join(format!("znet_expected_{}.json", uuid::Uuid::new_v4())); std::env::temp_dir().join(format!("znet_expected_{}.json", uuid::Uuid::new_v4()));
// Command will write JSON into expected_path; stdout will be something else to ensure we don't read it // Command will write JSON into expected_path; stdout will be something else to ensure we don't read it
let program = "bash".to_string(); let program = "bash".to_string();
let script = format!( let script = format!(
"echo -n '{{\"hello\":\"world\"}}' > {} && echo should_not_be_used", "echo -n '{{\"hello\":\"world\"}}' > {} && echo should_not_be_used",
expected_path.to_string_lossy() expected_path.to_string_lossy()
); );
let args: Vec<String> = vec!["-lc".into(), script]; let args: Vec<String> = vec!["-lc".into(), script];
let out_name = PathBuf::from("result_expected.json"); let out_name = PathBuf::from("result_expected.json");
let cmd = GenerateFileCommand::new(program, out_name.clone()).args(args); let cmd = GenerateFileCommand::new(program, out_name.clone()).args(args);
let options = GenerateFilesOptions::new(vec![cmd], None, Some(expected_path.clone())); let options = GenerateFilesOptions::new(vec![cmd], None, Some(expected_path.clone()));
ns.generate_files(options) ns.generate_files(options).await.expect("generation should succeed");
.await
.expect("generation should succeed");
// Read produced file from namespace base_dir // Read produced file from namespace base_dir
let produced_path = base_dir.join(out_name); let produced_path = base_dir.join(out_name);
let produced = fs let produced = fs.read_to_string(&produced_path).await.expect("should read produced file");
.read_to_string(&produced_path) assert_eq!(produced, "{\"hello\":\"world\"}");
.await }
.expect("should read produced file");
assert_eq!(produced, "{\"hello\":\"world\"}");
}
#[tokio::test] #[tokio::test]
async fn generate_files_uses_stdout_when_expected_path_absent() { async fn generate_files_uses_stdout_when_expected_path_absent() {
let fs = LocalFileSystem; let fs = LocalFileSystem;
let provider = NativeProvider::new(fs.clone()); let provider = NativeProvider::new(fs.clone());
let base_dir = unique_temp_dir(); let base_dir = unique_temp_dir();
let ns = provider let ns = provider
.create_namespace_with_base_dir(&base_dir) .create_namespace_with_base_dir(&base_dir)
.await .await
.expect("namespace should be created"); .expect("namespace should be created");
// Command prints to stdout only // Command prints to stdout only
let program = "bash".to_string(); let program = "bash".to_string();
let args: Vec<String> = vec!["-lc".into(), "echo -n 42".into()]; let args: Vec<String> = vec!["-lc".into(), "echo -n 42".into()];
let out_name = PathBuf::from("result_stdout.txt"); let out_name = PathBuf::from("result_stdout.txt");
let cmd = GenerateFileCommand::new(program, out_name.clone()).args(args); let cmd = GenerateFileCommand::new(program, out_name.clone()).args(args);
let options = GenerateFilesOptions::new(vec![cmd], None, None); let options = GenerateFilesOptions::new(vec![cmd], None, None);
ns.generate_files(options) ns.generate_files(options).await.expect("generation should succeed");
.await
.expect("generation should succeed");
let produced_path = base_dir.join(out_name); let produced_path = base_dir.join(out_name);
let produced = fs let produced = fs.read_to_string(&produced_path).await.expect("should read produced file");
.read_to_string(&produced_path) assert_eq!(produced, "42");
.await }
.expect("should read produced file");
assert_eq!(produced, "42");
}
} }
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -10,133 +10,124 @@ use tokio::sync::RwLock;
use super::namespace::NativeNamespace; use super::namespace::NativeNamespace;
use crate::{ use crate::{
shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider, shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider,
ProviderError, ProviderNamespace, ProviderError, ProviderNamespace,
}; };
pub const PROVIDER_NAME: &str = "native"; pub const PROVIDER_NAME: &str = "native";
pub struct NativeProvider<FS> pub struct NativeProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
weak: Weak<NativeProvider<FS>>, weak: Weak<NativeProvider<FS>>,
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
tmp_dir: PathBuf, tmp_dir: PathBuf,
filesystem: FS, filesystem: FS,
pub(super) namespaces: RwLock<HashMap<String, Arc<NativeNamespace<FS>>>>, pub(super) namespaces: RwLock<HashMap<String, Arc<NativeNamespace<FS>>>>,
} }
impl<FS> NativeProvider<FS> impl<FS> NativeProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone, FS: FileSystem + Send + Sync + Clone,
{ {
pub fn new(filesystem: FS) -> Arc<Self> { pub fn new(filesystem: FS) -> Arc<Self> {
Arc::new_cyclic(|weak| NativeProvider { Arc::new_cyclic(|weak| NativeProvider {
weak: weak.clone(), weak: weak.clone(),
capabilities: ProviderCapabilities { capabilities: ProviderCapabilities {
has_resources: false, has_resources: false,
requires_image: false, requires_image: false,
prefix_with_full_path: true, prefix_with_full_path: true,
use_default_ports_in_cmd: false, use_default_ports_in_cmd: false,
}, },
// NOTE: temp_dir in linux return `/tmp` but on mac something like // NOTE: temp_dir in linux return `/tmp` but on mac something like
// `/var/folders/rz/1cyx7hfj31qgb98d8_cg7jwh0000gn/T/`, having // `/var/folders/rz/1cyx7hfj31qgb98d8_cg7jwh0000gn/T/`, having
// one `trailing slash` and the other no can cause issues if // one `trailing slash` and the other no can cause issues if
// you try to build a fullpath by concatenate. Use Pathbuf to prevent the issue. // you try to build a fullpath by concatenate. Use Pathbuf to prevent the issue.
tmp_dir: std::env::temp_dir(), tmp_dir: std::env::temp_dir(),
filesystem, filesystem,
namespaces: RwLock::new(HashMap::new()), namespaces: RwLock::new(HashMap::new()),
}) })
} }
pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self { pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self {
self.tmp_dir = tmp_dir.into(); self.tmp_dir = tmp_dir.into();
self self
} }
} }
#[async_trait] #[async_trait]
impl<FS> Provider for NativeProvider<FS> impl<FS> Provider for NativeProvider<FS>
where where
FS: FileSystem + Send + Sync + Clone + 'static, FS: FileSystem + Send + Sync + Clone + 'static,
{ {
fn name(&self) -> &str { fn name(&self) -> &str {
PROVIDER_NAME PROVIDER_NAME
} }
fn capabilities(&self) -> &ProviderCapabilities { fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities &self.capabilities
} }
async fn namespaces(&self) -> HashMap<String, DynNamespace> { async fn namespaces(&self) -> HashMap<String, DynNamespace> {
self.namespaces self.namespaces
.read() .read()
.await .await
.iter() .iter()
.map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace)) .map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace))
.collect() .collect()
} }
async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> { async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> {
let namespace = NativeNamespace::new( let namespace = NativeNamespace::new(
&self.weak, &self.weak,
&self.tmp_dir, &self.tmp_dir,
&self.capabilities, &self.capabilities,
&self.filesystem, &self.filesystem,
None, None,
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
async fn create_namespace_with_base_dir( async fn create_namespace_with_base_dir(
&self, &self,
base_dir: &Path, base_dir: &Path,
) -> Result<DynNamespace, ProviderError> { ) -> Result<DynNamespace, ProviderError> {
let namespace = NativeNamespace::new( let namespace = NativeNamespace::new(
&self.weak, &self.weak,
&self.tmp_dir, &self.tmp_dir,
&self.capabilities, &self.capabilities,
&self.filesystem, &self.filesystem,
Some(base_dir), Some(base_dir),
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
async fn create_namespace_from_json( async fn create_namespace_from_json(
&self, &self,
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<DynNamespace, ProviderError> { ) -> Result<DynNamespace, ProviderError> {
let (base_dir, name) = extract_namespace_info(json_value)?; let (base_dir, name) = extract_namespace_info(json_value)?;
let namespace = NativeNamespace::attach_to_live( let namespace = NativeNamespace::attach_to_live(
&self.weak, &self.weak,
&self.capabilities, &self.capabilities,
&self.filesystem, &self.filesystem,
&base_dir, &base_dir,
&name, &name,
) )
.await?; .await?;
self.namespaces self.namespaces.write().await.insert(namespace.name().to_string(), namespace.clone());
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace) Ok(namespace)
} }
} }
@@ -6,74 +6,69 @@ use crate::{types::RunCommandOptions, DynNode, ProviderError};
/// Check if we are running in `CI` by checking the 'RUN_IN_CI' env var /// Check if we are running in `CI` by checking the 'RUN_IN_CI' env var
pub fn running_in_ci() -> bool { pub fn running_in_ci() -> bool {
env::var("RUN_IN_CI").unwrap_or_default() == "1" env::var("RUN_IN_CI").unwrap_or_default() == "1"
} }
/// Executes a command on a temporary node and extracts the execution result either from the /// Executes a command on a temporary node and extracts the execution result either from the
/// standard output or a file. /// standard output or a file.
pub async fn extract_execution_result( pub async fn extract_execution_result(
temp_node: &DynNode, temp_node: &DynNode,
options: RunCommandOptions, options: RunCommandOptions,
expected_path: Option<&PathBuf>, expected_path: Option<&PathBuf>,
) -> Result<String, ProviderError> { ) -> Result<String, ProviderError> {
let output_contents = temp_node let output_contents = temp_node
.run_command(options) .run_command(options)
.await? .await?
.map_err(|(_, msg)| ProviderError::FileGenerationFailed(anyhow!("{msg}")))?; .map_err(|(_, msg)| ProviderError::FileGenerationFailed(anyhow!("{msg}")))?;
// If an expected_path is provided, read the file contents from inside the container // If an expected_path is provided, read the file contents from inside the container
if let Some(expected_path) = expected_path.as_ref() { if let Some(expected_path) = expected_path.as_ref() {
Ok(temp_node Ok(temp_node
.run_command( .run_command(
RunCommandOptions::new("cat") RunCommandOptions::new("cat")
.args(vec![expected_path.to_string_lossy().to_string()]), .args(vec![expected_path.to_string_lossy().to_string()]),
) )
.await? .await?
.map_err(|(_, msg)| { .map_err(|(_, msg)| {
ProviderError::FileGenerationFailed(anyhow!(format!( ProviderError::FileGenerationFailed(anyhow!(format!(
"failed reading expected_path {}: {}", "failed reading expected_path {}: {}",
expected_path.display(), expected_path.display(),
msg msg
))) )))
})?) })?)
} else { } else {
Ok(output_contents) Ok(output_contents)
} }
} }
pub fn extract_namespace_info( pub fn extract_namespace_info(
json_value: &serde_json::Value, json_value: &serde_json::Value,
) -> Result<(PathBuf, String), ProviderError> { ) -> Result<(PathBuf, String), ProviderError> {
let base_dir = json_value let base_dir =
.get("local_base_dir") json_value.get("local_base_dir").and_then(|v| v.as_str()).map(PathBuf::from).ok_or(
.and_then(|v| v.as_str()) ProviderError::InvalidConfig(
.map(PathBuf::from) "`field local_base_dir` is missing from zombie.json".to_string(),
.ok_or(ProviderError::InvalidConfig( ),
"`field local_base_dir` is missing from zombie.json".to_string(), )?;
))?;
let name = let name = json_value.get("ns").and_then(|v| v.as_str()).ok_or(
json_value ProviderError::InvalidConfig("field `ns` is missing from zombie.json".to_string()),
.get("ns") )?;
.and_then(|v| v.as_str())
.ok_or(ProviderError::InvalidConfig(
"field `ns` is missing from zombie.json".to_string(),
))?;
Ok((base_dir, name.to_string())) Ok((base_dir, name.to_string()))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn check_runing_in_ci_env_var() { fn check_runing_in_ci_env_var() {
assert!(!running_in_ci()); assert!(!running_in_ci());
// now set the env var // now set the env var
env::set_var("RUN_IN_CI", "1"); env::set_var("RUN_IN_CI", "1");
assert!(running_in_ci()); assert!(running_in_ci());
// reset // reset
env::set_var("RUN_IN_CI", ""); env::set_var("RUN_IN_CI", "");
} }
} }
+286 -297
View File
@@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::ExitStatus, process::ExitStatus,
}; };
use configuration::{shared::resources::Resources, types::AssetLocation}; use configuration::{shared::resources::Resources, types::AssetLocation};
@@ -13,363 +13,352 @@ pub type ExecutionResult = Result<String, (ExitStatus, String)>;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ProviderCapabilities { pub struct ProviderCapabilities {
// default ports internal // default ports internal
/// Ensure that we have an image for each node (k8s/podman/docker) /// Ensure that we have an image for each node (k8s/podman/docker)
pub requires_image: bool, pub requires_image: bool,
/// Allow to customize the resources through manifest (k8s). /// Allow to customize the resources through manifest (k8s).
pub has_resources: bool, pub has_resources: bool,
/// Used in native to prefix filepath with fullpath /// Used in native to prefix filepath with fullpath
pub prefix_with_full_path: bool, pub prefix_with_full_path: bool,
/// Use default ports in node cmd/args. /// Use default ports in node cmd/args.
/// NOTE: generally used in k8s/dockers since the images expose those ports. /// NOTE: generally used in k8s/dockers since the images expose those ports.
pub use_default_ports_in_cmd: bool, pub use_default_ports_in_cmd: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SpawnNodeOptions { pub struct SpawnNodeOptions {
/// Name of the node /// Name of the node
pub name: String, pub name: String,
/// Image of the node (IFF is supported by the provider) /// Image of the node (IFF is supported by the provider)
pub image: Option<String>, pub image: Option<String>,
/// Resources to apply to the node (IFF is supported by the provider) /// Resources to apply to the node (IFF is supported by the provider)
pub resources: Option<Resources>, pub resources: Option<Resources>,
/// Main command to execute /// Main command to execute
pub program: String, pub program: String,
/// Arguments to pass to the main command /// Arguments to pass to the main command
pub args: Vec<String>, pub args: Vec<String>,
/// Environment to set when running the `program` /// Environment to set when running the `program`
pub env: Vec<(String, String)>, pub env: Vec<(String, String)>,
// TODO: rename startup_files // TODO: rename startup_files
/// Files to inject at startup /// Files to inject at startup
pub injected_files: Vec<TransferedFile>, pub injected_files: Vec<TransferedFile>,
/// Paths to create before start the node (e.g keystore) /// Paths to create before start the node (e.g keystore)
/// should be created with `create_dir_all` in order /// should be created with `create_dir_all` in order
/// to create the full path even when we have missing parts /// to create the full path even when we have missing parts
pub created_paths: Vec<PathBuf>, pub created_paths: Vec<PathBuf>,
/// Database snapshot to be injected (should be a tgz file) /// Database snapshot to be injected (should be a tgz file)
/// Could be a local or remote asset /// Could be a local or remote asset
pub db_snapshot: Option<AssetLocation>, pub db_snapshot: Option<AssetLocation>,
pub port_mapping: Option<HashMap<Port, Port>>, pub port_mapping: Option<HashMap<Port, Port>>,
/// Optionally specify a log path for the node /// Optionally specify a log path for the node
pub node_log_path: Option<PathBuf>, pub node_log_path: Option<PathBuf>,
} }
impl SpawnNodeOptions { impl SpawnNodeOptions {
pub fn new<S>(name: S, program: S) -> Self pub fn new<S>(name: S, program: S) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
{ {
Self { Self {
name: name.as_ref().to_string(), name: name.as_ref().to_string(),
image: None, image: None,
resources: None, resources: None,
program: program.as_ref().to_string(), program: program.as_ref().to_string(),
args: vec![], args: vec![],
env: vec![], env: vec![],
injected_files: vec![], injected_files: vec![],
created_paths: vec![], created_paths: vec![],
db_snapshot: None, db_snapshot: None,
port_mapping: None, port_mapping: None,
node_log_path: None, node_log_path: None,
} }
} }
pub fn image<S>(mut self, image: S) -> Self pub fn image<S>(mut self, image: S) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
{ {
self.image = Some(image.as_ref().to_string()); self.image = Some(image.as_ref().to_string());
self self
} }
pub fn resources(mut self, resources: Resources) -> Self { pub fn resources(mut self, resources: Resources) -> Self {
self.resources = Some(resources); self.resources = Some(resources);
self self
} }
pub fn db_snapshot(mut self, db_snap: Option<AssetLocation>) -> Self { pub fn db_snapshot(mut self, db_snap: Option<AssetLocation>) -> Self {
self.db_snapshot = db_snap; self.db_snapshot = db_snap;
self self
} }
pub fn args<S, I>(mut self, args: I) -> Self pub fn args<S, I>(mut self, args: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
{ {
self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect(); self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect();
self self
} }
pub fn env<S, I>(mut self, env: I) -> Self pub fn env<S, I>(mut self, env: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = (S, S)>, I: IntoIterator<Item = (S, S)>,
{ {
self.env = env self.env = env
.into_iter() .into_iter()
.map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string())) .map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string()))
.collect(); .collect();
self self
} }
pub fn injected_files<I>(mut self, injected_files: I) -> Self pub fn injected_files<I>(mut self, injected_files: I) -> Self
where where
I: IntoIterator<Item = TransferedFile>, I: IntoIterator<Item = TransferedFile>,
{ {
self.injected_files = injected_files.into_iter().collect(); self.injected_files = injected_files.into_iter().collect();
self self
} }
pub fn created_paths<P, I>(mut self, created_paths: I) -> Self pub fn created_paths<P, I>(mut self, created_paths: I) -> Self
where where
P: AsRef<Path>, P: AsRef<Path>,
I: IntoIterator<Item = P>, I: IntoIterator<Item = P>,
{ {
self.created_paths = created_paths self.created_paths = created_paths.into_iter().map(|path| path.as_ref().into()).collect();
.into_iter() self
.map(|path| path.as_ref().into()) }
.collect();
self
}
pub fn port_mapping(mut self, ports: HashMap<Port, Port>) -> Self { pub fn port_mapping(mut self, ports: HashMap<Port, Port>) -> Self {
self.port_mapping = Some(ports); self.port_mapping = Some(ports);
self self
} }
pub fn node_log_path(mut self, path: Option<PathBuf>) -> Self { pub fn node_log_path(mut self, path: Option<PathBuf>) -> Self {
self.node_log_path = path; self.node_log_path = path;
self self
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct GenerateFileCommand { pub struct GenerateFileCommand {
pub program: String, pub program: String,
pub args: Vec<String>, pub args: Vec<String>,
pub env: Vec<(String, String)>, pub env: Vec<(String, String)>,
pub local_output_path: PathBuf, pub local_output_path: PathBuf,
} }
impl GenerateFileCommand { impl GenerateFileCommand {
pub fn new<S, P>(program: S, local_output_path: P) -> Self pub fn new<S, P>(program: S, local_output_path: P) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
P: AsRef<Path>, P: AsRef<Path>,
{ {
Self { Self {
program: program.as_ref().to_string(), program: program.as_ref().to_string(),
args: vec![], args: vec![],
env: vec![], env: vec![],
local_output_path: local_output_path.as_ref().into(), local_output_path: local_output_path.as_ref().into(),
} }
} }
pub fn args<S, I>(mut self, args: I) -> Self pub fn args<S, I>(mut self, args: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
{ {
self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect(); self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect();
self self
} }
pub fn env<S, I>(mut self, env: I) -> Self pub fn env<S, I>(mut self, env: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = (S, S)>, I: IntoIterator<Item = (S, S)>,
{ {
self.env = env self.env = env
.into_iter() .into_iter()
.map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string())) .map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string()))
.collect(); .collect();
self self
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct GenerateFilesOptions { pub struct GenerateFilesOptions {
pub commands: Vec<GenerateFileCommand>, pub commands: Vec<GenerateFileCommand>,
pub image: Option<String>, pub image: Option<String>,
pub injected_files: Vec<TransferedFile>, pub injected_files: Vec<TransferedFile>,
// Allow to control the name of the node used to create the files. // Allow to control the name of the node used to create the files.
pub temp_name: Option<String>, pub temp_name: Option<String>,
pub expected_path: Option<PathBuf>, pub expected_path: Option<PathBuf>,
} }
impl GenerateFilesOptions { impl GenerateFilesOptions {
pub fn new<I>(commands: I, image: Option<String>, expected_path: Option<PathBuf>) -> Self pub fn new<I>(commands: I, image: Option<String>, expected_path: Option<PathBuf>) -> Self
where where
I: IntoIterator<Item = GenerateFileCommand>, I: IntoIterator<Item = GenerateFileCommand>,
{ {
Self { Self {
commands: commands.into_iter().collect(), commands: commands.into_iter().collect(),
injected_files: vec![], injected_files: vec![],
image, image,
temp_name: None, temp_name: None,
expected_path, expected_path,
} }
} }
pub fn with_files<I>( pub fn with_files<I>(
commands: I, commands: I,
image: Option<String>, image: Option<String>,
injected_files: &[TransferedFile], injected_files: &[TransferedFile],
expected_path: Option<PathBuf>, expected_path: Option<PathBuf>,
) -> Self ) -> Self
where where
I: IntoIterator<Item = GenerateFileCommand>, I: IntoIterator<Item = GenerateFileCommand>,
{ {
Self { Self {
commands: commands.into_iter().collect(), commands: commands.into_iter().collect(),
injected_files: injected_files.into(), injected_files: injected_files.into(),
image, image,
temp_name: None, temp_name: None,
expected_path, expected_path,
} }
} }
pub fn image<S>(mut self, image: S) -> Self pub fn image<S>(mut self, image: S) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
{ {
self.image = Some(image.as_ref().to_string()); self.image = Some(image.as_ref().to_string());
self self
} }
pub fn injected_files<I>(mut self, injected_files: I) -> Self pub fn injected_files<I>(mut self, injected_files: I) -> Self
where where
I: IntoIterator<Item = TransferedFile>, I: IntoIterator<Item = TransferedFile>,
{ {
self.injected_files = injected_files.into_iter().collect(); self.injected_files = injected_files.into_iter().collect();
self self
} }
pub fn temp_name(mut self, name: impl Into<String>) -> Self { pub fn temp_name(mut self, name: impl Into<String>) -> Self {
self.temp_name = Some(name.into()); self.temp_name = Some(name.into());
self self
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct RunCommandOptions { pub struct RunCommandOptions {
pub program: String, pub program: String,
pub args: Vec<String>, pub args: Vec<String>,
pub env: Vec<(String, String)>, pub env: Vec<(String, String)>,
} }
impl RunCommandOptions { impl RunCommandOptions {
pub fn new<S>(program: S) -> Self pub fn new<S>(program: S) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
{ {
Self { Self { program: program.as_ref().to_string(), args: vec![], env: vec![] }
program: program.as_ref().to_string(), }
args: vec![],
env: vec![],
}
}
pub fn args<S, I>(mut self, args: I) -> Self pub fn args<S, I>(mut self, args: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
{ {
self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect(); self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect();
self self
} }
pub fn env<S, I>(mut self, env: I) -> Self pub fn env<S, I>(mut self, env: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = (S, S)>, I: IntoIterator<Item = (S, S)>,
{ {
self.env = env self.env = env
.into_iter() .into_iter()
.map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string())) .map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string()))
.collect(); .collect();
self self
} }
} }
pub struct RunScriptOptions { pub struct RunScriptOptions {
pub local_script_path: PathBuf, pub local_script_path: PathBuf,
pub args: Vec<String>, pub args: Vec<String>,
pub env: Vec<(String, String)>, pub env: Vec<(String, String)>,
} }
impl RunScriptOptions { impl RunScriptOptions {
pub fn new<P>(local_script_path: P) -> Self pub fn new<P>(local_script_path: P) -> Self
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Self { Self { local_script_path: local_script_path.as_ref().into(), args: vec![], env: vec![] }
local_script_path: local_script_path.as_ref().into(), }
args: vec![],
env: vec![],
}
}
pub fn args<S, I>(mut self, args: I) -> Self pub fn args<S, I>(mut self, args: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
{ {
self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect(); self.args = args.into_iter().map(|s| s.as_ref().to_string()).collect();
self self
} }
pub fn env<S, I>(mut self, env: I) -> Self pub fn env<S, I>(mut self, env: I) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
I: IntoIterator<Item = (S, S)>, I: IntoIterator<Item = (S, S)>,
{ {
self.env = env self.env = env
.into_iter() .into_iter()
.map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string())) .map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string()))
.collect(); .collect();
self self
} }
} }
// TODO(team): I think we can rename it to FileMap? // TODO(team): I think we can rename it to FileMap?
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferedFile { pub struct TransferedFile {
pub local_path: PathBuf, pub local_path: PathBuf,
pub remote_path: PathBuf, pub remote_path: PathBuf,
// TODO: Can be narrowed to have strict typing on this? // TODO: Can be narrowed to have strict typing on this?
pub mode: String, pub mode: String,
} }
impl TransferedFile { impl TransferedFile {
pub fn new<P>(local_path: P, remote_path: P) -> Self pub fn new<P>(local_path: P, remote_path: P) -> Self
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Self { Self {
local_path: local_path.as_ref().into(), local_path: local_path.as_ref().into(),
remote_path: remote_path.as_ref().into(), remote_path: remote_path.as_ref().into(),
mode: "0644".to_string(), // default to rw-r--r-- mode: "0644".to_string(), // default to rw-r--r--
} }
} }
pub fn mode<S>(mut self, mode: S) -> Self pub fn mode<S>(mut self, mode: S) -> Self
where where
S: AsRef<str>, S: AsRef<str>,
{ {
self.mode = mode.as_ref().to_string(); self.mode = mode.as_ref().to_string();
self self
} }
} }
impl std::fmt::Display for TransferedFile { impl std::fmt::Display for TransferedFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
"File to transfer (local: {}, remote: {})", "File to transfer (local: {}, remote: {})",
self.local_path.display(), self.local_path.display(),
self.remote_path.display() self.remote_path.display()
) )
} }
} }
+47 -47
View File
@@ -2,8 +2,8 @@
use std::{env, future::Future, path::PathBuf, pin::Pin}; use std::{env, future::Future, path::PathBuf, pin::Pin};
use crate::{ use crate::{
AttachToLive, AttachToLiveNetwork, LocalFileSystem, Network, NetworkConfig, NetworkConfigExt, AttachToLive, AttachToLiveNetwork, LocalFileSystem, Network, NetworkConfig, NetworkConfigExt,
OrchestratorError, OrchestratorError,
}; };
const DEFAULT_POLKADOT_IMAGE: &str = "docker.io/parity/polkadot:latest"; const DEFAULT_POLKADOT_IMAGE: &str = "docker.io/parity/polkadot:latest";
@@ -11,80 +11,80 @@ const DEFAULT_CUMULUS_IMAGE: &str = "docker.io/parity/polkadot-parachain:latest"
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Images { pub struct Images {
pub polkadot: String, pub polkadot: String,
pub cumulus: String, pub cumulus: String,
} }
impl Images { impl Images {
/// Alias for polkadot field - returns reference to pezkuwi/polkadot image /// Alias for polkadot field - returns reference to pezkuwi/polkadot image
pub fn pezkuwi(&self) -> &str { pub fn pezkuwi(&self) -> &str {
&self.polkadot &self.polkadot
} }
/// Alias for cumulus field - returns reference to pezcumulus/cumulus image /// Alias for cumulus field - returns reference to pezcumulus/cumulus image
pub fn pezcumulus(&self) -> &str { pub fn pezcumulus(&self) -> &str {
&self.cumulus &self.cumulus
} }
} }
pub enum Provider { pub enum Provider {
Native, Native,
K8s, K8s,
Docker, Docker,
} }
impl Provider { impl Provider {
pub fn get_spawn_fn( pub fn get_spawn_fn(
&self, &self,
) -> fn(NetworkConfig) -> Pin<Box<dyn Future<Output = SpawnResult> + Send>> { ) -> fn(NetworkConfig) -> Pin<Box<dyn Future<Output = SpawnResult> + Send>> {
match self { match self {
Provider::Native => NetworkConfigExt::spawn_native, Provider::Native => NetworkConfigExt::spawn_native,
Provider::K8s => NetworkConfigExt::spawn_k8s, Provider::K8s => NetworkConfigExt::spawn_k8s,
Provider::Docker => NetworkConfigExt::spawn_docker, Provider::Docker => NetworkConfigExt::spawn_docker,
} }
} }
} }
// Use `docker` as default provider // Use `docker` as default provider
impl From<String> for Provider { impl From<String> for Provider {
fn from(value: String) -> Self { fn from(value: String) -> Self {
match value.to_ascii_lowercase().as_ref() { match value.to_ascii_lowercase().as_ref() {
"native" => Provider::Native, "native" => Provider::Native,
"k8s" => Provider::K8s, "k8s" => Provider::K8s,
_ => Provider::Docker, // default provider _ => Provider::Docker, // default provider
} }
} }
} }
pub fn get_images_from_env() -> Images { pub fn get_images_from_env() -> Images {
let polkadot = env::var("POLKADOT_IMAGE").unwrap_or(DEFAULT_POLKADOT_IMAGE.into()); let polkadot = env::var("POLKADOT_IMAGE").unwrap_or(DEFAULT_POLKADOT_IMAGE.into());
let cumulus = env::var("CUMULUS_IMAGE").unwrap_or(DEFAULT_CUMULUS_IMAGE.into()); let cumulus = env::var("CUMULUS_IMAGE").unwrap_or(DEFAULT_CUMULUS_IMAGE.into());
Images { polkadot, cumulus } Images { polkadot, cumulus }
} }
pub fn get_provider_from_env() -> Provider { pub fn get_provider_from_env() -> Provider {
env::var("ZOMBIE_PROVIDER").unwrap_or_default().into() env::var("ZOMBIE_PROVIDER").unwrap_or_default().into()
} }
pub type SpawnResult = Result<Network<LocalFileSystem>, OrchestratorError>; pub type SpawnResult = Result<Network<LocalFileSystem>, OrchestratorError>;
pub fn get_spawn_fn() -> fn(NetworkConfig) -> Pin<Box<dyn Future<Output = SpawnResult> + Send>> { pub fn get_spawn_fn() -> fn(NetworkConfig) -> Pin<Box<dyn Future<Output = SpawnResult> + Send>> {
let provider = get_provider_from_env(); let provider = get_provider_from_env();
match provider { match provider {
Provider::Native => NetworkConfigExt::spawn_native, Provider::Native => NetworkConfigExt::spawn_native,
Provider::K8s => NetworkConfigExt::spawn_k8s, Provider::K8s => NetworkConfigExt::spawn_k8s,
Provider::Docker => NetworkConfigExt::spawn_docker, Provider::Docker => NetworkConfigExt::spawn_docker,
} }
} }
pub type AttachResult = Result<Network<LocalFileSystem>, OrchestratorError>; pub type AttachResult = Result<Network<LocalFileSystem>, OrchestratorError>;
pub fn get_attach_fn() -> fn(PathBuf) -> Pin<Box<dyn Future<Output = AttachResult> + Send>> { pub fn get_attach_fn() -> fn(PathBuf) -> Pin<Box<dyn Future<Output = AttachResult> + Send>> {
let provider = get_provider_from_env(); let provider = get_provider_from_env();
match provider { match provider {
Provider::Native => AttachToLiveNetwork::attach_native, Provider::Native => AttachToLiveNetwork::attach_native,
Provider::K8s => AttachToLiveNetwork::attach_k8s, Provider::K8s => AttachToLiveNetwork::attach_k8s,
Provider::Docker => AttachToLiveNetwork::attach_docker, Provider::Docker => AttachToLiveNetwork::attach_docker,
} }
} }
+86 -86
View File
@@ -2,20 +2,20 @@ use std::path::PathBuf;
use async_trait::async_trait; use async_trait::async_trait;
pub use configuration::{ pub use configuration::{
GlobalSettings, GlobalSettingsBuilder, NetworkConfig, NetworkConfigBuilder, GlobalSettings, GlobalSettingsBuilder, NetworkConfig, NetworkConfigBuilder,
RegistrationStrategy, WithRelaychain, RegistrationStrategy, WithRelaychain,
}; };
pub use orchestrator::{ pub use orchestrator::{
errors::OrchestratorError, errors::OrchestratorError,
network::{node::NetworkNode, Network}, network::{node::NetworkNode, Network},
pezsc_chain_spec, AddCollatorOptions, AddNodeOptions, Orchestrator, pezsc_chain_spec, AddCollatorOptions, AddNodeOptions, Orchestrator,
}; };
// Helpers used for interact with the network // Helpers used for interact with the network
pub mod tx_helper { pub mod tx_helper {
pub use orchestrator::{ pub use orchestrator::{
network::chain_upgrade::ChainUpgrade, shared::types::RuntimeUpgradeOptions, network::chain_upgrade::ChainUpgrade, shared::types::RuntimeUpgradeOptions,
}; };
} }
use provider::{DockerProvider, KubernetesProvider, NativeProvider}; use provider::{DockerProvider, KubernetesProvider, NativeProvider};
@@ -32,100 +32,100 @@ pub use pezkuwi_subxt_signer as subxt_signer;
#[async_trait] #[async_trait]
pub trait NetworkConfigExt { pub trait NetworkConfigExt {
/// Spawns a network using the native or k8s provider. /// Spawns a network using the native or k8s provider.
/// ///
/// # Example: /// # Example:
/// ```rust /// ```rust
/// # use zombienet_sdk::{NetworkConfig, NetworkConfigExt}; /// # use zombienet_sdk::{NetworkConfig, NetworkConfigExt};
/// # async fn example() -> Result<(), zombienet_sdk::OrchestratorError> { /// # async fn example() -> Result<(), zombienet_sdk::OrchestratorError> {
/// let network = NetworkConfig::load_from_toml("config.toml")? /// let network = NetworkConfig::load_from_toml("config.toml")?
/// .spawn_native() /// .spawn_native()
/// .await?; /// .await?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
async fn spawn_native(self) -> Result<Network<LocalFileSystem>, OrchestratorError>; async fn spawn_native(self) -> Result<Network<LocalFileSystem>, OrchestratorError>;
async fn spawn_k8s(self) -> Result<Network<LocalFileSystem>, OrchestratorError>; async fn spawn_k8s(self) -> Result<Network<LocalFileSystem>, OrchestratorError>;
async fn spawn_docker(self) -> Result<Network<LocalFileSystem>, OrchestratorError>; async fn spawn_docker(self) -> Result<Network<LocalFileSystem>, OrchestratorError>;
} }
#[async_trait] #[async_trait]
pub trait AttachToLive { pub trait AttachToLive {
/// Attaches to a running live network using the native, docker or k8s provider. /// Attaches to a running live network using the native, docker or k8s provider.
/// ///
/// # Example: /// # Example:
/// ```rust /// ```rust
/// # use zombienet_sdk::{AttachToLive, AttachToLiveNetwork}; /// # use zombienet_sdk::{AttachToLive, AttachToLiveNetwork};
/// # use std::path::PathBuf; /// # use std::path::PathBuf;
/// # async fn example() -> Result<(), zombienet_sdk::OrchestratorError> { /// # async fn example() -> Result<(), zombienet_sdk::OrchestratorError> {
/// let zombie_json_path = PathBuf::from("some/path/zombie.json"); /// let zombie_json_path = PathBuf::from("some/path/zombie.json");
/// let network = AttachToLiveNetwork::attach_native(zombie_json_path).await?; /// let network = AttachToLiveNetwork::attach_native(zombie_json_path).await?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
async fn attach_native( async fn attach_native(
zombie_json_path: PathBuf, zombie_json_path: PathBuf,
) -> Result<Network<LocalFileSystem>, OrchestratorError>; ) -> Result<Network<LocalFileSystem>, OrchestratorError>;
async fn attach_k8s( async fn attach_k8s(
zombie_json_path: PathBuf, zombie_json_path: PathBuf,
) -> Result<Network<LocalFileSystem>, OrchestratorError>; ) -> Result<Network<LocalFileSystem>, OrchestratorError>;
async fn attach_docker( async fn attach_docker(
zombie_json_path: PathBuf, zombie_json_path: PathBuf,
) -> Result<Network<LocalFileSystem>, OrchestratorError>; ) -> Result<Network<LocalFileSystem>, OrchestratorError>;
} }
#[async_trait] #[async_trait]
impl NetworkConfigExt for NetworkConfig { impl NetworkConfigExt for NetworkConfig {
async fn spawn_native(self) -> Result<Network<LocalFileSystem>, OrchestratorError> { async fn spawn_native(self) -> Result<Network<LocalFileSystem>, OrchestratorError> {
let filesystem = LocalFileSystem; let filesystem = LocalFileSystem;
let provider = NativeProvider::new(filesystem.clone()); let provider = NativeProvider::new(filesystem.clone());
let orchestrator = Orchestrator::new(filesystem, provider); let orchestrator = Orchestrator::new(filesystem, provider);
orchestrator.spawn(self).await orchestrator.spawn(self).await
} }
async fn spawn_k8s(self) -> Result<Network<LocalFileSystem>, OrchestratorError> { async fn spawn_k8s(self) -> Result<Network<LocalFileSystem>, OrchestratorError> {
let filesystem = LocalFileSystem; let filesystem = LocalFileSystem;
let provider = KubernetesProvider::new(filesystem.clone()).await; let provider = KubernetesProvider::new(filesystem.clone()).await;
let orchestrator = Orchestrator::new(filesystem, provider); let orchestrator = Orchestrator::new(filesystem, provider);
orchestrator.spawn(self).await orchestrator.spawn(self).await
} }
async fn spawn_docker(self) -> Result<Network<LocalFileSystem>, OrchestratorError> { async fn spawn_docker(self) -> Result<Network<LocalFileSystem>, OrchestratorError> {
let filesystem = LocalFileSystem; let filesystem = LocalFileSystem;
let provider = DockerProvider::new(filesystem.clone()).await; let provider = DockerProvider::new(filesystem.clone()).await;
let orchestrator = Orchestrator::new(filesystem, provider); let orchestrator = Orchestrator::new(filesystem, provider);
orchestrator.spawn(self).await orchestrator.spawn(self).await
} }
} }
pub struct AttachToLiveNetwork; pub struct AttachToLiveNetwork;
#[async_trait] #[async_trait]
impl AttachToLive for AttachToLiveNetwork { impl AttachToLive for AttachToLiveNetwork {
async fn attach_native( async fn attach_native(
zombie_json_path: PathBuf, zombie_json_path: PathBuf,
) -> Result<Network<LocalFileSystem>, OrchestratorError> { ) -> Result<Network<LocalFileSystem>, OrchestratorError> {
let filesystem = LocalFileSystem; let filesystem = LocalFileSystem;
let provider = NativeProvider::new(filesystem.clone()); let provider = NativeProvider::new(filesystem.clone());
let orchestrator = Orchestrator::new(filesystem, provider); let orchestrator = Orchestrator::new(filesystem, provider);
orchestrator.attach_to_live(zombie_json_path.as_ref()).await orchestrator.attach_to_live(zombie_json_path.as_ref()).await
} }
async fn attach_k8s( async fn attach_k8s(
zombie_json_path: PathBuf, zombie_json_path: PathBuf,
) -> Result<Network<LocalFileSystem>, OrchestratorError> { ) -> Result<Network<LocalFileSystem>, OrchestratorError> {
let filesystem = LocalFileSystem; let filesystem = LocalFileSystem;
let provider = KubernetesProvider::new(filesystem.clone()).await; let provider = KubernetesProvider::new(filesystem.clone()).await;
let orchestrator = Orchestrator::new(filesystem, provider); let orchestrator = Orchestrator::new(filesystem, provider);
orchestrator.attach_to_live(zombie_json_path.as_ref()).await orchestrator.attach_to_live(zombie_json_path.as_ref()).await
} }
async fn attach_docker( async fn attach_docker(
zombie_json_path: PathBuf, zombie_json_path: PathBuf,
) -> Result<Network<LocalFileSystem>, OrchestratorError> { ) -> Result<Network<LocalFileSystem>, OrchestratorError> {
let filesystem = LocalFileSystem; let filesystem = LocalFileSystem;
let provider = DockerProvider::new(filesystem.clone()).await; let provider = DockerProvider::new(filesystem.clone()).await;
let orchestrator = Orchestrator::new(filesystem, provider); let orchestrator = Orchestrator::new(filesystem, provider);
orchestrator.attach_to_live(zombie_json_path.as_ref()).await orchestrator.attach_to_live(zombie_json_path.as_ref()).await
} }
} }
@@ -5,9 +5,9 @@ const BEST_BLOCK_METRIC: &str = "block_height{status=\"best\"}";
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn rococo_local_with_omni_node_and_wasm_runtime() { async fn rococo_local_with_omni_node_and_wasm_runtime() {
let _ = tracing_subscriber::fmt::try_init(); let _ = tracing_subscriber::fmt::try_init();
let config = NetworkConfigBuilder::new() let config = NetworkConfigBuilder::new()
.with_relaychain(|relaychain| { .with_relaychain(|relaychain| {
relaychain relaychain
.with_chain("rococo-local") .with_chain("rococo-local")
@@ -29,47 +29,32 @@ async fn rococo_local_with_omni_node_and_wasm_runtime() {
.build() .build()
.unwrap(); .unwrap();
let spawn_fn = get_spawn_fn(); let spawn_fn = get_spawn_fn();
let network = spawn_fn(config).await.unwrap(); let network = spawn_fn(config).await.unwrap();
println!("🚀🚀🚀🚀 network deployed"); println!("🚀🚀🚀🚀 network deployed");
// wait 2 blocks // wait 2 blocks
let alice = network.get_node("alice").unwrap(); let alice = network.get_node("alice").unwrap();
assert!(alice assert!(alice.wait_metric(BEST_BLOCK_METRIC, |b| b > 2_f64).await.is_ok());
.wait_metric(BEST_BLOCK_METRIC, |b| b > 2_f64)
.await
.is_ok());
// omni-collator-1 // omni-collator-1
let collator = network.get_node("omni-collator-1").unwrap(); let collator = network.get_node("omni-collator-1").unwrap();
let client = collator let client = collator.wait_client::<subxt::PolkadotConfig>().await.unwrap();
.wait_client::<subxt::PolkadotConfig>()
.await
.unwrap();
// wait 1 blocks // wait 1 blocks
let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1); let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1);
while let Some(block) = blocks.next().await { while let Some(block) = blocks.next().await {
println!( println!("Block (omni-collator-1) #{}", block.unwrap().header().number);
"Block (omni-collator-1) #{}", }
block.unwrap().header().number
);
}
// omni-collator-2 // omni-collator-2
let collator = network.get_node("omni-collator-2").unwrap(); let collator = network.get_node("omni-collator-2").unwrap();
let client = collator let client = collator.wait_client::<subxt::PolkadotConfig>().await.unwrap();
.wait_client::<subxt::PolkadotConfig>()
.await
.unwrap();
// wait 1 blocks // wait 1 blocks
let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1); let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1);
while let Some(block) = blocks.next().await { while let Some(block) = blocks.next().await {
println!( println!("Block (omni-collator-2) #{}", block.unwrap().header().number);
"Block (omni-collator-2) #{}", }
block.unwrap().header().number
);
}
} }
@@ -5,9 +5,9 @@ const BEST_BLOCK_METRIC: &str = "block_height{status=\"best\"}";
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn polkadot_local_with_chain_spec_runtime() { async fn polkadot_local_with_chain_spec_runtime() {
let _ = tracing_subscriber::fmt::try_init(); let _ = tracing_subscriber::fmt::try_init();
let config = NetworkConfigBuilder::new() let config = NetworkConfigBuilder::new()
.with_relaychain(|relaychain| { .with_relaychain(|relaychain| {
relaychain relaychain
.with_chain("polkadot-local") .with_chain("polkadot-local")
@@ -30,47 +30,32 @@ async fn polkadot_local_with_chain_spec_runtime() {
.build() .build()
.unwrap(); .unwrap();
let spawn_fn = get_spawn_fn(); let spawn_fn = get_spawn_fn();
let network = spawn_fn(config).await.unwrap(); let network = spawn_fn(config).await.unwrap();
println!("🚀🚀🚀🚀 network deployed"); println!("🚀🚀🚀🚀 network deployed");
// wait 2 blocks // wait 2 blocks
let alice = network.get_node("alice").unwrap(); let alice = network.get_node("alice").unwrap();
assert!(alice assert!(alice.wait_metric(BEST_BLOCK_METRIC, |b| b > 2_f64).await.is_ok());
.wait_metric(BEST_BLOCK_METRIC, |b| b > 2_f64)
.await
.is_ok());
// asset-hub-collator-1 // asset-hub-collator-1
let collator = network.get_node("asset-hub-collator-1").unwrap(); let collator = network.get_node("asset-hub-collator-1").unwrap();
let client = collator let client = collator.wait_client::<subxt::PolkadotConfig>().await.unwrap();
.wait_client::<subxt::PolkadotConfig>()
.await
.unwrap();
// wait 1 blocks // wait 1 blocks
let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1); let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1);
while let Some(block) = blocks.next().await { while let Some(block) = blocks.next().await {
println!( println!("Block (asset-hub-collator-1) #{}", block.unwrap().header().number);
"Block (asset-hub-collator-1) #{}", }
block.unwrap().header().number
);
}
// asset-hub-collator-2 // asset-hub-collator-2
let collator = network.get_node("asset-hub-collator-2").unwrap(); let collator = network.get_node("asset-hub-collator-2").unwrap();
let client = collator let client = collator.wait_client::<subxt::PolkadotConfig>().await.unwrap();
.wait_client::<subxt::PolkadotConfig>()
.await
.unwrap();
// wait 1 blocks // wait 1 blocks
let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1); let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(1);
while let Some(block) = blocks.next().await { while let Some(block) = blocks.next().await {
println!( println!("Block (asset-hub-collator-2) #{}", block.unwrap().header().number);
"Block (asset-hub-collator-2) #{}", }
block.unwrap().header().number
);
}
} }
+16 -19
View File
@@ -4,7 +4,7 @@ use configuration::{NetworkConfig, NetworkConfigBuilder};
use zombienet_sdk::environment::get_spawn_fn; use zombienet_sdk::environment::get_spawn_fn;
fn small_network() -> NetworkConfig { fn small_network() -> NetworkConfig {
NetworkConfigBuilder::new() NetworkConfigBuilder::new()
.with_relaychain(|r| { .with_relaychain(|r| {
r.with_chain("rococo-local") r.with_chain("rococo-local")
.with_default_command("polkadot") .with_default_command("polkadot")
@@ -31,27 +31,24 @@ fn small_network() -> NetworkConfig {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn ci_native_smoke_should_works() { async fn ci_native_smoke_should_works() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
const BEST_BLOCK_METRIC: &str = "block_height{status=\"best\"}"; const BEST_BLOCK_METRIC: &str = "block_height{status=\"best\"}";
let now = Instant::now(); let now = Instant::now();
let config = small_network(); let config = small_network();
let spawn_fn = get_spawn_fn(); let spawn_fn = get_spawn_fn();
let network = spawn_fn(config).await.unwrap(); let network = spawn_fn(config).await.unwrap();
let elapsed = now.elapsed(); let elapsed = now.elapsed();
println!("🚀🚀🚀🚀 network deployed in {elapsed:.2?}"); println!("🚀🚀🚀🚀 network deployed in {elapsed:.2?}");
network.wait_until_is_up(20).await.unwrap(); network.wait_until_is_up(20).await.unwrap();
let elapsed = now.elapsed(); let elapsed = now.elapsed();
println!("✅✅✅✅ network is up in {elapsed:.2?}"); println!("✅✅✅✅ network is up in {elapsed:.2?}");
// Get a ref to the node // Get a ref to the node
let alice = network.get_node("alice").unwrap(); let alice = network.get_node("alice").unwrap();
// wait 10 blocks // wait 10 blocks
alice alice.wait_metric(BEST_BLOCK_METRIC, |x| x > 9_f64).await.unwrap();
.wait_metric(BEST_BLOCK_METRIC, |x| x > 9_f64)
.await
.unwrap();
} }
+109 -137
View File
@@ -6,174 +6,146 @@ use orchestrator::{AddCollatorOptions, AddNodeOptions};
use zombienet_sdk::environment::{get_attach_fn, get_spawn_fn}; use zombienet_sdk::environment::{get_attach_fn, get_spawn_fn};
fn small_network() -> NetworkConfig { fn small_network() -> NetworkConfig {
NetworkConfigBuilder::new() NetworkConfigBuilder::new()
.with_relaychain(|r| { .with_relaychain(|r| {
r.with_chain("rococo-local") r.with_chain("rococo-local")
.with_default_command("polkadot") .with_default_command("polkadot")
.with_default_image("docker.io/parity/polkadot:v1.20.2") .with_default_image("docker.io/parity/polkadot:v1.20.2")
.with_validator(|node| node.with_name("alice")) .with_validator(|node| node.with_name("alice"))
.with_validator(|node| node.with_name("bob")) .with_validator(|node| node.with_name("bob"))
}) })
.with_parachain(|p| { .with_parachain(|p| {
p.with_id(2000).cumulus_based(true).with_collator(|n| { p.with_id(2000).cumulus_based(true).with_collator(|n| {
n.with_name("collator") n.with_name("collator")
.with_command("polkadot-parachain") .with_command("polkadot-parachain")
.with_image("docker.io/parity/polkadot-parachain:1.7.0") .with_image("docker.io/parity/polkadot-parachain:1.7.0")
}) })
}) })
.with_parachain(|p| { .with_parachain(|p| {
p.with_id(3000).cumulus_based(true).with_collator(|n| { p.with_id(3000).cumulus_based(true).with_collator(|n| {
n.with_name("collator-new") n.with_name("collator-new")
.with_command("polkadot-parachain") .with_command("polkadot-parachain")
.with_image("docker.io/parity/polkadot-parachain:v1.20.2") .with_image("docker.io/parity/polkadot-parachain:v1.20.2")
}) })
}) })
.with_global_settings(|g| { .with_global_settings(|g| {
g.with_base_dir(PathBuf::from("/tmp/zombie-1")) g.with_base_dir(PathBuf::from("/tmp/zombie-1")).with_tear_down_on_failure(false)
.with_tear_down_on_failure(false) })
}) .build()
.build() .unwrap()
.unwrap()
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn ci_k8s_basic_functionalities_should_works() { async fn ci_k8s_basic_functionalities_should_works() {
let _ = tracing_subscriber::fmt::try_init(); let _ = tracing_subscriber::fmt::try_init();
const BEST_BLOCK_METRIC: &str = "block_height{status=\"best\"}"; const BEST_BLOCK_METRIC: &str = "block_height{status=\"best\"}";
let now = Instant::now(); let now = Instant::now();
let config = small_network(); let config = small_network();
let spawn_fn = get_spawn_fn(); let spawn_fn = get_spawn_fn();
let network = spawn_fn(config).await.unwrap(); let network = spawn_fn(config).await.unwrap();
let elapsed = now.elapsed(); let elapsed = now.elapsed();
println!("🚀🚀🚀🚀 network deployed in {elapsed:.2?}"); println!("🚀🚀🚀🚀 network deployed in {elapsed:.2?}");
// detach and attach to running // detach and attach to running
network.detach().await; network.detach().await;
drop(network); drop(network);
let attach_fn = get_attach_fn(); let attach_fn = get_attach_fn();
let zombie_path = PathBuf::from("/tmp/zombie-1/zombie.json"); let zombie_path = PathBuf::from("/tmp/zombie-1/zombie.json");
let mut network = attach_fn(zombie_path).await.unwrap(); let mut network = attach_fn(zombie_path).await.unwrap();
// Get a ref to the node // Get a ref to the node
let alice = network.get_node("alice").unwrap(); let alice = network.get_node("alice").unwrap();
let (_best_block_pass, client) = try_join!( let (_best_block_pass, client) = try_join!(
alice.wait_metric(BEST_BLOCK_METRIC, |x| x > 5_f64), alice.wait_metric(BEST_BLOCK_METRIC, |x| x > 5_f64),
alice.wait_client::<subxt::PolkadotConfig>() alice.wait_client::<subxt::PolkadotConfig>()
) )
.unwrap(); .unwrap();
alice alice.wait_log_line_count("*rted #1*", true, 10).await.unwrap();
.wait_log_line_count("*rted #1*", true, 10)
.await
.unwrap();
// check best block through metrics with timeout // check best block through metrics with timeout
assert!(alice assert!(alice
.wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > 10_f64, 45_u32) .wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > 10_f64, 45_u32)
.await .await
.is_ok()); .is_ok());
// ensure timeout error // ensure timeout error
let best_block = alice.reports(BEST_BLOCK_METRIC).await.unwrap(); let best_block = alice.reports(BEST_BLOCK_METRIC).await.unwrap();
let res = alice let res = alice
.wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > (best_block * 2_f64), 10_u32) .wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > (best_block * 2_f64), 10_u32)
.await; .await;
assert!(res.is_err()); assert!(res.is_err());
// get single metric // get single metric
let role = alice.reports("node_roles").await.unwrap(); let role = alice.reports("node_roles").await.unwrap();
println!("Role is {role}"); println!("Role is {role}");
assert_eq!(role, 4.0); assert_eq!(role, 4.0);
// subxt // subxt
// wait 3 blocks // wait 3 blocks
let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(3); let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(3);
while let Some(block) = blocks.next().await { while let Some(block) = blocks.next().await {
println!("Block #{}", block.unwrap().header().number); println!("Block #{}", block.unwrap().header().number);
} }
// drop the client // drop the client
drop(client); drop(client);
// check best block through metrics // check best block through metrics
let best_block = alice let best_block = alice.reports("block_height{status=\"best\"}").await.unwrap();
.reports("block_height{status=\"best\"}")
.await
.unwrap();
assert!(best_block >= 2.0, "Current best {best_block}"); assert!(best_block >= 2.0, "Current best {best_block}");
// collator // collator
let collator = network.get_node("collator").unwrap(); let collator = network.get_node("collator").unwrap();
let client = collator let client = collator.wait_client::<subxt::PolkadotConfig>().await.unwrap();
.wait_client::<subxt::PolkadotConfig>()
.await
.unwrap();
// wait 3 blocks // wait 3 blocks
let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(3); let mut blocks = client.blocks().subscribe_finalized().await.unwrap().take(3);
while let Some(block) = blocks.next().await { while let Some(block) = blocks.next().await {
println!("Block (para) #{}", block.unwrap().header().number); println!("Block (para) #{}", block.unwrap().header().number);
} }
// add node // add node
let opts = AddNodeOptions { let opts = AddNodeOptions { rpc_port: Some(9444), is_validator: true, ..Default::default() };
rpc_port: Some(9444),
is_validator: true,
..Default::default()
};
network.add_node("new1", opts).await.unwrap(); network.add_node("new1", opts).await.unwrap();
// add collator // add collator
let col_opts = AddCollatorOptions { let col_opts = AddCollatorOptions {
command: Some("polkadot-parachain".try_into().unwrap()), command: Some("polkadot-parachain".try_into().unwrap()),
image: Some( image: Some("docker.io/parity/polkadot-parachain:1.7.0".try_into().unwrap()),
"docker.io/parity/polkadot-parachain:1.7.0" ..Default::default()
.try_into() };
.unwrap(),
),
..Default::default()
};
network network.add_collator("new-col-1", col_opts, 2000).await.unwrap();
.add_collator("new-col-1", col_opts, 2000)
.await
.unwrap();
// pause / resume // pause / resume
let alice = network.get_node("alice").unwrap(); let alice = network.get_node("alice").unwrap();
alice.pause().await.unwrap(); alice.pause().await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let res_err = alice let res_err = alice.wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > 5_f64, 5_u32).await;
.wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > 5_f64, 5_u32)
.await;
assert!(res_err.is_err()); assert!(res_err.is_err());
alice.resume().await.unwrap(); alice.resume().await.unwrap();
alice alice.wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > 5_f64, 5_u32).await.unwrap();
.wait_metric_with_timeout(BEST_BLOCK_METRIC, |x| x > 5_f64, 5_u32)
.await
.unwrap();
// timeout connecting ws // timeout connecting ws
let collator = network.get_node("collator").unwrap(); let collator = network.get_node("collator").unwrap();
collator.pause().await.unwrap(); collator.pause().await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let r = collator let r = collator.wait_client_with_timeout::<subxt::PolkadotConfig>(1_u32).await;
.wait_client_with_timeout::<subxt::PolkadotConfig>(1_u32) assert!(r.is_err());
.await;
assert!(r.is_err());
// tear down (optional if you don't detach the network) // tear down (optional if you don't detach the network)
network.destroy().await.unwrap(); network.destroy().await.unwrap();
} }
@@ -2,43 +2,43 @@ use zombienet_sdk::{environment::get_spawn_fn, NetworkConfigBuilder};
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn two_paras_same_id() { async fn two_paras_same_id() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let spawn_fn = get_spawn_fn(); let spawn_fn = get_spawn_fn();
let config = NetworkConfigBuilder::new() let config = NetworkConfigBuilder::new()
.with_relaychain(|r| { .with_relaychain(|r| {
r.with_chain("rococo-local") r.with_chain("rococo-local")
.with_default_command("polkadot") .with_default_command("polkadot")
.with_default_image("docker.io/parity/polkadot:v1.7.0") .with_default_image("docker.io/parity/polkadot:v1.7.0")
.with_validator(|node| node.with_name("alice")) .with_validator(|node| node.with_name("alice"))
.with_validator(|node| node.with_name("bob")) .with_validator(|node| node.with_name("bob"))
}) })
.with_parachain(|p| { .with_parachain(|p| {
p.with_id(2000) p.with_id(2000)
.with_default_command("polkadot-parachain") .with_default_command("polkadot-parachain")
.with_default_image("docker.io/parity/polkadot-parachain:1.7.0") .with_default_image("docker.io/parity/polkadot-parachain:1.7.0")
.with_collator(|n| n.with_name("collator")) .with_collator(|n| n.with_name("collator"))
}) })
.with_parachain(|p| { .with_parachain(|p| {
p.with_id(2000) p.with_id(2000)
.with_default_command("polkadot-parachain") .with_default_command("polkadot-parachain")
.with_default_image("docker.io/parity/polkadot-parachain:1.7.0") .with_default_image("docker.io/parity/polkadot-parachain:1.7.0")
.with_registration_strategy(zombienet_sdk::RegistrationStrategy::Manual) .with_registration_strategy(zombienet_sdk::RegistrationStrategy::Manual)
.with_collator(|n| n.with_name("collator1")) .with_collator(|n| n.with_name("collator1"))
}) })
.build() .build()
.unwrap(); .unwrap();
let network = spawn_fn(config).await.unwrap(); let network = spawn_fn(config).await.unwrap();
assert!(network.get_node("collator").is_ok()); assert!(network.get_node("collator").is_ok());
assert!(network.get_node("collator1").is_ok()); assert!(network.get_node("collator1").is_ok());
// First parachain (out of two) is fetched // First parachain (out of two) is fetched
assert_eq!(network.parachain(2000).unwrap().unique_id(), "2000"); assert_eq!(network.parachain(2000).unwrap().unique_id(), "2000");
// First and second parachain hav the same para_id // First and second parachain hav the same para_id
assert_eq!( assert_eq!(
network.parachain_by_unique_id("2000").unwrap().para_id(), network.parachain_by_unique_id("2000").unwrap().para_id(),
network.parachain_by_unique_id("2000-1").unwrap().para_id(), network.parachain_by_unique_id("2000-1").unwrap().para_id(),
); );
} }
@@ -11,14 +11,14 @@ pub const VALIDATION_CHECK: &str = "validation failed ";
pub const PREFIX_CANT_BE_NONE: &str = "name prefix can't be None if a value exists "; pub const PREFIX_CANT_BE_NONE: &str = "name prefix can't be None if a value exists ";
pub const GRAPH_CONTAINS_NAME: &str = pub const GRAPH_CONTAINS_NAME: &str =
"graph contains node name; we initialize it with all node names"; "graph contains node name; we initialize it with all node names";
pub const GRAPH_CONTAINS_DEP: &str = "graph contains dep_name; we filter out deps not contained in by_name and populate the graph with all nodes"; pub const GRAPH_CONTAINS_DEP: &str = "graph contains dep_name; we filter out deps not contained in by_name and populate the graph with all nodes";
pub const INDEGREE_CONTAINS_NAME: &str = pub const INDEGREE_CONTAINS_NAME: &str =
"indegree contains node name; we initialize it with all node names"; "indegree contains node name; we initialize it with all node names";
pub const QUEUE_NOT_EMPTY: &str = "queue is not empty; we're looping over its length"; pub const QUEUE_NOT_EMPTY: &str = "queue is not empty; we're looping over its length";
pub const THIS_IS_A_BUG: &str = pub const THIS_IS_A_BUG: &str =
"- this is a bug please report it: https://github.com/paritytech/zombienet-sdk/issues"; "- this is a bug please report it: https://github.com/paritytech/zombienet-sdk/issues";
/// environment variable which can be used to override node spawn timeout /// environment variable which can be used to override node spawn timeout
pub const ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS: &str = "ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS"; pub const ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS: &str = "ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS";
+33 -33
View File
@@ -10,51 +10,51 @@ pub mod local;
pub struct FileSystemError(#[from] anyhow::Error); pub struct FileSystemError(#[from] anyhow::Error);
impl From<std::io::Error> for FileSystemError { impl From<std::io::Error> for FileSystemError {
fn from(error: std::io::Error) -> Self { fn from(error: std::io::Error) -> Self {
Self(error.into()) Self(error.into())
} }
} }
pub type FileSystemResult<T> = Result<T, FileSystemError>; pub type FileSystemResult<T> = Result<T, FileSystemError>;
#[async_trait] #[async_trait]
pub trait FileSystem { pub trait FileSystem {
async fn create_dir<P>(&self, path: P) -> FileSystemResult<()> async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send; P: AsRef<Path> + Send;
async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()> async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send; P: AsRef<Path> + Send;
async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>> async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
where where
P: AsRef<Path> + Send; P: AsRef<Path> + Send;
async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String> async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
where where
P: AsRef<Path> + Send; P: AsRef<Path> + Send;
async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()> async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
C: AsRef<[u8]> + Send; C: AsRef<[u8]> + Send;
async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()> async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
C: AsRef<[u8]> + Send; C: AsRef<[u8]> + Send;
async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()> async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
where where
P1: AsRef<Path> + Send, P1: AsRef<Path> + Send,
P2: AsRef<Path> + Send; P2: AsRef<Path> + Send;
async fn set_mode<P>(&self, path: P, perm: u32) -> FileSystemResult<()> async fn set_mode<P>(&self, path: P, perm: u32) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send; P: AsRef<Path> + Send;
async fn exists<P>(&self, path: P) -> bool async fn exists<P>(&self, path: P) -> bool
where where
P: AsRef<Path> + Send; P: AsRef<Path> + Send;
} }
File diff suppressed because it is too large Load Diff
+286 -299
View File
@@ -10,381 +10,368 @@ pub struct LocalFileSystem;
#[async_trait] #[async_trait]
impl FileSystem for LocalFileSystem { impl FileSystem for LocalFileSystem {
async fn create_dir<P>(&self, path: P) -> FileSystemResult<()> async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
tokio::fs::create_dir(path).await.map_err(Into::into) tokio::fs::create_dir(path).await.map_err(Into::into)
} }
async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()> async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
tokio::fs::create_dir_all(path).await.map_err(Into::into) tokio::fs::create_dir_all(path).await.map_err(Into::into)
} }
async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>> async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
tokio::fs::read(path).await.map_err(Into::into) tokio::fs::read(path).await.map_err(Into::into)
} }
async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String> async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
tokio::fs::read_to_string(path).await.map_err(Into::into) tokio::fs::read_to_string(path).await.map_err(Into::into)
} }
async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()> async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
C: AsRef<[u8]> + Send, C: AsRef<[u8]> + Send,
{ {
tokio::fs::write(path, contents).await.map_err(Into::into) tokio::fs::write(path, contents).await.map_err(Into::into)
} }
async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()> async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
C: AsRef<[u8]> + Send, C: AsRef<[u8]> + Send,
{ {
let contents = contents.as_ref(); let contents = contents.as_ref();
let mut file = tokio::fs::OpenOptions::new() let mut file = tokio::fs::OpenOptions::new()
.create(true) .create(true)
.append(true) .append(true)
.open(path) .open(path)
.await .await
.map_err(Into::<FileSystemError>::into)?; .map_err(Into::<FileSystemError>::into)?;
file.write_all(contents) file.write_all(contents).await.map_err(Into::<FileSystemError>::into)?;
.await
.map_err(Into::<FileSystemError>::into)?;
file.flush().await.and(Ok(())).map_err(Into::into) file.flush().await.and(Ok(())).map_err(Into::into)
} }
async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()> async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
where where
P1: AsRef<Path> + Send, P1: AsRef<Path> + Send,
P2: AsRef<Path> + Send, P2: AsRef<Path> + Send,
{ {
tokio::fs::copy(from, to) tokio::fs::copy(from, to).await.and(Ok(())).map_err(Into::into)
.await }
.and(Ok(()))
.map_err(Into::into)
}
async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()> async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
tokio::fs::set_permissions(path, Permissions::from_mode(mode)) tokio::fs::set_permissions(path, Permissions::from_mode(mode)).await.map_err(Into::into)
.await }
.map_err(Into::into)
}
async fn exists<P>(&self, path: P) -> bool async fn exists<P>(&self, path: P) -> bool
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
path.as_ref().exists() path.as_ref().exists()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use uuid::Uuid; use uuid::Uuid;
use super::*; use super::*;
const FILE_BITS: u32 = 0o100000; const FILE_BITS: u32 = 0o100000;
const DIR_BITS: u32 = 0o40000; const DIR_BITS: u32 = 0o40000;
fn setup() -> String { fn setup() -> String {
let test_dir = format!("/tmp/unit_test_{}", Uuid::new_v4()); let test_dir = format!("/tmp/unit_test_{}", Uuid::new_v4());
std::fs::create_dir(&test_dir).unwrap(); std::fs::create_dir(&test_dir).unwrap();
test_dir test_dir
} }
fn teardown(test_dir: String) { fn teardown(test_dir: String) {
std::fs::remove_dir_all(test_dir).unwrap(); std::fs::remove_dir_all(test_dir).unwrap();
} }
#[tokio::test] #[tokio::test]
async fn create_dir_should_create_a_new_directory_at_path() { async fn create_dir_should_create_a_new_directory_at_path() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let new_dir = format!("{test_dir}/mynewdir"); let new_dir = format!("{test_dir}/mynewdir");
fs.create_dir(&new_dir).await.unwrap(); fs.create_dir(&new_dir).await.unwrap();
let new_dir_path = Path::new(&new_dir); let new_dir_path = Path::new(&new_dir);
assert!(new_dir_path.exists() && new_dir_path.is_dir()); assert!(new_dir_path.exists() && new_dir_path.is_dir());
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn create_dir_should_bubble_up_error_if_some_happens() { async fn create_dir_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let new_dir = format!("{test_dir}/mynewdir"); let new_dir = format!("{test_dir}/mynewdir");
// intentionally create new dir before calling function to force error // intentionally create new dir before calling function to force error
std::fs::create_dir(&new_dir).unwrap(); std::fs::create_dir(&new_dir).unwrap();
let err = fs.create_dir(&new_dir).await.unwrap_err(); let err = fs.create_dir(&new_dir).await.unwrap_err();
assert_eq!(err.to_string(), "File exists (os error 17)"); assert_eq!(err.to_string(), "File exists (os error 17)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn create_dir_all_should_create_a_new_directory_and_all_of_it_ancestors_at_path() { async fn create_dir_all_should_create_a_new_directory_and_all_of_it_ancestors_at_path() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let new_dir = format!("{test_dir}/the/path/to/mynewdir"); let new_dir = format!("{test_dir}/the/path/to/mynewdir");
fs.create_dir_all(&new_dir).await.unwrap(); fs.create_dir_all(&new_dir).await.unwrap();
let new_dir_path = Path::new(&new_dir); let new_dir_path = Path::new(&new_dir);
assert!(new_dir_path.exists() && new_dir_path.is_dir()); assert!(new_dir_path.exists() && new_dir_path.is_dir());
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn create_dir_all_should_bubble_up_error_if_some_happens() { async fn create_dir_all_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let new_dir = format!("{test_dir}/the/path/to/mynewdir"); let new_dir = format!("{test_dir}/the/path/to/mynewdir");
// intentionally create new file as ancestor before calling function to force error // intentionally create new file as ancestor before calling function to force error
std::fs::write(format!("{test_dir}/the"), b"test").unwrap(); std::fs::write(format!("{test_dir}/the"), b"test").unwrap();
let err = fs.create_dir_all(&new_dir).await.unwrap_err(); let err = fs.create_dir_all(&new_dir).await.unwrap_err();
assert_eq!(err.to_string(), "Not a directory (os error 20)"); assert_eq!(err.to_string(), "Not a directory (os error 20)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn read_should_return_the_contents_of_the_file_at_path() { async fn read_should_return_the_contents_of_the_file_at_path() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
std::fs::write(&file_path, b"Test").unwrap(); std::fs::write(&file_path, b"Test").unwrap();
let contents = fs.read(file_path).await.unwrap(); let contents = fs.read(file_path).await.unwrap();
assert_eq!(contents, b"Test"); assert_eq!(contents, b"Test");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn read_should_bubble_up_error_if_some_happens() { async fn read_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
// intentionally forget to create file to force error // intentionally forget to create file to force error
let err = fs.read(file_path).await.unwrap_err(); let err = fs.read(file_path).await.unwrap_err();
assert_eq!(err.to_string(), "No such file or directory (os error 2)"); assert_eq!(err.to_string(), "No such file or directory (os error 2)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn read_to_string_should_return_the_contents_of_the_file_at_path_as_string() { async fn read_to_string_should_return_the_contents_of_the_file_at_path_as_string() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
std::fs::write(&file_path, b"Test").unwrap(); std::fs::write(&file_path, b"Test").unwrap();
let contents = fs.read_to_string(file_path).await.unwrap(); let contents = fs.read_to_string(file_path).await.unwrap();
assert_eq!(contents, "Test"); assert_eq!(contents, "Test");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn read_to_string_should_bubble_up_error_if_some_happens() { async fn read_to_string_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
// intentionally forget to create file to force error // intentionally forget to create file to force error
let err = fs.read_to_string(file_path).await.unwrap_err(); let err = fs.read_to_string(file_path).await.unwrap_err();
assert_eq!(err.to_string(), "No such file or directory (os error 2)"); assert_eq!(err.to_string(), "No such file or directory (os error 2)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn write_should_create_a_new_file_at_path_with_contents() { async fn write_should_create_a_new_file_at_path_with_contents() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
fs.write(&file_path, "Test").await.unwrap(); fs.write(&file_path, "Test").await.unwrap();
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test"); assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn write_should_overwrite_an_existing_file_with_contents() { async fn write_should_overwrite_an_existing_file_with_contents() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
std::fs::write(&file_path, "Test").unwrap(); std::fs::write(&file_path, "Test").unwrap();
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test"); assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
fs.write(&file_path, "Test updated").await.unwrap(); fs.write(&file_path, "Test updated").await.unwrap();
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated"); assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn write_should_bubble_up_error_if_some_happens() { async fn write_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
// intentionally create directory instead of file to force error // intentionally create directory instead of file to force error
std::fs::create_dir(&file_path).unwrap(); std::fs::create_dir(&file_path).unwrap();
let err = fs.write(&file_path, "Test").await.unwrap_err(); let err = fs.write(&file_path, "Test").await.unwrap_err();
assert_eq!(err.to_string(), "Is a directory (os error 21)"); assert_eq!(err.to_string(), "Is a directory (os error 21)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn append_should_create_a_new_file_at_path_with_contents() { async fn append_should_create_a_new_file_at_path_with_contents() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
fs.append(&file_path, "Test").await.unwrap(); fs.append(&file_path, "Test").await.unwrap();
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test"); assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn append_should_updates_an_existing_file_by_appending_contents() { async fn append_should_updates_an_existing_file_by_appending_contents() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
std::fs::write(&file_path, "Test").unwrap(); std::fs::write(&file_path, "Test").unwrap();
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test"); assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
fs.append(&file_path, " updated").await.unwrap(); fs.append(&file_path, " updated").await.unwrap();
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated"); assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn append_should_bubble_up_error_if_some_happens() { async fn append_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let file_path = format!("{test_dir}/myfile"); let file_path = format!("{test_dir}/myfile");
// intentionally create directory instead of file to force error // intentionally create directory instead of file to force error
std::fs::create_dir(&file_path).unwrap(); std::fs::create_dir(&file_path).unwrap();
let err = fs.append(&file_path, "Test").await.unwrap_err(); let err = fs.append(&file_path, "Test").await.unwrap_err();
assert_eq!(err.to_string(), "Is a directory (os error 21)"); assert_eq!(err.to_string(), "Is a directory (os error 21)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn copy_should_create_a_duplicate_of_source() { async fn copy_should_create_a_duplicate_of_source() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let from_path = format!("{test_dir}/myfile"); let from_path = format!("{test_dir}/myfile");
std::fs::write(&from_path, "Test").unwrap(); std::fs::write(&from_path, "Test").unwrap();
let to_path = format!("{test_dir}/mycopy"); let to_path = format!("{test_dir}/mycopy");
fs.copy(&from_path, &to_path).await.unwrap(); fs.copy(&from_path, &to_path).await.unwrap();
assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Test"); assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Test");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn copy_should_ovewrite_destination_if_alread_exists() { async fn copy_should_ovewrite_destination_if_alread_exists() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let from_path = format!("{test_dir}/myfile"); let from_path = format!("{test_dir}/myfile");
std::fs::write(&from_path, "Test").unwrap(); std::fs::write(&from_path, "Test").unwrap();
let to_path = format!("{test_dir}/mycopy"); let to_path = format!("{test_dir}/mycopy");
std::fs::write(&from_path, "Some content").unwrap(); std::fs::write(&from_path, "Some content").unwrap();
fs.copy(&from_path, &to_path).await.unwrap(); fs.copy(&from_path, &to_path).await.unwrap();
assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Some content"); assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Some content");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn copy_should_bubble_up_error_if_some_happens() { async fn copy_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let from_path = format!("{test_dir}/nonexistentfile"); let from_path = format!("{test_dir}/nonexistentfile");
let to_path = format!("{test_dir}/mycopy"); let to_path = format!("{test_dir}/mycopy");
let err = fs.copy(&from_path, &to_path).await.unwrap_err(); let err = fs.copy(&from_path, &to_path).await.unwrap_err();
assert_eq!(err.to_string(), "No such file or directory (os error 2)"); assert_eq!(err.to_string(), "No such file or directory (os error 2)");
teardown(test_dir); teardown(test_dir);
} }
#[tokio::test] #[tokio::test]
async fn set_mode_should_update_the_file_mode_at_path() { async fn set_mode_should_update_the_file_mode_at_path() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let path = format!("{test_dir}/myfile"); let path = format!("{test_dir}/myfile");
std::fs::write(&path, "Test").unwrap(); std::fs::write(&path, "Test").unwrap();
assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (FILE_BITS + 0o400)); assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (FILE_BITS + 0o400));
fs.set_mode(&path, 0o400).await.unwrap(); fs.set_mode(&path, 0o400).await.unwrap();
assert_eq!( assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode(), FILE_BITS + 0o400);
std::fs::metadata(&path).unwrap().permissions().mode(), teardown(test_dir);
FILE_BITS + 0o400 }
);
teardown(test_dir);
}
#[tokio::test] #[tokio::test]
async fn set_mode_should_update_the_directory_mode_at_path() { async fn set_mode_should_update_the_directory_mode_at_path() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let path = format!("{test_dir}/mydir"); let path = format!("{test_dir}/mydir");
std::fs::create_dir(&path).unwrap(); std::fs::create_dir(&path).unwrap();
assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (DIR_BITS + 0o700)); assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (DIR_BITS + 0o700));
fs.set_mode(&path, 0o700).await.unwrap(); fs.set_mode(&path, 0o700).await.unwrap();
assert_eq!( assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode(), DIR_BITS + 0o700);
std::fs::metadata(&path).unwrap().permissions().mode(), teardown(test_dir);
DIR_BITS + 0o700 }
);
teardown(test_dir);
}
#[tokio::test] #[tokio::test]
async fn set_mode_should_bubble_up_error_if_some_happens() { async fn set_mode_should_bubble_up_error_if_some_happens() {
let test_dir = setup(); let test_dir = setup();
let fs = LocalFileSystem; let fs = LocalFileSystem;
let path = format!("{test_dir}/somemissingfile"); let path = format!("{test_dir}/somemissingfile");
// intentionnally don't create file // intentionnally don't create file
let err = fs.set_mode(&path, 0o400).await.unwrap_err(); let err = fs.set_mode(&path, 0o400).await.unwrap_err();
assert_eq!(err.to_string(), "No such file or directory (os error 2)"); assert_eq!(err.to_string(), "No such file or directory (os error 2)");
teardown(test_dir); teardown(test_dir);
} }
} }
+38 -38
View File
@@ -8,53 +8,53 @@ use crate::constants::THIS_IS_A_BUG;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub async fn download_file(url: String, dest: String) -> Result<()> { pub async fn download_file(url: String, dest: String) -> Result<()> {
let response = reqwest::get(url).await?; let response = reqwest::get(url).await?;
let mut file = std::fs::File::create(dest)?; let mut file = std::fs::File::create(dest)?;
let mut content = Cursor::new(response.bytes().await?); let mut content = Cursor::new(response.bytes().await?);
std::io::copy(&mut content, &mut file)?; std::io::copy(&mut content, &mut file)?;
Ok(()) Ok(())
} }
pub async fn wait_ws_ready(url: &str) -> Result<()> { pub async fn wait_ws_ready(url: &str) -> Result<()> {
let mut parsed = Url::from_str(url)?; let mut parsed = Url::from_str(url)?;
parsed parsed
.set_scheme("http") .set_scheme("http")
.map_err(|_| anyhow::anyhow!("Can not set the scheme, {THIS_IS_A_BUG}"))?; .map_err(|_| anyhow::anyhow!("Can not set the scheme, {THIS_IS_A_BUG}"))?;
let http_client = reqwest::Client::new(); let http_client = reqwest::Client::new();
loop { loop {
let req = Request::new(Method::OPTIONS, parsed.clone()); let req = Request::new(Method::OPTIONS, parsed.clone());
let res = http_client.execute(req).await; let res = http_client.execute(req).await;
match res { match res {
Ok(res) => { Ok(res) => {
if res.status() == StatusCode::OK { if res.status() == StatusCode::OK {
// ready to go! // ready to go!
break; break;
} }
trace!("http_client status: {}, continuing...", res.status()); trace!("http_client status: {}, continuing...", res.status());
}, },
Err(e) => { Err(e) => {
if !skip_err_while_waiting(&e) { if !skip_err_while_waiting(&e) {
return Err(e.into()); return Err(e.into());
} }
trace!("http_client err: {}, continuing... ", e.to_string()); trace!("http_client err: {}, continuing... ", e.to_string());
}, },
} }
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
} }
Ok(()) Ok(())
} }
pub fn skip_err_while_waiting(e: &reqwest::Error) -> bool { pub fn skip_err_while_waiting(e: &reqwest::Error) -> bool {
// if the error is connecting/request could be the case that the node // if the error is connecting/request could be the case that the node
// is not listening yet, so we keep waiting // is not listening yet, so we keep waiting
// Skipped errs like: // Skipped errs like:
// 'tcp connect error: Connection refused (os error 61)' // 'tcp connect error: Connection refused (os error 61)'
// 'operation was canceled: connection closed before message completed' // 'operation was canceled: connection closed before message completed'
// 'connection error: Connection reset by peer (os error 54)' // 'connection error: Connection reset by peer (os error 54)'
e.is_connect() || e.is_request() e.is_connect() || e.is_request()
} }
+138 -140
View File
@@ -7,191 +7,189 @@ use tracing::{trace, warn};
use crate::constants::{SHOULD_COMPILE, THIS_IS_A_BUG}; use crate::constants::{SHOULD_COMPILE, THIS_IS_A_BUG};
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new(r#"\{\{([a-zA-Z0-9_]*)\}\}"#) static ref RE: Regex = Regex::new(r#"\{\{([a-zA-Z0-9_]*)\}\}"#)
.unwrap_or_else(|_| panic!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}")); .unwrap_or_else(|_| panic!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
static ref TOKEN_PLACEHOLDER: Regex = Regex::new(r#"\{\{ZOMBIE:(.*?):(.*?)\}\}"#) static ref TOKEN_PLACEHOLDER: Regex = Regex::new(r#"\{\{ZOMBIE:(.*?):(.*?)\}\}"#)
.unwrap_or_else(|_| panic!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}")); .unwrap_or_else(|_| panic!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
static ref PLACEHOLDER_COMPAT: HashMap<&'static str, &'static str> = { static ref PLACEHOLDER_COMPAT: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert("multiAddress", "multiaddr"); m.insert("multiAddress", "multiaddr");
m.insert("wsUri", "ws_uri"); m.insert("wsUri", "ws_uri");
m.insert("prometheusUri", "prometheus_uri"); m.insert("prometheusUri", "prometheus_uri");
m m
}; };
} }
/// Return true if the text contains any TOKEN_PLACEHOLDER /// Return true if the text contains any TOKEN_PLACEHOLDER
pub fn has_tokens(text: &str) -> bool { pub fn has_tokens(text: &str) -> bool {
TOKEN_PLACEHOLDER.is_match(text) TOKEN_PLACEHOLDER.is_match(text)
} }
pub fn apply_replacements(text: &str, replacements: &HashMap<&str, &str>) -> String { pub fn apply_replacements(text: &str, replacements: &HashMap<&str, &str>) -> String {
let augmented_text = RE.replace_all(text, |caps: &Captures| { let augmented_text = RE.replace_all(text, |caps: &Captures| {
if let Some(replacements_value) = replacements.get(&caps[1]) { if let Some(replacements_value) = replacements.get(&caps[1]) {
replacements_value.to_string() replacements_value.to_string()
} else { } else {
caps[0].to_string() caps[0].to_string()
} }
}); });
augmented_text.to_string() augmented_text.to_string()
} }
pub fn apply_env_replacements(text: &str) -> String { pub fn apply_env_replacements(text: &str) -> String {
let augmented_text = RE.replace_all(text, |caps: &Captures| { let augmented_text = RE.replace_all(text, |caps: &Captures| {
if let Ok(replacements_value) = std::env::var(&caps[1]) { if let Ok(replacements_value) = std::env::var(&caps[1]) {
replacements_value replacements_value
} else { } else {
caps[0].to_string() caps[0].to_string()
} }
}); });
augmented_text.to_string() augmented_text.to_string()
} }
pub fn apply_running_network_replacements(text: &str, network: &serde_json::Value) -> String { pub fn apply_running_network_replacements(text: &str, network: &serde_json::Value) -> String {
let augmented_text = TOKEN_PLACEHOLDER.replace_all(text, |caps: &Captures| { let augmented_text = TOKEN_PLACEHOLDER.replace_all(text, |caps: &Captures| {
trace!("appling replacements for caps: {caps:#?}"); trace!("appling replacements for caps: {caps:#?}");
if let Some(node) = network.get(&caps[1]) { if let Some(node) = network.get(&caps[1]) {
trace!("caps1 {} - node: {node}", &caps[1]); trace!("caps1 {} - node: {node}", &caps[1]);
let field = *PLACEHOLDER_COMPAT.get(&caps[2]).unwrap_or(&&caps[2]); let field = *PLACEHOLDER_COMPAT.get(&caps[2]).unwrap_or(&&caps[2]);
if let Some(val) = node.get(field) { if let Some(val) = node.get(field) {
trace!("caps2 {} - node: {node}", field); trace!("caps2 {} - node: {node}", field);
val.as_str().unwrap_or("Invalid string").to_string() val.as_str().unwrap_or("Invalid string").to_string()
} else { } else {
warn!( warn!(
"⚠️ The node with name {} doesn't have the value {} in context", "⚠️ The node with name {} doesn't have the value {} in context",
&caps[1], &caps[2] &caps[1], &caps[2]
); );
caps[0].to_string() caps[0].to_string()
} }
} else { } else {
warn!("⚠️ No node with name {} in context", &caps[1]); warn!("⚠️ No node with name {} in context", &caps[1]);
caps[0].to_string() caps[0].to_string()
} }
}); });
augmented_text.to_string() augmented_text.to_string()
} }
pub fn get_tokens_to_replace(text: &str) -> HashSet<String> { pub fn get_tokens_to_replace(text: &str) -> HashSet<String> {
let mut tokens = HashSet::new(); let mut tokens = HashSet::new();
TOKEN_PLACEHOLDER TOKEN_PLACEHOLDER.captures_iter(text).for_each(|caps: Captures| {
.captures_iter(text) tokens.insert(caps[1].to_string());
.for_each(|caps: Captures| { });
tokens.insert(caps[1].to_string());
});
tokens tokens
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json::json; use serde_json::json;
use super::*; use super::*;
#[test] #[test]
fn replace_should_works() { fn replace_should_works() {
let text = "some {{namespace}}"; let text = "some {{namespace}}";
let mut replacements = HashMap::new(); let mut replacements = HashMap::new();
replacements.insert("namespace", "demo-123"); replacements.insert("namespace", "demo-123");
let res = apply_replacements(text, &replacements); let res = apply_replacements(text, &replacements);
assert_eq!("some demo-123".to_string(), res); assert_eq!("some demo-123".to_string(), res);
} }
#[test] #[test]
fn replace_env_should_works() { fn replace_env_should_works() {
let text = "some {{namespace}}"; let text = "some {{namespace}}";
std::env::set_var("namespace", "demo-123"); std::env::set_var("namespace", "demo-123");
// let mut replacements = HashMap::new(); // let mut replacements = HashMap::new();
// replacements.insert("namespace", "demo-123"); // replacements.insert("namespace", "demo-123");
let res = apply_env_replacements(text); let res = apply_env_replacements(text);
assert_eq!("some demo-123".to_string(), res); assert_eq!("some demo-123".to_string(), res);
} }
#[test] #[test]
fn replace_multiple_should_works() { fn replace_multiple_should_works() {
let text = r#"some {{namespace}} let text = r#"some {{namespace}}
other is {{other}}"#; other is {{other}}"#;
let augmented_text = r#"some demo-123 let augmented_text = r#"some demo-123
other is other-123"#; other is other-123"#;
let mut replacements = HashMap::new(); let mut replacements = HashMap::new();
replacements.insert("namespace", "demo-123"); replacements.insert("namespace", "demo-123");
replacements.insert("other", "other-123"); replacements.insert("other", "other-123");
let res = apply_replacements(text, &replacements); let res = apply_replacements(text, &replacements);
assert_eq!(augmented_text, res); assert_eq!(augmented_text, res);
} }
#[test] #[test]
fn replace_multiple_with_missing_should_works() { fn replace_multiple_with_missing_should_works() {
let text = r#"some {{namespace}} let text = r#"some {{namespace}}
other is {{other}}"#; other is {{other}}"#;
let augmented_text = r#"some demo-123 let augmented_text = r#"some demo-123
other is {{other}}"#; other is {{other}}"#;
let mut replacements = HashMap::new(); let mut replacements = HashMap::new();
replacements.insert("namespace", "demo-123"); replacements.insert("namespace", "demo-123");
let res = apply_replacements(text, &replacements); let res = apply_replacements(text, &replacements);
assert_eq!(augmented_text, res); assert_eq!(augmented_text, res);
} }
#[test] #[test]
fn replace_without_replacement_should_leave_text_unchanged() { fn replace_without_replacement_should_leave_text_unchanged() {
let text = "some {{namespace}}"; let text = "some {{namespace}}";
let mut replacements = HashMap::new(); let mut replacements = HashMap::new();
replacements.insert("other", "demo-123"); replacements.insert("other", "demo-123");
let res = apply_replacements(text, &replacements); let res = apply_replacements(text, &replacements);
assert_eq!(text.to_string(), res); assert_eq!(text.to_string(), res);
} }
#[test] #[test]
fn replace_running_network_should_work() { fn replace_running_network_should_work() {
let network = json!({ let network = json!({
"alice" : { "alice" : {
"multiaddr": "some/demo/127.0.0.1" "multiaddr": "some/demo/127.0.0.1"
} }
}); });
let res = apply_running_network_replacements("{{ZOMBIE:alice:multiaddr}}", &network); let res = apply_running_network_replacements("{{ZOMBIE:alice:multiaddr}}", &network);
assert_eq!(res.as_str(), "some/demo/127.0.0.1"); assert_eq!(res.as_str(), "some/demo/127.0.0.1");
} }
#[test] #[test]
fn replace_running_network_with_compat_should_work() { fn replace_running_network_with_compat_should_work() {
let network = json!({ let network = json!({
"alice" : { "alice" : {
"multiaddr": "some/demo/127.0.0.1" "multiaddr": "some/demo/127.0.0.1"
} }
}); });
let res = apply_running_network_replacements("{{ZOMBIE:alice:multiAddress}}", &network); let res = apply_running_network_replacements("{{ZOMBIE:alice:multiAddress}}", &network);
assert_eq!(res.as_str(), "some/demo/127.0.0.1"); assert_eq!(res.as_str(), "some/demo/127.0.0.1");
} }
#[test] #[test]
fn replace_running_network_with_missing_field_should_not_replace_nothing() { fn replace_running_network_with_missing_field_should_not_replace_nothing() {
let network = json!({ let network = json!({
"alice" : { "alice" : {
"multiaddr": "some/demo/127.0.0.1" "multiaddr": "some/demo/127.0.0.1"
} }
}); });
let res = apply_running_network_replacements("{{ZOMBIE:alice:someField}}", &network); let res = apply_running_network_replacements("{{ZOMBIE:alice:someField}}", &network);
assert_eq!(res.as_str(), "{{ZOMBIE:alice:someField}}"); assert_eq!(res.as_str(), "{{ZOMBIE:alice:someField}}");
} }
#[test] #[test]
fn get_tokens_to_replace_should_work() { fn get_tokens_to_replace_should_work() {
let res = get_tokens_to_replace("{{ZOMBIE:alice:multiaddr}} {{ZOMBIE:bob:multiaddr}}"); let res = get_tokens_to_replace("{{ZOMBIE:alice:multiaddr}} {{ZOMBIE:bob:multiaddr}}");
let mut expected = HashSet::new(); let mut expected = HashSet::new();
expected.insert("alice".to_string()); expected.insert("alice".to_string());
expected.insert("bob".to_string()); expected.insert("bob".to_string());
assert_eq!(res, expected); assert_eq!(res, expected);
} }
} }
+14 -21
View File
@@ -1,26 +1,19 @@
# https://rust-lang.github.io/rustfmt/?version=v1.7.0 # Pezkuwi ZombieNet SDK - Stable Rustfmt Configuration
# Only stable features for compatibility with CI
# general # Basic (stable)
indent_style = "Block" hard_tabs = true
max_width = 100
use_small_heuristics = "Max"
# rewriting # Imports (stable)
condense_wildcard_suffixes = true reorder_imports = true
reorder_modules = true
# Consistency (stable)
newline_style = "Unix"
# Misc (stable)
match_block_trailing_comma = true match_block_trailing_comma = true
use_field_init_shorthand = true use_field_init_shorthand = true
use_try_shorthand = true use_try_shorthand = true
# normalization
normalize_comments = true
normalize_doc_attributes = true
# reordering
reorder_impl_items = true
reorder_imports = true
reorder_modules = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
# additional formating
format_code_in_doc_comments = true
format_macro_matchers = true
format_macro_bodies = true