From 8bba3eab395053ef7dedab464f31ca5ced9e03d2 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 16 Nov 2023 22:09:35 +0300 Subject: [PATCH] feat(config): add multiple file config sources (#1907) --- crates/peer-metrics/Cargo.toml | 2 +- crates/server-config/Cargo.toml | 1 - crates/server-config/src/args.rs | 13 +- crates/server-config/src/defaults.rs | 9 - crates/server-config/src/node_config.rs | 31 ---- crates/server-config/src/resolved_config.rs | 192 ++++++++++++++++---- nox/src/main.rs | 8 +- 7 files changed, 176 insertions(+), 80 deletions(-) diff --git a/crates/peer-metrics/Cargo.toml b/crates/peer-metrics/Cargo.toml index f991cf4384..f772a74888 100644 --- a/crates/peer-metrics/Cargo.toml +++ b/crates/peer-metrics/Cargo.toml @@ -11,7 +11,7 @@ fluence-app-service = { workspace = true } fluence-libp2p = { workspace = true } particle-execution = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["macros", "tracing"] } tokio-stream = { workspace = true } futures = { workspace = true } serde = { version = "1.0.192", features = ["derive"] } diff --git a/crates/server-config/Cargo.toml b/crates/server-config/Cargo.toml index 559d3537f9..173ae08cd2 100644 --- a/crates/server-config/Cargo.toml +++ b/crates/server-config/Cargo.toml @@ -15,7 +15,6 @@ fluence-keypair = { workspace = true } log = "0.4.20" toml = "0.7.3" - libp2p = { workspace = true } libp2p-metrics = { workspace = true } libp2p-connection-limits = { workspace = true } diff --git a/crates/server-config/src/args.rs b/crates/server-config/src/args.rs index a20ba96634..a86ee4f074 100644 --- a/crates/server-config/src/args.rs +++ b/crates/server-config/src/args.rs @@ -359,14 +359,21 @@ pub(crate) struct DerivedArgs { #[arg( short('c'), - long, + long("config"), id = "CONFIG_FILE", help_heading = "Node configuration", help = "TOML configuration file", + long_help = "TOML configuration file. If not specified, the default configuration is used. \ + If specified, the default configuration is merged with the specified one. \ + The argument can by used multiple times. \ + The last configuration overrides the previous ones.", value_name = "PATH", - display_order = 15 + num_args(1..), + value_delimiter(','), + display_order = 15, + )] - pub(crate) config: Option, + pub(crate) configs: Option>, #[arg( short('d'), long, diff --git a/crates/server-config/src/defaults.rs b/crates/server-config/src/defaults.rs index fcaaeda786..fcba7d5c7a 100644 --- a/crates/server-config/src/defaults.rs +++ b/crates/server-config/src/defaults.rs @@ -54,11 +54,6 @@ pub fn default_connection_idle_timeout() -> Duration { pub fn default_max_established_per_peer_limit() -> Option { Some(5) } - -pub fn default_auto_particle_ttl() -> Duration { - Duration::from_secs(200) -} - pub fn default_bootstrap_nodes() -> Vec { vec![] } @@ -164,10 +159,6 @@ pub fn default_execution_timeout() -> Duration { Duration::from_secs(20) } -pub fn default_autodeploy_retry_attempts() -> u16 { - 5 -} - pub fn default_processing_timeout() -> Duration { Duration::from_secs(120) } diff --git a/crates/server-config/src/node_config.rs b/crates/server-config/src/node_config.rs index 8decc70778..7a9b0582b8 100644 --- a/crates/server-config/src/node_config.rs +++ b/crates/server-config/src/node_config.rs @@ -35,20 +35,6 @@ pub struct UnresolvedNodeConfig { #[serde(default)] pub builtins_key_pair: Option, - /// Particle ttl for autodeploy - #[serde(default = "default_auto_particle_ttl")] - #[serde(with = "humantime_serde")] - pub autodeploy_particle_ttl: Duration, - - /// Configure the number of ping attempts to check the readiness of the vm pool. - /// Total wait time is the autodeploy_particle_ttl times the number of attempts. - #[serde(default = "default_autodeploy_retry_attempts")] - pub autodeploy_retry_attempts: u16, - - /// Affects builtins autodeploy. If set to true, then all builtins should be recreated and their state is cleaned up. - #[serde(default)] - pub force_builtins_redeploy: bool, - #[serde(flatten)] pub transport_config: TransportConfig, @@ -188,10 +174,6 @@ impl UnresolvedNodeConfig { allow_local_addresses: self.allow_local_addresses, particle_execution_timeout: self.particle_execution_timeout, management_peer_id: self.management_peer_id, - - autodeploy_particle_ttl: self.autodeploy_particle_ttl, - autodeploy_retry_attempts: self.autodeploy_retry_attempts, - force_builtins_redeploy: self.force_builtins_redeploy, transport_config: self.transport_config, listen_config: self.listen_config, allowed_binaries, @@ -305,16 +287,6 @@ pub struct NodeConfig { #[derivative(Debug = "ignore")] pub builtins_key_pair: KeyPair, - /// Particle ttl for autodeploy - pub autodeploy_particle_ttl: Duration, - - /// Configure the number of ping attempts to check the readiness of the vm pool. - /// Total wait time is the autodeploy_particle_ttl times the number of attempts. - pub autodeploy_retry_attempts: u16, - - /// Affects builtins autodeploy. If set to true, then all builtins should be recreated and their state is cleaned up. - pub force_builtins_redeploy: bool, - pub transport_config: TransportConfig, pub listen_config: ListenConfig, @@ -452,9 +424,6 @@ pub struct ListenConfig { /// For ws connections #[serde(default = "default_websocket_port")] pub websocket_port: u16, - - #[serde(default)] - pub listen_multiaddrs: Vec, } #[derive(Clone, Deserialize, Serialize, Debug, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/crates/server-config/src/resolved_config.rs b/crates/server-config/src/resolved_config.rs index 9362a2e2d5..a5ff282f4d 100644 --- a/crates/server-config/src/resolved_config.rs +++ b/crates/server-config/src/resolved_config.rs @@ -21,11 +21,12 @@ use std::path::PathBuf; use std::str::FromStr; use clap::{Args, Command, FromArgMatches}; -use config::{Config, Environment, File, FileFormat}; +use config::{Config, Environment, File, FileFormat, FileSourceFile}; use libp2p::core::{multiaddr::Protocol, Multiaddr}; use serde::{Deserialize, Serialize}; use crate::args; +use crate::args::DerivedArgs; use crate::dir_config::{ResolvedDirConfig, UnresolvedDirConfig}; use crate::node_config::{NodeConfig, UnresolvedNodeConfig}; @@ -171,6 +172,23 @@ pub struct ConfigData { pub description: String, } +/// Hierarchically loads the configuration using args and envs. +/// The source order is: +/// - Load and parse Config.toml from cwd, if exists +/// - Load and parse files provided by FLUENCE_CONFIG env var +/// - Load and parse files provided by --config arg +/// - Load config values from env vars +/// - Load config values from args (throw error on conflicts with env vars) +/// On each stage the values override the previous ones. +/// +/// # Arguments +/// +/// - `data`: Optional `ConfigData` to customize the configuration. +/// +/// # Returns +/// +/// Returns a Result containing the unresolved configuration or an Eyre error. +/// pub fn load_config(data: Option) -> eyre::Result { let raw_args = std::env::args_os().collect::>(); load_config_with_args(raw_args, data) @@ -180,31 +198,18 @@ pub fn load_config_with_args( raw_args: Vec, data: Option, ) -> eyre::Result { - let command = Command::new("Fluence peer"); - let command = if let Some(data) = data { - command - .version(&data.version) - .author(&data.authors) - .about(data.description) - .override_usage(format!("{} [FLAGS] [OPTIONS]", data.binary_name)) - } else { - command - }; - - let raw_cli_config = args::DerivedArgs::augment_args(command); - let matches = raw_cli_config.get_matches_from(raw_args); - let cli_config = args::DerivedArgs::from_arg_matches(&matches)?; - - let file_source = cli_config - .config - .clone() - .or_else(|| std::env::var_os("FLUENCE_CONFIG").map(PathBuf::from)) - .map(|path| File::from(path).format(FileFormat::Toml)) - .unwrap_or( - File::with_name("Config.toml") - .required(false) - .format(FileFormat::Toml), - ); + let arg_source = process_args(raw_args, data)?; + + let arg_config_sources: Vec> = arg_source + .configs + .iter() + .flat_map(|paths| { + paths + .iter() + .map(|path| File::from(path.clone()).format(FileFormat::Toml)) + .collect::>>() + }) + .collect(); let env_source = Environment::with_prefix("FLUENCE") .try_parsing(true) @@ -217,17 +222,57 @@ pub fn load_config_with_args( .with_list_parse_key("listen_config.listen_multiaddrs") .with_list_parse_key("system_services.enable"); - let config = Config::builder() - .add_source(file_source) - .add_source(env_source) - .add_source(cli_config) - .build()?; + let env_config_sources: Vec> = + std::env::var_os("FLUENCE_CONFIG") + .and_then(|str| str.into_string().ok()) + .map(|str| { + str.trim() + .split(",") + .map(PathBuf::from) + .map(|path| File::from(path.clone()).format(FileFormat::Toml)) + .collect() + }) + .unwrap_or_default(); + + let mut config_builder = Config::builder().add_source( + File::with_name("Config.toml") + .required(false) + .format(FileFormat::Toml), + ); + + for source in env_config_sources { + config_builder = config_builder.add_source(source) + } + + for source in arg_config_sources { + config_builder = config_builder.add_source(source) + } + config_builder = config_builder.add_source(env_source).add_source(arg_source); + let config = config_builder.build()?; let config: UnresolvedConfig = config.try_deserialize()?; Ok(config) } +fn process_args(raw_args: Vec, data: Option) -> eyre::Result { + let command = Command::new("Fluence peer"); + let command = if let Some(data) = data { + command + .version(&data.version) + .author(&data.authors) + .about(data.description) + .override_usage(format!("{} [FLAGS] [OPTIONS]", data.binary_name)) + } else { + command + }; + + let raw_cli_config = args::DerivedArgs::augment_args(command); + let matches = raw_cli_config.get_matches_from(raw_args); + let arg_source = args::DerivedArgs::from_arg_matches(&matches)?; + Ok(arg_source) +} + #[cfg(test)] mod tests { use std::io::Write; @@ -763,4 +808,89 @@ mod tests { ); }); } + + #[test] + fn load_multiple_configs() { + let mut file = NamedTempFile::new().expect("Could not create temp file"); + write!( + file, + r#" + [protocol_config] + upgrade_timeout = "60s" + "# + ) + .expect("Could not write in file"); + + let path = file.path().display().to_string(); + + let mut file2 = NamedTempFile::new().expect("Could not create temp file"); + write!( + file2, + r#" + allowed_binaries = [ + "/usr/bin/curl", + "/usr/bin/ipfs", + "/usr/bin/glaze", + "/usr/bin/bitcoin-cli" + ] + websocket_port = 1234 + "# + ) + .expect("Could not write in file"); + + let path2 = file2.path().display().to_string(); + + let mut file3 = NamedTempFile::new().expect("Could not create temp file"); + write!( + file3, + r#" + websocket_port = 666 + "# + ) + .expect("Could not write in file"); + + let path3 = file3.path().display().to_string(); + + let mut file4 = NamedTempFile::new().expect("Could not create temp file"); + write!( + file4, + r#" + aquavm_pool_size = 160 + "# + ) + .expect("Could not write in file"); + + let path4 = file4.path().display().to_string(); + + let args = vec![ + OsString::from("nox"), + OsString::from("--config"), + OsString::from(path3.to_string()), + OsString::from("--config"), + OsString::from(path4.to_string()), + ]; + + temp_env::with_var( + "FLUENCE_CONFIG", + Some(format!("{},{}", path, path2)), + || { + let config = load_config_with_args(args, None).expect("Could not load config"); + assert_eq!( + config.node_config.protocol_config.upgrade_timeout, + Duration::from_secs(60) + ); + assert_eq!( + config.node_config.allowed_binaries, + vec![ + "/usr/bin/curl", + "/usr/bin/ipfs", + "/usr/bin/glaze", + "/usr/bin/bitcoin-cli" + ] + ); + assert_eq!(config.node_config.listen_config.websocket_port, 666); + assert_eq!(config.node_config.aquavm_pool_size, 160); + }, + ); + } } diff --git a/nox/src/main.rs b/nox/src/main.rs index afe7106f19..a373ad20b0 100644 --- a/nox/src/main.rs +++ b/nox/src/main.rs @@ -106,10 +106,6 @@ fn main() -> eyre::Result<()> { .build() .expect("Could not make tokio runtime") .block_on(async { - if let Some(true) = config.print_config { - log::info!("Loaded config: {:#?}", config); - } - let resolver_config = config.clone().resolve()?; let key_pair = resolver_config.node_config.root_key_pair.clone(); @@ -123,6 +119,10 @@ fn main() -> eyre::Result<()> { .with(tracing_layer(&config.tracing, peer_id, VERSION)?) .init(); + if let Some(true) = config.print_config { + log::info!("Loaded config: {:#?}", config); + } + log::info!("node public key = {}", base64_key_pair); log::info!("node server peer id = {}", peer_id);