diff --git a/Cargo.lock b/Cargo.lock index 280b22322e..85565d0fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7600,12 +7600,15 @@ version = "0.1.0" dependencies = [ "anyhow", "clap 4.4.7", + "dotenvy", + "humantime 2.1.0", "humantime-serde", "lazy_static", "nix 0.27.1", "nym-bin-common", "nym-config", "serde", + "thiserror", "tokio", "tracing", ] diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 3778b480b9..667dd52307 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4.31", features = ["serde"] } clap = { workspace = true, features = ["cargo", "derive"] } -dotenvy = "0.15.6" +dotenvy = { workspace = true } humantime-serde = "1.0" isocountry = "0.3.2" itertools = "0.10.3" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 18c3bc6a3c..c7b7f89459 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -10,7 +10,7 @@ bs58 = "0.4" clap = { workspace = true, features = ["derive"] } clap_complete = "4.0" clap_complete_fig = "4.0" -dotenvy = "0.15.6" +dotenvy = { workspace = true } log = { workspace = true } pretty_env_logger = "0.4" serde = { version = "1", features = ["derive"] } diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 28a7739ff5..77c07a52f8 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -13,11 +13,14 @@ license.workspace = true [dependencies] anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } +dotenvy = { workspace = true } +humantime = "2.1.0" humantime-serde = "1.1.1" lazy_static = { workspace = true } nix = "0.27.1" serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt", "macros", "signal", "process"] } +thiserror = { workspace = true } tracing = { workspace = true } nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } diff --git a/tools/nymvisor/src/cli/add_upgrade.rs b/tools/nymvisor/src/cli/add_upgrade.rs index 0747cbbcc2..2ae798808a 100644 --- a/tools/nymvisor/src/cli/add_upgrade.rs +++ b/tools/nymvisor/src/cli/add_upgrade.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::NymvisorError; use nym_bin_common::output_format::OutputFormat; #[derive(clap::Args, Debug)] @@ -9,7 +10,7 @@ pub(crate) struct Args { output: OutputFormat, } -pub(crate) fn execute(args: Args) -> anyhow::Result<()> { +pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { println!("add upgrade"); Ok(()) } diff --git a/tools/nymvisor/src/cli/build_info.rs b/tools/nymvisor/src/cli/build_info.rs index 00d175150f..07c8b3545a 100644 --- a/tools/nymvisor/src/cli/build_info.rs +++ b/tools/nymvisor/src/cli/build_info.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::NymvisorError; use nym_bin_common::bin_info_owned; use nym_bin_common::output_format::OutputFormat; @@ -10,7 +11,7 @@ pub(crate) struct Args { output: OutputFormat, } -pub(crate) fn execute(args: Args) -> anyhow::Result<()> { +pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { println!("{}", args.output.format(&bin_info_owned!())); Ok(()) } diff --git a/tools/nymvisor/src/cli/config.rs b/tools/nymvisor/src/cli/config.rs index bb545e3ea1..e85ad2ce37 100644 --- a/tools/nymvisor/src/cli/config.rs +++ b/tools/nymvisor/src/cli/config.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::NymvisorError; use nym_bin_common::output_format::OutputFormat; #[derive(clap::Args, Debug)] @@ -9,7 +10,7 @@ pub(crate) struct Args { output: OutputFormat, } -pub(crate) fn execute(args: Args) -> anyhow::Result<()> { +pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { println!("config"); Ok(()) } diff --git a/tools/nymvisor/src/cli/init.rs b/tools/nymvisor/src/cli/init.rs index 7663483ddf..5403f9d690 100644 --- a/tools/nymvisor/src/cli/init.rs +++ b/tools/nymvisor/src/cli/init.rs @@ -1,6 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::env::Env; +use crate::error::NymvisorError; use nym_bin_common::output_format::OutputFormat; #[derive(clap::Args, Debug)] @@ -9,7 +11,9 @@ pub(crate) struct Args { output: OutputFormat, } -pub(crate) fn execute(args: Args) -> anyhow::Result<()> { +pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { + let env = Env::try_read()?; + println!("init"); Ok(()) } diff --git a/tools/nymvisor/src/cli/mod.rs b/tools/nymvisor/src/cli/mod.rs index 4f7a81d9dd..3d8c0444e7 100644 --- a/tools/nymvisor/src/cli/mod.rs +++ b/tools/nymvisor/src/cli/mod.rs @@ -7,6 +7,8 @@ mod config; mod init; mod run; +use crate::env::setup_env; +use crate::error::NymvisorError; use clap::{Parser, Subcommand}; use lazy_static::lazy_static; use nym_bin_common::bin_info; @@ -23,14 +25,18 @@ fn pretty_build_info_static() -> &'static str { #[derive(Parser, Debug)] #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Cli { - // I doubt we're gonna need any global flags here, but I'm going to leave the the option open - // so that'd be easier to add them later if needed + /// Path pointing to an env file that configures the nymvisor and overrides any preconfigured values. + #[clap(short, long)] + pub(crate) config_env_file: Option, + #[clap(subcommand)] command: Commands, } impl Cli { - pub(crate) fn execute(self) -> anyhow::Result<()> { + pub(crate) fn execute(self) -> Result<(), NymvisorError> { + setup_env(&self.config_env_file)?; + match self.command { Commands::Init(args) => init::execute(args), Commands::Run(args) => run::execute(args), diff --git a/tools/nymvisor/src/cli/run.rs b/tools/nymvisor/src/cli/run.rs index 7dac6fed41..9897e06b44 100644 --- a/tools/nymvisor/src/cli/run.rs +++ b/tools/nymvisor/src/cli/run.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::NymvisorError; use tokio::runtime; #[derive(clap::Args, Debug)] @@ -10,7 +11,7 @@ pub(crate) struct Args { daemon_args: Vec, } -pub(crate) fn execute(args: Args) -> anyhow::Result<()> { +pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { // TODO: experiment with the minimal runtime // look at futures::executor::LocalPool let rt = runtime::Builder::new_current_thread().enable_io().build()?; diff --git a/tools/nymvisor/src/config/mod.rs b/tools/nymvisor/src/config/mod.rs index b5d3e738fe..d3de2b39ed 100644 --- a/tools/nymvisor/src/config/mod.rs +++ b/tools/nymvisor/src/config/mod.rs @@ -123,6 +123,7 @@ impl Config { #[serde(deny_unknown_fields)] pub struct Nymvisor { /// ID specifies the human readable ID of this particular nymvisor instance. + /// Can be overridden with $NYMVISOR_ID environmental variable. pub id: String, /// Further optional configuration options associated with the nymvisor. @@ -134,10 +135,12 @@ pub struct Nymvisor { pub struct NymvisorDebug { /// If set to true, this will disable `nymvisor` logs (but not the underlying process) /// default: false + /// Can be overridden with $NYMVISOR_DISABLE_LOGS environmental variable. pub disable_logs: bool, /// Set custom directory for upgrade data - binaries and upgrade plans. /// If not set, the global nymvisors' data directory will be used instead. + /// Can be overridden with $NYMVISOR_DATA_UPGRADE_DIRECTORY environmental variable. #[serde(deserialize_with = "de_maybe_path")] pub data_upgrade_directory: Option, } @@ -156,11 +159,13 @@ impl Default for NymvisorDebug { #[serde(deny_unknown_fields)] pub struct Daemon { /// The name of the managed binary itself (e.g. nym-api, nym-mixnode, nym-gateway, etc.) + /// Can be overridden with $DAEMON_NAME environmental variable. pub name: String, /// The location where the `nymvisor/` directory is kept that contains the auxiliary files associated /// with the underlying daemon, such as any backups or current version information. /// (e.g. $HOME/.nym/nym-api/my-nym-api, $HOME/.nym/mixnodes/my-mixnode, etc.). + /// Can be overridden with $DAEMON_HOME environmental variable. pub home: String, /// Further optional configuration options associated with the daemon. @@ -172,26 +177,31 @@ pub struct Daemon { pub struct DaemonDebug { /// If set to true, this will enable auto-downloading of new binaries using the url provided in the `upgrade-info.json` /// default: true + /// Can be overridden with $DAEMON_ALLOW_BINARIES_DOWNLOAD environmental variable. pub allow_binaries_download: bool, /// If enabled nymvisor will require that a checksum is provided in the upgrade plan for the binary to be downloaded. /// If disabled, nymvisor will not require a checksum to be provided, but still check the checksum if one is provided. /// default: true + /// Can be overridden with $DAEMON_ENFORCE_DOWNLOAD_CHECKSUM environmental variable. pub enforce_download_checksum: bool, /// If enabled, nymvisor will restart the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade. /// Otherwise (if disabled), nymvisor will stop running after an upgrade and will require the system administrator to manually restart it. /// Note restart is only after the upgrade and does not auto-restart the subprocess after an error occurs. /// default: true + /// Can be overridden with $DAEMON_RESTART_AFTER_UPGRADE environmental variable. pub restart_after_upgrade: bool, /// If enabled, nymvisor will restart the subprocess with the same command-line arguments and flags after it has crashed /// default: false + /// Can be overridden with $DAEMON_RESTART_ON_FAILURE environmental variable. pub restart_on_failure: bool, /// If `restart_on_failure` is enabled, the following value defines the amount of time `nymvisor` shall wait before /// restarting the subprocess. /// default: 10s + /// Can be overridden with $DAEMON_FAILURE_RESTART_DELAY environmental variable. // The default value is so relatively high as to prevent constant restart loops in case of some underlying issue. #[serde(with = "humantime_serde")] pub failure_restart_delay: Duration, @@ -199,11 +209,13 @@ pub struct DaemonDebug { /// Defines the maximum number of startup failures the subprocess can experience in a quick succession before /// no further restarts will be attempted and `nymvisor` will exit/ /// default: 10 + /// Can be overridden with $DAEMON_MAX_STARTUP_FAILURES environmental variable. pub max_startup_failures: usize, /// Defines the length of time during which the subprocess is still considered to be in the startup phase /// when its failures are going to be considered in `max_startup_failures`. /// default: 120s + /// Can be overridden with $DAEMON_STARTUP_PERIOD_DURATION environmental variable. #[serde(with = "humantime_serde")] pub startup_period_duration: Duration, @@ -211,15 +223,18 @@ pub struct DaemonDebug { /// (for either an upgrade or shutdown of the `nymvisor` itself) /// Once the time passes, a kill signal is going to be sent instead. /// default: 10s + /// Can be overridden with $DAEMON_SHUTDOWN_GRACE_PERIOD environmental variable. #[serde(with = "humantime_serde")] pub shutdown_grace_period: Duration, /// Set custom backup directory for daemon data. If not set, the daemon's home directory will be used instead. + /// Can be overridden with $DAEMON_DATA_BACKUP_DIRECTORYenvironmental variable. #[serde(deserialize_with = "de_maybe_path")] pub data_backup_directory: Option, /// If enabled, `nymvisor` will perform upgrades directly without performing any backups. /// default: false + /// Can be overridden with $DAEMON_UNSAFE_SKIP_BACKUP environmental variable. pub unsafe_skip_backup: bool, } diff --git a/tools/nymvisor/src/env.rs b/tools/nymvisor/src/env.rs new file mode 100644 index 0000000000..3ffea4979a --- /dev/null +++ b/tools/nymvisor/src/env.rs @@ -0,0 +1,147 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NymvisorError; +use std::env::VarError; +use std::path::PathBuf; +use std::time::Duration; + +const TRUTHY_BOOLS: &[&str] = &["true", "t", "1"]; +const FALSY_BOOLS: &[&str] = &["false", "f", "0"]; + +pub mod vars { + pub const NYMVISOR_ID: &str = "NYMVISOR_ID"; + pub const NYMVISOR_CONFIG_PATH: &str = "NYMVISOR_CONFIG_PATH"; + pub const NYMVISOR_DATA_DIRECTORY: &str = "NYMVISOR_DATA_DIRECTORY"; + pub const NYMVISOR_DISABLE_LOGS: &str = "NYMVISOR_DISABLE_LOGS"; + pub const NYMVISOR_DATA_UPGRADE_DIRECTORY: &str = "NYMVISOR_DATA_UPGRADE_DIRECTORY"; + + pub const DAEMON_NAME: &str = "DAEMON_NAME"; + pub const DAEMON_HOME: &str = "DAEMON_HOME"; + pub const DAEMON_ALLOW_BINARIES_DOWNLOAD: &str = "DAEMON_ALLOW_BINARIES_DOWNLOAD"; + pub const DAEMON_ENFORCE_DOWNLOAD_CHECKSUM: &str = "DAEMON_ENFORCE_DOWNLOAD_CHECKSUM"; + pub const DAEMON_RESTART_AFTER_UPGRADE: &str = "DAEMON_RESTART_AFTER_UPGRADE"; + pub const DAEMON_RESTART_ON_FAILURE: &str = "DAEMON_RESTART_ON_FAILURE"; + pub const DAEMON_FAILURE_RESTART_DELAY: &str = "DAEMON_FAILURE_RESTART_DELAY"; + pub const DAEMON_MAX_STARTUP_FAILURES: &str = "DAEMON_MAX_STARTUP_FAILURES"; + pub const DAEMON_STARTUP_PERIOD_DURATION: &str = "DAEMON_STARTUP_PERIOD_DURATION"; + pub const DAEMON_SHUTDOWN_GRACE_PERIOD: &str = "DAEMON_SHUTDOWN_GRACE_PERIOD"; + pub const DAEMON_DATA_BACKUP_DIRECTORY: &str = "DAEMON_DATA_BACKUP_DIRECTORY"; + pub const DAEMON_UNSAFE_SKIP_BACKUP: &str = "DAEMON_UNSAFE_SKIP_BACKUP"; +} + +pub(crate) fn setup_env(config_env_file: &Option) -> Result<(), NymvisorError> { + if let Some(env_file) = config_env_file { + dotenvy::from_path_override(env_file).map_err(Into::into) + } else { + Ok(()) + } +} + +pub(crate) struct Env { + pub(crate) nymvisor_id: Option, + pub(crate) nymvisor_config_path: Option, + pub(crate) nymvisor_data_directory: Option, + pub(crate) nymvisor_disable_logs: Option, + pub(crate) nymvisor_data_upgrade_directory: Option, + + pub(crate) daemon_name: Option, + pub(crate) daemon_home: Option, + pub(crate) daemon_allow_binaries_download: Option, + pub(crate) daemon_enforce_download_checksum: Option, + pub(crate) daemon_restart_after_upgrade: Option, + pub(crate) daemon_restart_on_failure: Option, + pub(crate) daemon_failure_restart_delay: Option, + pub(crate) daemon_max_startup_failures: Option, + pub(crate) daemon_startup_period_duration: Option, + pub(crate) daemon_shutdown_grace_period: Option, + pub(crate) daemon_data_backup_directory: Option, + pub(crate) daemon_unsafe_skip_backup: Option, +} + +// TODO: all of those seem like they could be moved to some common crate if we ever needed similar functionality elsewhere +fn read_string(var: &str) -> Result, NymvisorError> { + match std::env::var(var) { + Ok(val) => Ok(Some(val)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(value)) => Err(NymvisorError::MalformedEnvVariable { + variable: var.to_string(), + value, + }), + } +} + +fn read_bool(var: &str) -> Result, NymvisorError> { + read_string(var)? + .map(|raw| { + let normalised = raw.to_ascii_lowercase(); + if TRUTHY_BOOLS.contains(&&*normalised) { + Ok(true) + } else if FALSY_BOOLS.contains(&&*normalised) { + Ok(false) + } else { + Err(NymvisorError::MalformedBoolEnvVariable { + variable: var.to_string(), + value: raw.to_string(), + }) + } + }) + .transpose() +} + +fn read_duration(var: &str) -> Result, NymvisorError> { + read_string(var)? + .map(|raw| { + humantime::parse_duration(&raw).map_err(|source| { + NymvisorError::MalformedDurationEnvVariable { + variable: var.to_string(), + value: raw.to_string(), + source, + } + }) + }) + .transpose() +} + +fn read_pathbuf(var: &str) -> Result, NymvisorError> { + Ok(read_string(var)?.map(PathBuf::from)) +} + +fn read_usize(var: &str) -> Result, NymvisorError> { + read_string(var)? + .map(|raw| { + raw.parse() + .map_err(|source| NymvisorError::MalformedNumberEnvVariable { + variable: var.to_string(), + value: raw.to_string(), + source, + }) + }) + .transpose() +} + +impl Env { + // in general, if variable is missing from the environment that's fine. + // however, if something is out there, it MUST BE valid + pub(crate) fn try_read() -> Result { + Ok(Env { + nymvisor_id: read_string(vars::NYMVISOR_ID)?, + nymvisor_config_path: read_pathbuf(vars::NYMVISOR_CONFIG_PATH)?, + nymvisor_data_directory: read_pathbuf(vars::NYMVISOR_DATA_DIRECTORY)?, + nymvisor_disable_logs: read_bool(vars::NYMVISOR_DISABLE_LOGS)?, + nymvisor_data_upgrade_directory: read_pathbuf(vars::NYMVISOR_DATA_UPGRADE_DIRECTORY)?, + daemon_name: read_string(vars::DAEMON_NAME)?, + daemon_home: read_pathbuf(vars::DAEMON_HOME)?, + daemon_allow_binaries_download: read_bool(vars::DAEMON_ALLOW_BINARIES_DOWNLOAD)?, + daemon_enforce_download_checksum: read_bool(vars::DAEMON_ENFORCE_DOWNLOAD_CHECKSUM)?, + daemon_restart_after_upgrade: read_bool(vars::DAEMON_RESTART_AFTER_UPGRADE)?, + daemon_restart_on_failure: read_bool(vars::DAEMON_RESTART_ON_FAILURE)?, + daemon_failure_restart_delay: read_duration(vars::DAEMON_FAILURE_RESTART_DELAY)?, + daemon_max_startup_failures: read_usize(vars::DAEMON_MAX_STARTUP_FAILURES)?, + daemon_startup_period_duration: read_duration(vars::DAEMON_STARTUP_PERIOD_DURATION)?, + daemon_shutdown_grace_period: read_duration(vars::DAEMON_SHUTDOWN_GRACE_PERIOD)?, + daemon_data_backup_directory: read_pathbuf(vars::DAEMON_DATA_BACKUP_DIRECTORY)?, + daemon_unsafe_skip_backup: read_bool(vars::DAEMON_UNSAFE_SKIP_BACKUP)?, + }) + } +} diff --git a/tools/nymvisor/src/error.rs b/tools/nymvisor/src/error.rs new file mode 100644 index 0000000000..abcd39fff9 --- /dev/null +++ b/tools/nymvisor/src/error.rs @@ -0,0 +1,59 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::OsString; +use std::io; +use std::num::ParseIntError; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub(crate) enum NymvisorError { + #[error( + "failed to load config file for id {id} using path '{}'. detailed message: {source}", path.display() + )] + ConfigLoadFailure { + id: String, + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error( + "failed to save config file for id {id} using path '{}'. detailed message: {source}", path.display() + )] + ConfigSaveFailure { + id: String, + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error("the provided env file was malformed: {source}")] + MalformedEnvFile { + #[from] + source: dotenvy::Error, + }, + + #[error("the value provided for environmental variable '{variable}' was not valid unicode: {value:?}")] + MalformedEnvVariable { variable: String, value: OsString }, + + #[error("the value provided for environmental boolean variable '{variable}': '{value}' is not a valid boolean")] + MalformedBoolEnvVariable { variable: String, value: String }, + + #[error("the value provided for environmental duration variable '{variable}': '{value}' is not a valid duration: {source}")] + MalformedDurationEnvVariable { + variable: String, + value: String, + #[source] + source: humantime::DurationError, + }, + + #[error("the value provided for environmental numerical variable '{variable}': '{value}' is not a valid number: {source}")] + MalformedNumberEnvVariable { + variable: String, + value: String, + #[source] + source: ParseIntError, + }, +} diff --git a/tools/nymvisor/src/main.rs b/tools/nymvisor/src/main.rs index 09e262bfae..e15f370c59 100644 --- a/tools/nymvisor/src/main.rs +++ b/tools/nymvisor/src/main.rs @@ -6,10 +6,12 @@ use clap::Parser; pub(crate) mod cli; pub(crate) mod config; +pub(crate) mod env; +pub(crate) mod error; fn main() -> anyhow::Result<()> { let args = Cli::parse(); println!("{args:#?}"); - args.execute() + Ok(args.execute()?) }