diff --git a/crates/topos-config/src/genesis/mod.rs b/crates/topos-config/src/genesis/mod.rs index ab0db46d0..21ad50442 100644 --- a/crates/topos-config/src/genesis/mod.rs +++ b/crates/topos-config/src/genesis/mod.rs @@ -8,12 +8,13 @@ use topos_core::types::ValidatorId; use topos_p2p::{Multiaddr, PeerId}; use tracing::info; +use crate::node::NodeConfig; + #[cfg(test)] pub(crate) mod tests; /// From the Edge format pub struct Genesis { - pub path: PathBuf, pub json: Value, } @@ -21,19 +22,20 @@ pub struct Genesis { pub enum Error { #[error("Failed to parse validators")] ParseValidators, + #[error("Invalid genesis file on path {0}: {1}")] InvalidGenesisFile(String, String), } impl Genesis { - pub fn new(path: PathBuf) -> Result { + pub fn new(path: &PathBuf) -> Result { info!("Reading subnet genesis file {}", path.display()); - let genesis_file = fs::File::open(&path) + let genesis_file = fs::File::open(path) .map_err(|e| Error::InvalidGenesisFile(path.display().to_string(), e.to_string()))?; let json: Value = serde_json::from_reader(genesis_file).expect("genesis json parsed"); - Ok(Self { path, json }) + Ok(Self { json }) } // TODO: parse directly with serde @@ -103,3 +105,11 @@ impl Genesis { ) } } + +impl TryFrom<&NodeConfig> for Genesis { + type Error = Error; + + fn try_from(config: &NodeConfig) -> Result { + Genesis::new(&config.edge_path) + } +} diff --git a/crates/topos-config/src/genesis/tests.rs b/crates/topos-config/src/genesis/tests.rs index a0e769ab3..f4cd6ffe3 100644 --- a/crates/topos-config/src/genesis/tests.rs +++ b/crates/topos-config/src/genesis/tests.rs @@ -14,7 +14,7 @@ macro_rules! test_case { #[fixture] #[once] pub fn genesis() -> Genesis { - Genesis::new(test_case!("genesis-example.json").into()) + Genesis::new(&test_case!("genesis-example.json").into()) .expect("Expected valid test genesis file") } diff --git a/crates/topos-config/src/node.rs b/crates/topos-config/src/node.rs index 4297d6434..40f99a1bb 100644 --- a/crates/topos-config/src/node.rs +++ b/crates/topos-config/src/node.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use figment::{ providers::{Format, Toml}, @@ -6,6 +6,8 @@ use figment::{ }; use serde::{Deserialize, Serialize}; +use topos_wallet::SecretManager; +use tracing::{debug, error}; use crate::{ base::BaseConfig, edge::EdgeConfig, load_config, sequencer::SequencerConfig, tce::TceConfig, @@ -26,28 +28,86 @@ pub struct NodeConfig { pub tce: Option, pub sequencer: Option, pub edge: Option, + + #[serde(skip)] + pub home_path: PathBuf, + + #[serde(skip)] + pub node_path: PathBuf, + + #[serde(skip)] + pub edge_path: PathBuf, } impl NodeConfig { - pub fn new(home: &Path, config: Option) -> Self { - let base = load_config::(home, config); + /// Try to create a new node config struct from the given home path and node name. + /// It expects a config file to be present in the node's folder. + /// + /// This `config.toml` can be generated using: `topos node init` command + pub fn try_from( + home_path: &Path, + node_name: &str, + config: Option, + ) -> Result { + let node_path = home_path.join("node").join(node_name); + let config_path = node_path.join("config.toml"); + + // TODO: Move this to `topos-node` when migrated + if !Path::new(&config_path).exists() { + error!( + "Please run 'topos node init --name {node_name}' to create a config file first \ + for {node_name}." + ); + std::process::exit(1); + } + + Ok(Self::build_config(node_path, home_path, config)) + } + + /// Create a new node config struct from the given home path and node name. + /// + /// 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 { + 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 { + 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 + .join("subnet") + .join(base.subnet.clone()) + .join("genesis.json"); let mut config = NodeConfig { + node_path: node_path.to_path_buf(), + edge_path, + home_path: home_path.to_path_buf(), base: base.clone(), sequencer: base .need_sequencer() - .then(|| load_config::(home, None)), + .then(|| load_config::(node_folder, None)), tce: base .need_tce() - .then(|| load_config::(home, None)), + .then(|| load_config::(node_folder, None)), edge: base .need_edge() - .then(|| load_config::(home, None)), + .then(|| load_config::(node_folder, None)), }; // Make the TCE DB path relative to the folder if let Some(config) = config.tce.as_mut() { - config.db_path = home.join(&config.db_path); + config.db_path = node_folder.join(&config.db_path); + debug!( + "Maked TCE DB path relative to the node folder -> {:?}", + config.db_path + ); } config @@ -71,3 +131,12 @@ impl Config for NodeConfig { "default".to_string() } } + +impl From<&NodeConfig> for SecretManager { + fn from(val: &NodeConfig) -> Self { + match val.base.secrets_config.as_ref() { + Some(secrets_config) => SecretManager::from_aws(secrets_config), + None => SecretManager::from_fs(val.node_path.clone()), + } + } +} diff --git a/crates/topos-node/src/lib.rs b/crates/topos-node/src/lib.rs index 748a3e4cb..3b9b6a5a3 100644 --- a/crates/topos-node/src/lib.rs +++ b/crates/topos-node/src/lib.rs @@ -1,5 +1,5 @@ //! Temporary lib exposition for backward topos CLI compatibility -use std::{path::PathBuf, process::ExitStatus}; +use std::process::ExitStatus; use futures::stream::FuturesUnordered; use futures::StreamExt; @@ -18,53 +18,43 @@ use topos_config::{ }; use topos_telemetry::tracing::setup_tracing; use topos_wallet::SecretManager; -use tracing::{error, info}; +use tracing::{debug, error, info}; use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::util::TryInitError; mod process; -#[allow(clippy::too_many_arguments)] +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + GenesisFile(#[from] topos_config::genesis::Error), + + #[error("Unable to setup tracing logger: {0}")] + Tracing(#[from] TryInitError), + + #[error(transparent)] + IO(#[from] std::io::Error), + + #[error( + "The role in the config file expect to have a sequencer config defined, none was found" + )] + MissingSequencerConfig, + + #[error("An Edge config was expected to be found in the config file")] + MissingEdgeConfig, + + #[error("A TCE config was expected to be found in the config file")] + MissingTCEConfig, +} + pub async fn start( verbose: u8, no_color: bool, otlp_agent: Option, otlp_service_name: Option, no_edge_process: bool, - node_path: PathBuf, - home: PathBuf, config: NodeConfig, -) -> Result<(), Box> { - println!( - "⚙️ Reading the configuration from {}/config.toml", - node_path.display() - ); - - // Load genesis pointed by the local config - let genesis_file_path = home - .join("subnet") - .join(config.base.subnet.clone()) - .join("genesis.json"); - - let genesis = match Genesis::new(genesis_file_path.clone()) { - Ok(genesis) => genesis, - Err(_) => { - println!( - "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.", - genesis_file_path.display(), - home.display(), - &config.base.subnet - ); - std::process::exit(1); - } - }; - - // Get secrets - let keys = match &config.base.secrets_config { - Some(secrets_config) => SecretManager::from_aws(secrets_config), - None => SecretManager::from_fs(node_path.clone()), - }; - +) -> Result<(), Error> { // Setup instrumentation if both otlp agent and otlp service name // are provided as arguments let basic_controller = setup_tracing( @@ -75,6 +65,29 @@ pub async fn start( env!("TOPOS_VERSION"), )?; + info!( + "⚙️ Read the configuration from {}/config.toml", + config.node_path.display() + ); + + debug!("TceConfig: {:?}", config); + + let config_ref = &config; + let genesis: Genesis = config_ref.try_into().map_err(|error| { + 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.home_path.display(), + &config.base.subnet + ); + + error + })?; + + // Get secrets + let keys: SecretManager = config_ref.into(); + info!( "🧢 New joiner: {} for the \"{}\" subnet as {:?}", config.base.name, config.base.subnet, config.base.role @@ -88,13 +101,11 @@ pub async fn start( let mut processes = spawn_processes( no_edge_process, config, - node_path, - home, genesis, shutdown_sender, keys, shutdown_token, - ); + )?; let mut sigterm_stream = signal::unix::signal(SignalKind::terminate())?; @@ -130,51 +141,51 @@ pub async fn start( } } }; + Ok(()) } -#[allow(clippy::too_many_arguments)] fn spawn_processes( no_edge_process: bool, - config: NodeConfig, - node_path: PathBuf, - edge_path: PathBuf, + mut config: NodeConfig, genesis: Genesis, shutdown_sender: mpsc::Sender<()>, keys: SecretManager, shutdown_token: CancellationToken, -) -> FuturesUnordered>> { +) -> Result>>, Error> { let processes = FuturesUnordered::new(); // Edge node if no_edge_process { info!("Using external edge node, skip running of local edge instance...") - } else if let Some(edge_config) = config.edge { - let data_dir = node_path.clone(); + } else { + let edge_config = config.edge.take().ok_or(Error::MissingEdgeConfig)?; + + let data_dir = config.node_path.clone(); + info!( "Spawning edge process with genesis file: {}, data directory: {}, additional edge \ arguments: {:?}", - genesis.path.display(), + config.edge_path.display(), data_dir.display(), edge_config.args ); + processes.push(process::spawn_edge_process( - edge_path.join(BINARY_NAME), + config.home_path.join(BINARY_NAME), data_dir, - genesis.path.clone(), + config.edge_path.clone(), edge_config.args, )); - } else { - error!("Missing edge configuration, could not run edge node!"); - std::process::exit(1); } // Sequencer if matches!(config.base.role, NodeRole::Sequencer) { let sequencer_config = config .sequencer - .clone() - .expect("valid sequencer configuration"); + .take() + .ok_or(Error::MissingSequencerConfig)?; + info!( "Running sequencer with configuration {:?}", sequencer_config @@ -188,9 +199,11 @@ fn spawn_processes( // TCE if config.base.subnet == "topos" { + let tce_config = config.tce.ok_or(Error::MissingTCEConfig)?; info!("Running topos TCE service...",); + processes.push(process::spawn_tce_process( - config.tce.unwrap(), + tce_config, keys, genesis, (shutdown_token.clone(), shutdown_sender.clone()), @@ -198,7 +211,7 @@ fn spawn_processes( } drop(shutdown_sender); - processes + Ok(processes) } async fn shutdown( diff --git a/crates/topos-telemetry/src/tracing.rs b/crates/topos-telemetry/src/tracing.rs index 4adf2725e..c8da45cdb 100644 --- a/crates/topos-telemetry/src/tracing.rs +++ b/crates/topos-telemetry/src/tracing.rs @@ -6,6 +6,7 @@ use opentelemetry::{global, KeyValue}; use opentelemetry_otlp::{SpanExporterBuilder, WithExportConfig}; use std::time::Duration; use tracing::Level; +use tracing_subscriber::util::TryInitError; use tracing_subscriber::{ prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, }; @@ -66,7 +67,7 @@ pub fn setup_tracing( otlp_agent: Option, otlp_service_name: Option, version: &'static str, -) -> Result, Box> { +) -> Result, TryInitError> { let mut layers = Vec::new(); let ansi = !no_color; diff --git a/crates/topos/src/components/node/mod.rs b/crates/topos/src/components/node/mod.rs index 66123c510..410791548 100644 --- a/crates/topos/src/components/node/mod.rs +++ b/crates/topos/src/components/node/mod.rs @@ -5,6 +5,7 @@ use std::{ use std::{path::Path, sync::Arc}; use tokio::sync::Mutex; use tonic::transport::{Channel, Endpoint}; +use topos_telemetry::tracing::setup_tracing; use tower::Service; use tracing::error; @@ -40,12 +41,13 @@ pub(crate) async fn handle_command( match subcommands { Some(NodeCommands::Init(cmd)) => { let cmd = *cmd; - let name = cmd.name.as_ref().expect("No name or default was given"); + let name = cmd.name.clone().expect("No name or default was given"); + _ = setup_tracing(verbose, no_color, None, None, env!("TOPOS_VERSION")); // Construct path to node config // will be $TOPOS_HOME/node/default/ with no given name // and $TOPOS_HOME/node// with a given name - let node_path = home.join("node").join(name); + let node_path = home.join("node").join(&name); // If the folders don't exist yet, create it create_dir_all(&node_path).expect("failed to create node folder"); @@ -93,7 +95,7 @@ pub(crate) async fn handle_command( } } - let node_config = NodeConfig::new(&node_path, Some(cmd)); + let node_config = NodeConfig::create(&home, &name, Some(cmd)); // Creating the TOML output let config_toml = match node_config.to_toml() { @@ -132,27 +134,15 @@ pub(crate) async fn handle_command( .name .as_ref() .expect("No name or default was given for node"); - let node_path = home.join("node").join(name); - let config_path = node_path.join("config.toml"); - // TODO: Move this to `topos-node` when migrated - if !Path::new(&config_path).exists() { - println!( - "Please run 'topos node init --name {name}' to create a config file first for \ - {name}." - ); - std::process::exit(1); - } + let config = NodeConfig::try_from(&home, name, Some(command))?; - let config = NodeConfig::new(&node_path, Some(command)); topos_node::start( verbose, no_color, cmd_cloned.otlp_agent, cmd_cloned.otlp_service_name, cmd_cloned.no_edge_process, - node_path, - home, config, ) .await?; diff --git a/crates/topos/tests/config.rs b/crates/topos/tests/config.rs index 270249010..3ac3c53c8 100644 --- a/crates/topos/tests/config.rs +++ b/crates/topos/tests/config.rs @@ -147,12 +147,16 @@ mod serial_integration { } /// Test node init env arguments + #[rstest] #[tokio::test] - async fn command_init_precedence_env() -> Result<(), Box> { - let tmp_home_directory = tempdir()?; - + async fn command_init_precedence_env( + create_folder: PathBuf, + ) -> Result<(), Box> { + let tmp_home_directory = create_folder; // Test node init with env variables - let node_init_home_env = tmp_home_directory.path().to_str().unwrap(); + let node_init_home_env = tmp_home_directory + .to_str() + .expect("path names are valid utf-8"); let node_edge_path_env = setup_polygon_edge(node_init_home_env).await; let node_init_name_env = "TEST_NODE_ENV"; let node_init_role_env = "full-node"; @@ -182,7 +186,6 @@ mod serial_integration { assert!(config_path.exists()); // Check if config file params are according to env params let config_contents = std::fs::read_to_string(&config_path).unwrap(); - println!("{:#?}", config_contents); assert!(config_contents.contains("name = \"TEST_NODE_ENV\"")); assert!(config_contents.contains("role = \"fullnode\"")); assert!(config_contents.contains("subnet = \"topos-env\"")); @@ -193,17 +196,17 @@ mod serial_integration { /// Test node cli arguments precedence over env arguments #[tokio::test] async fn command_init_precedence_cli_env() -> Result<(), Box> { - let tmp_home_dir_env = tempdir()?; - let tmp_home_dir_cli = tempdir()?; + let tmp_home_dir_env = create_folder("command_init_precedence_cli_env"); + let tmp_home_dir_cli = create_folder("command_init_precedence_cli_env"); // Test node init with both cli and env flags // Cli arguments should take precedence over env variables - let node_init_home_env = tmp_home_dir_env.path().to_str().unwrap(); + let node_init_home_env = tmp_home_dir_env.to_str().unwrap(); let node_edge_path_env = setup_polygon_edge(node_init_home_env).await; let node_init_name_env = "TEST_NODE_ENV"; let node_init_role_env = "full-node"; let node_init_subnet_env = "topos-env"; - let node_init_home_cli = tmp_home_dir_cli.path().to_str().unwrap(); + let node_init_home_cli = tmp_home_dir_cli.to_str().unwrap(); let node_edge_path_cli = node_edge_path_env.clone(); let node_init_name_cli = "TEST_NODE_CLI"; let node_init_role_cli = "sequencer"; @@ -359,7 +362,6 @@ mod serial_integration { 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"); - println!("config path {:?}", config_path); assert!(config_path.exists()); let mut file = OpenOptions::new() @@ -394,7 +396,6 @@ mod serial_integration { // Generate polygon edge genesis file let polygon_edge_bin = format!("{}/polygon-edge", node_edge_path_env); - println!("polygon_edge_bin {:?}", polygon_edge_bin); utils::generate_polygon_edge_genesis_file( &polygon_edge_bin, node_up_home_env, @@ -430,7 +431,7 @@ mod serial_integration { if let Ok(output) = cmd.wait_with_output().await { assert!(output.status.success()); - let stdout = output.unwrap().unwrap().stdout; + let stdout = output.stdout; let stdout = String::from_utf8_lossy(&stdout); let reg =