diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml index 1a082a1ad2..941a8619f0 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -1,11 +1,55 @@ name: Build and upload Node Status API container to harbor.nymte.ch - on: workflow_dispatch: +env: + WORKING_DIRECTORY: "nym-node-status-api" + CONTAINER_NAME: "node-status-api" + jobs: - my-job: - runs-on: arc-ubuntu-22.04 + build-container: + runs-on: arc-ubuntu-22.04-dind steps: - - name: my-step - run: echo "Hello World!" + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.result }} already exists" + fi + + - name: Remove existing tag if exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/envs/mainnet.env b/envs/mainnet.env index e40e54a471..419547bf97 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -21,8 +21,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mp REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYXD="https://rpc.nymtech.net" -NYM_API="https://validator.nymtech.net/api/" +NYXD=https://rpc.nymtech.net +NYM_API=https://validator.nymtech.net/api/ NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" +EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API="https://nymvpn.com/api" diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 03e6fe60a7..056271e41d 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -16,7 +16,7 @@ rust-version.workspace = true anyhow = { workspace = true } axum = { workspace = true, features = ["tokio"] } chrono = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } cosmwasm-std = { workspace = true } envy = { workspace = true } futures-util = { workspace = true } @@ -53,5 +53,10 @@ utoipauto = { workspace = true } [build-dependencies] anyhow = { workspace = true } -tokio = { workspace = true, features = ["macros" ] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +tokio = { workspace = true, features = ["macros"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } diff --git a/nym-node-status-api/Dockerfile b/nym-node-status-api/Dockerfile new file mode 100644 index 0000000000..ceab7c9392 --- /dev/null +++ b/nym-node-status-api/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-api + +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh index e5059c5011..1db6bcc1b2 100755 --- a/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/launch_node_status_api.sh @@ -4,7 +4,15 @@ set -e export RUST_LOG=${RUST_LOG:-debug} -export NYM_API_CLIENT_TIMEOUT=60; -export EXPLORER_CLIENT_TIMEOUT=60; +export NYM_API_CLIENT_TIMEOUT=60 +export EXPLORER_CLIENT_TIMEOUT=60 +#export NYXD=https://rpc.nymtech.net +#export NYM_API=https://validator.nymtech.net/api/ +#export EXPLORER_API=https://explorer.nymtech.net/api/ +#export NETWORK_NAME=mainnet -cargo run --package nym-node-status-api --release -- --config-env-file ../envs/mainnet.env +#cargo run --package nym-node-status-api --release -- --connection-url "sqlite://node-status-api.sqlite?mode=rwc" + +cd .. +docker build -t node-status-api -f nym-node-status-api/Dockerfile . +docker run --env-file envs/mainnet.env -e NYM_NODE_STATUS_API_CONNECTION_URL="sqlite://node-status-api.sqlite?mode=rwc" node-status-api diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs index 7f46578863..ad214b6a23 100644 --- a/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/src/cli/mod.rs @@ -1,6 +1,7 @@ use clap::Parser; use nym_bin_common::bin_info; -use std::sync::OnceLock; +use reqwest::Url; +use std::{sync::OnceLock, time::Duration}; // Helper for passing LONG_VERSION to clap fn pretty_build_info_static() -> &'static str { @@ -8,10 +9,61 @@ fn pretty_build_info_static() -> &'static str { PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) } -#[derive(Parser, Debug)] +#[derive(Clone, Debug, Parser)] #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Cli { - /// Path pointing to an env file that configures the Nym API. - #[clap(short, long)] - pub(crate) config_env_file: Option, + /// Network name for the network to which we're connecting. + #[clap(long, env = "NETWORK_NAME")] + pub(crate) network_name: String, + + /// Explorer api url. + #[clap(short, long, env = "EXPLORER_API")] + pub(crate) explorer_api: String, + + /// Nym api url. + #[clap(short, long, env = "NYM_API")] + pub(crate) nym_api: String, + + /// TTL for the http cache. + #[clap( + long, + default_value_t = 30, + env = "NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL" + )] + pub(crate) nym_http_cache_ttl: u64, + + /// HTTP port on which to run node status api. + #[clap(long, default_value_t = 8000, env = "NYM_NODE_STATUS_API_HTTP_PORT")] + pub(crate) http_port: u16, + + /// Nyxd address. + #[clap(long, env = "NYXD")] + pub(crate) nyxd_addr: Url, + + /// Nym api client timeout. + #[clap( + long, + default_value = "15", + env = "NYM_NODE_STATUS_API_NYM_API_CLIENT_TIMEOUT" + )] + #[arg(value_parser = parse_duration)] + pub(crate) nym_api_client_timeout: Duration, + + /// Explorer api client timeout. + #[clap( + long, + default_value = "15", + env = "NYM_NODE_STATUS_API_EXPLORER_CLIENT_TIMEOUT" + )] + #[arg(value_parser = parse_duration)] + pub(crate) explorer_client_timeout: Duration, + + /// Connection url for the database. + #[clap(long, env = "NYM_NODE_STATUS_API_CONNECTION_URL")] + pub(crate) connection_url: String, +} + +fn parse_duration(arg: &str) -> Result { + let seconds = arg.parse()?; + Ok(std::time::Duration::from_secs(seconds)) } diff --git a/nym-node-status-api/src/config.rs b/nym-node-status-api/src/config.rs deleted file mode 100644 index c499ef3c65..0000000000 --- a/nym-node-status-api/src/config.rs +++ /dev/null @@ -1,72 +0,0 @@ -use anyhow::anyhow; -use reqwest::Url; -use serde::Deserialize; -use std::time::Duration; - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct Config { - #[serde(default = "Config::default_http_cache_seconds")] - nym_http_cache_ttl: u64, - #[serde(default = "Config::default_http_port")] - http_port: u16, - #[serde(rename = "nyxd")] - nyxd_addr: Url, - #[serde(default = "Config::default_client_timeout")] - #[serde(deserialize_with = "parse_duration")] - explorer_client_timeout: Duration, -} - -impl Config { - pub(crate) fn from_env() -> anyhow::Result { - envy::from_env::().map_err(|e| { - tracing::error!("Failed to load config from env: {e}"); - anyhow::Error::from(e) - }) - } - - fn default_client_timeout() -> Duration { - Duration::from_secs(15) - } - - fn default_http_port() -> u16 { - 8000 - } - - fn default_http_cache_seconds() -> u64 { - 30 - } - - pub(crate) fn nym_http_cache_ttl(&self) -> u64 { - self.nym_http_cache_ttl - } - - pub(crate) fn http_port(&self) -> u16 { - self.http_port - } - - pub(crate) fn nyxd_addr(&self) -> &Url { - &self.nyxd_addr - } - - pub(crate) fn nym_explorer_client_timeout(&self) -> Duration { - self.explorer_client_timeout.to_owned() - } -} - -fn parse_duration<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let secs: u64 = s.parse().map_err(serde::de::Error::custom)?; - Ok(Duration::from_secs(secs)) -} - -pub(super) fn read_env_var(env_var: &str) -> anyhow::Result { - std::env::var(env_var) - .map_err(|_| anyhow!("You need to set {}", env_var)) - .map(|value| { - tracing::trace!("{}={}", env_var, value); - value - }) -} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs index 2df2483687..8e840252f6 100644 --- a/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/src/db/mod.rs @@ -1,13 +1,11 @@ use std::str::FromStr; -use crate::read_env_var; use anyhow::{anyhow, Result}; use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; pub(crate) mod models; pub(crate) mod queries; -pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); pub(crate) type DbPool = SqlitePool; @@ -17,8 +15,7 @@ pub(crate) struct Storage { } impl Storage { - pub async fn init() -> Result { - let connection_url = read_env_var(DATABASE_URL_ENV_VAR)?; + pub async fn init(connection_url: String) -> Result { let connect_options = SqliteConnectOptions::from_str(&connection_url)? .create_if_missing(true) .disable_statement_logging(); diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs index 0fb1d0e1d2..a15cfe68a4 100644 --- a/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/src/main.rs @@ -1,11 +1,7 @@ use clap::Parser; -use nym_network_defaults::setup_env; use nym_task::signal::wait_for_signal; -use crate::config::read_env_var; - mod cli; -mod config; mod db; mod http; mod logging; @@ -16,33 +12,27 @@ async fn main() -> anyhow::Result<()> { logging::setup_tracing_logger(); let args = cli::Cli::parse(); - // if dotenv file is present, load its values - // otherwise, default to mainnet - setup_env(args.config_env_file.as_ref()); - tracing::debug!("{:?}", read_env_var("NETWORK_NAME")); - tracing::debug!("{:?}", read_env_var("EXPLORER_API")); - tracing::debug!("{:?}", read_env_var("NYM_API")); - let conf = config::Config::from_env()?; - tracing::debug!("Using config:\n{:#?}", conf); + let connection_url = args.connection_url.clone(); + tracing::debug!("Using config:\n{:#?}", args); - let storage = db::Storage::init().await?; + let storage = db::Storage::init(connection_url).await?; let db_pool = storage.pool_owned().await; - let conf_clone = conf.clone(); + let args_clone = args.clone(); tokio::spawn(async move { - monitor::spawn_in_background(db_pool, conf_clone).await; + monitor::spawn_in_background(db_pool, args_clone).await; }); tracing::info!("Started monitor task"); let shutdown_handles = http::server::start_http_api( storage.pool_owned().await, - conf.http_port(), - conf.nym_http_cache_ttl(), + args.http_port, + args.nym_http_cache_ttl, ) .await .expect("Failed to start server"); - tracing::info!("Started HTTP server on port {}", conf.http_port()); + tracing::info!("Started HTTP server on port {}", args.http_port); wait_for_signal().await; diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 5079af3b5a..3c5172c8a9 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::cli::Cli; use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, @@ -29,7 +29,7 @@ static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5 // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin -pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Config) -> JoinHandle<()> { +pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHandle<()> { let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); loop { @@ -54,7 +54,7 @@ pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Config) -> Join async fn run( pool: &DbPool, network_details: &NymNetworkDetails, - config: &Config, + config: &Cli, ) -> anyhow::Result<()> { let default_api_url = network_details .endpoints @@ -70,10 +70,8 @@ async fn run( let default_explorer_url = default_explorer_url.expect("explorer url missing in network config"); - let explorer_client = ExplorerClient::new_with_timeout( - default_explorer_url, - config.nym_explorer_client_timeout(), - )?; + let explorer_client = + ExplorerClient::new_with_timeout(default_explorer_url, config.explorer_client_timeout)?; let explorer_gateways = explorer_client .get_gateways() .await @@ -126,7 +124,7 @@ async fn run( .await .log_error("get_active_mixnodes")?; let delegation_program_members = - get_delegation_program_details(network_details, config.nyxd_addr()).await?; + get_delegation_program_details(network_details, &config.nyxd_addr).await?; // keep stats for later let count_bonded_mixnodes = mixnodes.len();