From b2a1af06dfa6987261a08ccbc8f05ed1bdc0d0b8 Mon Sep 17 00:00:00 2001 From: Simon Paitrault Date: Wed, 20 Mar 2024 18:39:24 +0100 Subject: [PATCH] fix(config): fix the parse of edge_path ENV var (#482) Signed-off-by: Simon Paitrault Co-authored-by: Monir Hadji --- crates/topos-config/src/edge.rs | 35 +++++++++ crates/topos-config/src/edge/command.rs | 5 +- crates/topos-config/src/genesis/mod.rs | 2 +- crates/topos-config/src/lib.rs | 4 +- crates/topos-config/src/node.rs | 28 +++++-- crates/topos-node/src/lib.rs | 10 +-- crates/topos/src/components/node/commands.rs | 7 +- .../src/components/node/commands/init.rs | 6 ++ .../topos/src/components/node/commands/up.rs | 6 ++ crates/topos/src/components/node/mod.rs | 4 +- crates/topos/tests/node.rs | 76 +++++++++++++++++++ crates/topos/tests/setup.rs | 68 ++++++++++++++++- 12 files changed, 231 insertions(+), 20 deletions(-) diff --git a/crates/topos-config/src/edge.rs b/crates/topos-config/src/edge.rs index 3cbd1e4a3..1e2da6eb9 100644 --- a/crates/topos-config/src/edge.rs +++ b/crates/topos-config/src/edge.rs @@ -12,6 +12,8 @@ use std::{ use tokio::{spawn, task::JoinHandle}; use tracing::{error, info}; +use self::command::BINARY_NAME; + // TODO: Provides the default arguments here // Serde `flatten` and `default` doesn't work together yet // https://github.com/serde-rs/serde/issues/1626 @@ -44,6 +46,39 @@ impl Config for EdgeConfig { } } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct EdgeBinConfig { + pub edge_path: PathBuf, +} + +impl EdgeBinConfig { + pub fn binary_path(&self) -> PathBuf { + self.edge_path.join(BINARY_NAME) + } +} + +impl Config for EdgeBinConfig { + type Output = EdgeBinConfig; + + fn load_from_file(figment: Figment, home: &Path) -> Figment { + let home = home.join("config.toml"); + + let edge = Figment::new() + .merge(Toml::file(home).nested()) + .select("edge"); + + figment.merge(edge) + } + + fn load_context(figment: Figment) -> Result { + figment.extract() + } + + fn profile() -> String { + "edge".to_string() + } +} pub mod command; pub fn generate_edge_config( diff --git a/crates/topos-config/src/edge/command.rs b/crates/topos-config/src/edge/command.rs index 33c0026e3..c60512501 100644 --- a/crates/topos-config/src/edge/command.rs +++ b/crates/topos-config/src/edge/command.rs @@ -64,7 +64,10 @@ impl CommandConfig { } pub async fn spawn(self) -> Result { - info!("Spawning Polygon Edge with args: {:?}", self.binary_path); + info!( + "Spawning Polygon Edge binary located at: {:?}, args: {:?}", + self.binary_path, self.args + ); let mut command = Command::new(self.binary_path); command.kill_on_drop(true); command.args(self.args); diff --git a/crates/topos-config/src/genesis/mod.rs b/crates/topos-config/src/genesis/mod.rs index 21ad50442..5c8a84f87 100644 --- a/crates/topos-config/src/genesis/mod.rs +++ b/crates/topos-config/src/genesis/mod.rs @@ -110,6 +110,6 @@ impl TryFrom<&NodeConfig> for Genesis { type Error = Error; fn try_from(config: &NodeConfig) -> Result { - Genesis::new(&config.edge_path) + Genesis::new(&config.genesis_path) } } diff --git a/crates/topos-config/src/lib.rs b/crates/topos-config/src/lib.rs index 59610b749..600cce92f 100644 --- a/crates/topos-config/src/lib.rs +++ b/crates/topos-config/src/lib.rs @@ -46,7 +46,7 @@ pub trait Config: Serialize { /// It will load the configuration from the file and an optional existing struct (if any) /// and then extract the configuration from the context in order to build the Config. /// The Config is then returned or an error if the configuration is not valid. - fn load(home: &Path, config: Option) -> Result { + fn load(home: &Path, config: Option<&S>) -> Result { let mut figment = Figment::new(); figment = Self::load_from_file(figment, home); @@ -61,7 +61,7 @@ pub trait Config: Serialize { pub(crate) fn load_config( node_path: &Path, - config: Option, + config: Option<&S>, ) -> T::Output { match T::load(node_path, config) { Ok(config) => config, diff --git a/crates/topos-config/src/node.rs b/crates/topos-config/src/node.rs index 40f99a1bb..28d049f2c 100644 --- a/crates/topos-config/src/node.rs +++ b/crates/topos-config/src/node.rs @@ -10,7 +10,11 @@ use topos_wallet::SecretManager; use tracing::{debug, error}; use crate::{ - base::BaseConfig, edge::EdgeConfig, load_config, sequencer::SequencerConfig, tce::TceConfig, + base::BaseConfig, + edge::{EdgeBinConfig, EdgeConfig}, + load_config, + sequencer::SequencerConfig, + tce::TceConfig, Config, }; @@ -36,7 +40,10 @@ pub struct NodeConfig { pub node_path: PathBuf, #[serde(skip)] - pub edge_path: PathBuf, + pub genesis_path: PathBuf, + + #[serde(skip)] + pub edge_bin: Option, } impl NodeConfig { @@ -47,7 +54,7 @@ impl NodeConfig { pub fn try_from( home_path: &Path, node_name: &str, - config: Option, + config: Option<&S>, ) -> Result { let node_path = home_path.join("node").join(node_name); let config_path = node_path.join("config.toml"); @@ -68,26 +75,30 @@ impl NodeConfig { /// /// It doesn't check the existence of the config file. /// It's useful for creating a config file for a new node, relying on the default values. - pub fn create(home_path: &Path, node_name: &str, config: Option) -> Self { + pub fn create(home_path: &Path, node_name: &str, config: Option<&S>) -> Self { let node_path = home_path.join("node").join(node_name); Self::build_config(node_path, home_path, config) } /// Common function to build a node config struct from the given home path and node name. - fn build_config(node_path: PathBuf, home_path: &Path, config: Option) -> Self { + fn build_config( + node_path: PathBuf, + home_path: &Path, + config: Option<&S>, + ) -> Self { let node_folder = node_path.as_path(); let base = load_config::(node_folder, config); // Load genesis pointed by the local config - let edge_path = home_path + let genesis_path = home_path .join("subnet") .join(base.subnet.clone()) .join("genesis.json"); let mut config = NodeConfig { node_path: node_path.to_path_buf(), - edge_path, + genesis_path, home_path: home_path.to_path_buf(), base: base.clone(), sequencer: base @@ -96,6 +107,9 @@ impl NodeConfig { tce: base .need_tce() .then(|| load_config::(node_folder, None)), + edge_bin: base + .need_edge() + .then(|| load_config::(node_folder, config)), edge: base .need_edge() .then(|| load_config::(node_folder, None)), diff --git a/crates/topos-node/src/lib.rs b/crates/topos-node/src/lib.rs index 07ded54b6..cca00c8bf 100644 --- a/crates/topos-node/src/lib.rs +++ b/crates/topos-node/src/lib.rs @@ -12,7 +12,6 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; use topos_config::{ - edge::command::BINARY_NAME, genesis::Genesis, node::{NodeConfig, NodeRole}, }; @@ -76,7 +75,7 @@ pub async fn start( info!( "Could not load genesis.json file on path {} \n Please make sure to have a valid \ genesis.json file for your subnet in the {}/subnet/{} folder.", - config.edge_path.display(), + config.genesis_path.display(), config.home_path.display(), &config.base.subnet ); @@ -159,21 +158,22 @@ fn spawn_processes( info!("Using external edge node, skip running of local edge instance...") } else { let edge_config = config.edge.take().ok_or(Error::MissingEdgeConfig)?; + let edge_bin_config = config.edge_bin.take().ok_or(Error::MissingEdgeConfig)?; let data_dir = config.node_path.clone(); info!( "Spawning edge process with genesis file: {}, data directory: {}, additional edge \ arguments: {:?}", - config.edge_path.display(), + config.genesis_path.display(), data_dir.display(), edge_config.args ); processes.push(process::spawn_edge_process( - config.home_path.join(BINARY_NAME), + edge_bin_config.binary_path(), data_dir, - config.edge_path.clone(), + config.genesis_path.clone(), edge_config.args, )); } diff --git a/crates/topos/src/components/node/commands.rs b/crates/topos/src/components/node/commands.rs index ed5e5f1ac..ab0b21bf5 100644 --- a/crates/topos/src/components/node/commands.rs +++ b/crates/topos/src/components/node/commands.rs @@ -30,7 +30,12 @@ pub(crate) struct NodeCommand { pub(crate) home: PathBuf, /// Installation directory path for Polygon Edge binary - #[arg(long, env = "TOPOS_POLYGON_EDGE_BIN_PATH", default_value = ".")] + #[arg( + global = true, + long, + env = "TOPOS_POLYGON_EDGE_BIN_PATH", + default_value = "." + )] pub(crate) edge_path: PathBuf, #[clap(subcommand)] diff --git a/crates/topos/src/components/node/commands/init.rs b/crates/topos/src/components/node/commands/init.rs index bad806672..02d5d608d 100644 --- a/crates/topos/src/components/node/commands/init.rs +++ b/crates/topos/src/components/node/commands/init.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Args; use serde::Serialize; use topos_config::node::NodeRole; @@ -27,4 +29,8 @@ pub struct Init { /// rely on polygon-edge during runtime. Example: A sequencer which runs for an external EVM chain #[arg(long, env = "TOPOS_NO_EDGE_PROCESS", action)] pub no_edge_process: bool, + + /// Installation directory path for Polygon Edge binary + #[clap(from_global)] + pub(crate) edge_path: PathBuf, } diff --git a/crates/topos/src/components/node/commands/up.rs b/crates/topos/src/components/node/commands/up.rs index d479ea73c..dc52a6eee 100644 --- a/crates/topos/src/components/node/commands/up.rs +++ b/crates/topos/src/components/node/commands/up.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Args; use serde::Serialize; @@ -26,4 +28,8 @@ pub struct Up { /// If not provided open telemetry will not be used #[arg(long, env = "TOPOS_OTLP_SERVICE_NAME")] pub otlp_service_name: Option, + + /// Installation directory path for Polygon Edge binary + #[clap(from_global)] + pub(crate) edge_path: PathBuf, } diff --git a/crates/topos/src/components/node/mod.rs b/crates/topos/src/components/node/mod.rs index 410791548..c1845be24 100644 --- a/crates/topos/src/components/node/mod.rs +++ b/crates/topos/src/components/node/mod.rs @@ -95,7 +95,7 @@ pub(crate) async fn handle_command( } } - let node_config = NodeConfig::create(&home, &name, Some(cmd)); + let node_config = NodeConfig::create(&home, &name, Some(&cmd)); // Creating the TOML output let config_toml = match node_config.to_toml() { @@ -135,7 +135,7 @@ pub(crate) async fn handle_command( .as_ref() .expect("No name or default was given for node"); - let config = NodeConfig::try_from(&home, name, Some(command))?; + let config = NodeConfig::try_from(&home, name, Some(&command))?; topos_node::start( verbose, diff --git a/crates/topos/tests/node.rs b/crates/topos/tests/node.rs index f7a15aedd..61bf56ac2 100644 --- a/crates/topos/tests/node.rs +++ b/crates/topos/tests/node.rs @@ -99,4 +99,80 @@ mod serial_integration { Ok(()) } + + #[test_log::test(tokio::test)] + async fn command_node_up_custom_polygon() -> Result<(), Box> { + let tmp_home_dir = tempdir()?; + + // Create config file + let node_up_home_env = tmp_home_dir.path().to_str().unwrap(); + let custom_path = tmp_home_dir.path().join("custom_path"); + let node_edge_path_env = utils::setup_polygon_edge(custom_path.to_str().unwrap()).await; + let node_up_name_env = "TEST_NODE_UP"; + let node_up_role_env = "full-node"; + let node_up_subnet_env = "topos-up-env-subnet"; + + let mut cmd = Command::cargo_bin("topos")?; + cmd.arg("node") + .env("TOPOS_POLYGON_EDGE_BIN_PATH", &node_edge_path_env) + .env("TOPOS_HOME", node_up_home_env) + .env("TOPOS_NODE_NAME", node_up_name_env) + .env("TOPOS_NODE_ROLE", node_up_role_env) + .env("TOPOS_NODE_SUBNET", node_up_subnet_env) + .arg("init"); + + let output = cmd.assert().success(); + let result: &str = std::str::from_utf8(&output.get_output().stdout)?; + assert!(result.contains("Created node config file")); + + // Run node init with cli flags + let home = PathBuf::from(node_up_home_env); + // Verification: check that the config file was created + let config_path = home.join("node").join(node_up_name_env).join("config.toml"); + assert!(config_path.exists()); + + // Generate polygon edge genesis file + let polygon_edge_bin = format!("{}/polygon-edge", node_edge_path_env); + generate_polygon_edge_genesis_file( + &polygon_edge_bin, + node_up_home_env, + node_up_name_env, + node_up_subnet_env, + ) + .await?; + let polygon_edge_genesis_path = home + .join("subnet") + .join(node_up_subnet_env) + .join("genesis.json"); + assert!(polygon_edge_genesis_path.exists()); + + let mut cmd = Command::cargo_bin("topos")?; + cmd.arg("node") + .env("TOPOS_POLYGON_EDGE_BIN_PATH", &node_edge_path_env) + .env("TOPOS_HOME", node_up_home_env) + .env("TOPOS_NODE_NAME", node_up_name_env) + .arg("up"); + + let mut cmd = tokio::process::Command::from(cmd).spawn().unwrap(); + let pid = cmd.id().unwrap(); + let _ = tokio::time::sleep(std::time::Duration::from_secs(10)).await; + + let s = System::new_all(); + if let Some(process) = s.process(Pid::from_u32(pid)) { + if process.kill_with(Signal::Term).is_none() { + eprintln!("This signal isn't supported on this platform"); + } + } + + if let Ok(code) = cmd.wait().await { + assert!(code.success()); + } else { + panic!("Failed to shutdown gracefully"); + } + + // Cleanup + std::fs::remove_dir_all(node_up_home_env)?; + + Ok(()) + } } diff --git a/crates/topos/tests/setup.rs b/crates/topos/tests/setup.rs index 7e20c1bfc..b82b748f0 100644 --- a/crates/topos/tests/setup.rs +++ b/crates/topos/tests/setup.rs @@ -1,6 +1,6 @@ mod utils; -use std::process::Command; +use std::{fs, process::Command}; use assert_cmd::prelude::*; use tempfile::tempdir; @@ -61,3 +61,69 @@ fn setup_subnet_fail_to_install_release() -> Result<(), Box Result<(), Box> { + let tmp_home_dir = tempdir()?; + let custom_path = tmp_home_dir.path().join("custom_path"); + + fs::create_dir(&custom_path).unwrap(); + + let mut cmd = Command::cargo_bin("topos")?; + cmd.arg("setup") + .arg("subnet") + .arg("--path") + .arg(&custom_path); + + let output = cmd.assert().success(); + + let result: &str = std::str::from_utf8(&output.get_output().stdout)?; + + assert!(result.contains("Polygon Edge installation successful")); + + let file = fs::read_dir(&custom_path) + .unwrap() + .filter_map(|x| match x.ok() { + Some(f) if f.path().ends_with("polygon-edge") => Some(f), + _ => None, + }) + .last() + .unwrap(); + + assert!(file.path().starts_with(&custom_path)); + + Ok(()) +} + +#[test] +fn setup_subnet_install_edge_custom_path_env() -> Result<(), Box> { + let tmp_home_dir = tempdir()?; + let custom_path = tmp_home_dir.path().join("custom_path"); + + fs::create_dir(&custom_path).unwrap(); + + let mut cmd = Command::cargo_bin("topos")?; + + cmd.env("TOPOS_SETUP_POLYGON_EDGE_DIR", &custom_path) + .arg("setup") + .arg("subnet"); + + let output = cmd.assert().success(); + + let result: &str = std::str::from_utf8(&output.get_output().stdout)?; + + assert!(result.contains("Polygon Edge installation successful")); + + let file = fs::read_dir(&custom_path) + .unwrap() + .filter_map(|x| match x.ok() { + Some(f) if f.path().ends_with("polygon-edge") => Some(f), + _ => None, + }) + .last() + .unwrap(); + + assert!(file.path().starts_with(&custom_path)); + + Ok(()) +}