diff --git a/tools/nymvisor/src/cli/init.rs b/tools/nymvisor/src/cli/init.rs index 80789bc000..03118a58f8 100644 --- a/tools/nymvisor/src/cli/init.rs +++ b/tools/nymvisor/src/cli/init.rs @@ -5,7 +5,7 @@ use crate::config::{default_config_filepath, Config, BIN_DIR, GENESIS_DIR}; use crate::daemon::Daemon; use crate::env::Env; use crate::error::NymvisorError; -use crate::upgrades::{UpgradeInfo, UpgradePlan}; +use crate::upgrades::types::{UpgradeInfo, UpgradePlan}; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_bin_common::logging::{setup_logging, setup_tracing_logger}; use nym_bin_common::output_format::OutputFormat; diff --git a/tools/nymvisor/src/cli/run.rs b/tools/nymvisor/src/cli/run.rs index 6b86701e99..cb23e2c2de 100644 --- a/tools/nymvisor/src/cli/run.rs +++ b/tools/nymvisor/src/cli/run.rs @@ -45,7 +45,7 @@ pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { // - one for managing the daemon launcher // - the other one for watching the upgrade plan file // - the last one for polling upstream source for upgrade info - // so once the daemon has finished, for whatever reason, abort the file watcher and upstream poller and terminate the nymvisor + // so once the daemon has finished, for whatever reason, abort the file watcher and upstream poller to terminate the nymvisor todo!() // // spawn the root task diff --git a/tools/nymvisor/src/daemon/mod.rs b/tools/nymvisor/src/daemon/mod.rs index 82a75b54c0..8dff09d0a1 100644 --- a/tools/nymvisor/src/daemon/mod.rs +++ b/tools/nymvisor/src/daemon/mod.rs @@ -115,6 +115,7 @@ pub(crate) struct ExecutingDaemon { interrupt_sent: bool, interrupt_handle: Option>, + // TODO: can we maybe get rid of that dynamic dispatch here in favour of concrete types? // interrupted: Option>>>, interrupted: Pin + Send + Sync>>, kill_timeout: Option>>, diff --git a/tools/nymvisor/src/helpers.rs b/tools/nymvisor/src/helpers.rs index 861e680706..38dc36ebc3 100644 --- a/tools/nymvisor/src/helpers.rs +++ b/tools/nymvisor/src/helpers.rs @@ -1,37 +1,2 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use futures::future::Aborted; -use futures::stream::AbortHandle; -use std::future::Future; -use tokio::task::{JoinError, JoinHandle}; - -pub(crate) struct TaskHandle -where - T: Send + 'static, -{ - abort_handle: AbortHandle, - // join_handle: JoinHandle, - join_handle: JoinHandle>, -} - -impl TaskHandle -where - T: Send + 'static, -{ - pub(crate) fn new( - abort_handle: AbortHandle, - join_handle: JoinHandle>, - ) -> Self { - TaskHandle { - abort_handle, - join_handle, - } - } - - // TODO: change return type - pub(crate) async fn abort_and_finalize(self) -> Result, JoinError> { - self.abort_handle.abort(); - self.join_handle.await - } -} diff --git a/tools/nymvisor/src/tasks/launcher/mod.rs b/tools/nymvisor/src/tasks/launcher/mod.rs index 0ed501f3b9..844a6f5079 100644 --- a/tools/nymvisor/src/tasks/launcher/mod.rs +++ b/tools/nymvisor/src/tasks/launcher/mod.rs @@ -4,16 +4,21 @@ use crate::config::Config; use crate::daemon::Daemon; use crate::error::NymvisorError; -use crate::upgrades::{UpgradeInfo, UpgradePlan}; +use crate::upgrades::{ + types::{UpgradeInfo, UpgradePlan}, + upgrade_binary, +}; use async_file_watcher::FileWatcherEventReceiver; use futures::future::{FusedFuture, OptionFuture}; use futures::{FutureExt, StreamExt}; use nym_task::signal::wait_for_signal; use std::sync::Arc; use std::time::Duration; +use time::OffsetDateTime; +use tokio::pin; use tokio::sync::Notify; -use tokio::time::Sleep; -use tracing::{error, info, warn}; +use tokio::time::{sleep, Sleep}; +use tracing::{debug, error, info, warn}; pub(crate) struct DaemonLauncher { config: Config, @@ -25,15 +30,43 @@ impl DaemonLauncher { todo!() } - // responsible for running until exit or until update is detected - pub(crate) async fn run(&mut self, args: Vec) -> Result<(), NymvisorError> { + // the full upgrade process process, i.e. run until upgrade, do backup and perform the upgrade. + // returns a boolean indicating whether the process should get restarted + pub(crate) async fn run(&mut self, args: Vec) -> Result { + let upgrade_available = self.wait_for_upgrade_or_termination(args.clone()).await?; + if !upgrade_available { + return Ok(false); + } + + self.perform_backup()?; + upgrade_binary()?; + todo!() } - fn check_upgrade_plan_changes(&self) { - // + /// this function gets called whenever the file watcher detects changes in the upgrade plan file + /// it returns an option indicating when the next upgrade should be performed + fn check_upgrade_plan_changes(&self) -> Option { + info!("checking changes in the upgrade plan file..."); + + let current_upgrade_plan = match UpgradePlan::try_load(self.config.upgrade_plan_filepath()) + { + Ok(upgrade_plan) => upgrade_plan, + Err(err) => { + error!("failed to read the current upgrade plan: {err}"); + return None; + } + }; + + if let Some(next) = current_upgrade_plan.next_upgrade() { + let now = OffsetDateTime::now_utc(); + Some((next.upgrade_time - now).try_into().unwrap_or_default()) + } else { + None + } } + // responsible for running until exit or until update is detected async fn wait_for_upgrade_or_termination( &mut self, args: Vec, @@ -45,6 +78,8 @@ impl DaemonLauncher { let current_upgrade_plan = UpgradePlan::try_load(self.config.upgrade_plan_filepath())?; let next = current_upgrade_plan.next_upgrade(); + // TODO: /\ + let mut running_daemon = daemon.execute_async(args)?; let interrupt_handle = running_daemon.interrupt_handle(); @@ -53,12 +88,10 @@ impl DaemonLauncher { let mut upgrade_timeout: OptionFuture<_> = None.into(); - upgrade_timeout = - Some(Box::pin(tokio::time::sleep(Duration::from_secs(123))).fuse()).into(); + let signal_fut = wait_for_signal(); + pin!(signal_fut); - let sig_fut = wait_for_signal(); - - // note: this has to be in a loop because `upgrade_plan_watcher` might receive events that do not necessarily trigger the upgrade + let mut received_interrupt = false; loop { tokio::select! { daemon_res = &mut fused_runner => { @@ -67,43 +100,52 @@ impl DaemonLauncher { info!("it finished with the following exit status: {exit_status}"); return Ok(false) } - _ = &mut self.upgrade_plan_watcher.next() => { - // + event = &mut self.upgrade_plan_watcher.next() => { + let Some(event) = event else { + // this is a critical failure since the file watcher task should NEVER terminate by itself + error!("CRITICAL FAILURE: the upgrade plan watcher channel got closed"); + panic!("CRITICAL FAILURE: the upgrade plan watcher channel got closed") + }; + println!("the file has changed - {event:?}"); + + debug!("the file has changed - {event:?}"); + if let Some(next_upgrade) = self.check_upgrade_plan_changes() { + info!("setting the upgrade timeout to {}", humantime::format_duration(next_upgrade)); + upgrade_timeout = Some(Box::pin(sleep(next_upgrade)).fuse()).into() + } + } _ = &mut upgrade_timeout, if !upgrade_timeout.is_terminated() => { + info!("the upgrade timeout has elapsed. the daemon will be now stopped in order to perform the upgrade"); + break + } + _ = &mut signal_fut => { + received_interrupt = true; + info!("the nymvisor has received an interrupt. the daemon will be now stopped before exiting"); break } - - - } } - // if the runner hasn't terminated by itself (which should be the almost every single time!) - // send the interrupt and wait for it to be done if fused_runner.is_terminated() { - todo!("error case") + return Ok(false); } interrupt_handle.interrupt_daemon(); - let res = fused_runner.await; - /* + match fused_runner.await { + Ok(exit_status) => { + info!("the daemon finished with the following exit status: {exit_status}"); + } + Err(err) => { + warn!("the daemon finished with an error: {err}"); + } + } - tokio select on: - - daemon terminating - - upgrade-plan.json changes - - https://nymtech.net/.wellknown//update-info.json changes - - - // todo: maybe move to a higher layer - - signals received (to propagate them to daemon before terminating to prevent creating zombie processes) - - */ - - todo!() + // if we received an interrupt, don't try to perform upgrade, just exit the nymvisor + Ok(!received_interrupt) } - async fn perform_backup(&self) { + fn perform_backup(&self) -> Result<(), NymvisorError> { todo!() } } diff --git a/tools/nymvisor/src/tasks/upgrade_plan_watcher.rs b/tools/nymvisor/src/tasks/upgrade_plan_watcher.rs index 8c9ecce509..7052b1f493 100644 --- a/tools/nymvisor/src/tasks/upgrade_plan_watcher.rs +++ b/tools/nymvisor/src/tasks/upgrade_plan_watcher.rs @@ -3,23 +3,18 @@ use crate::config::Config; use crate::error::NymvisorError; -use crate::helpers::TaskHandle; use async_file_watcher::{AsyncFileWatcher, FileWatcherEventReceiver, NotifyResult}; use futures::channel::mpsc; -use futures::future::{AbortHandle, Abortable}; +use tokio::task::JoinHandle; pub(crate) fn start_upgrade_plan_watcher( config: &Config, -) -> Result<(FileWatcherEventReceiver, TaskHandle>), NymvisorError> { +) -> Result<(FileWatcherEventReceiver, JoinHandle>), NymvisorError> { let (events_sender, events_receiver) = mpsc::unbounded(); let mut watcher = AsyncFileWatcher::new_file_changes_watcher(config.upgrade_plan_filepath(), events_sender)?; - let (abort_handle, abort_registration) = AbortHandle::new_pair(); - let join_handle = - tokio::spawn(async move { Abortable::new(watcher.watch(), abort_registration).await }); + let join_handle = tokio::spawn(async move { watcher.watch().await }); - let task_handle = TaskHandle::new(abort_handle, join_handle); - - Ok((events_receiver, task_handle)) + Ok((events_receiver, join_handle)) } diff --git a/tools/nymvisor/src/tasks/upstream_poller.rs b/tools/nymvisor/src/tasks/upstream_poller.rs index 4708c9571e..41fe1ee7ed 100644 --- a/tools/nymvisor/src/tasks/upstream_poller.rs +++ b/tools/nymvisor/src/tasks/upstream_poller.rs @@ -3,10 +3,9 @@ use crate::config::Config; use crate::error::NymvisorError; -use crate::helpers::TaskHandle; -use crate::upgrades::{UpgradeInfo, UpgradePlan}; -use futures::future::{AbortHandle, Abortable}; +use crate::upgrades::types::{UpgradeInfo, UpgradePlan}; use reqwest::get; +use tokio::task::JoinHandle; use tracing::{error, warn}; pub(crate) struct UpstreamPoller { @@ -71,11 +70,7 @@ impl UpstreamPoller { } } - pub(crate) async fn start(mut self) -> TaskHandle<()> { - let (abort_handle, abort_registration) = AbortHandle::new_pair(); - let join_handle = - tokio::spawn(async move { Abortable::new(self.run(), abort_registration).await }); - - TaskHandle::new(abort_handle, join_handle) + pub(crate) async fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) } } diff --git a/tools/nymvisor/src/upgrades/http_upstream.rs b/tools/nymvisor/src/upgrades/http_upstream.rs deleted file mode 100644 index 38dc36ebc3..0000000000 --- a/tools/nymvisor/src/upgrades/http_upstream.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 diff --git a/tools/nymvisor/src/upgrades/mod.rs b/tools/nymvisor/src/upgrades/mod.rs index 52fa52f836..8e21ac2e14 100644 --- a/tools/nymvisor/src/upgrades/mod.rs +++ b/tools/nymvisor/src/upgrades/mod.rs @@ -1,251 +1,20 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::GENESIS_DIR; use crate::error::NymvisorError; -use nym_bin_common::build_information::BinaryBuildInformationOwned; -use serde::{Deserialize, Serialize}; -use serde_helpers::{base64, option_offsetdatetime}; -use std::collections::{HashMap, VecDeque}; -use std::fs::OpenOptions; -use std::path::{Path, PathBuf}; -use std::{fs, io}; -use time::OffsetDateTime; -use tracing::error; -use url::Url; -mod http_upstream; mod serde_helpers; +pub(crate) mod types; -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "lowercase")] -pub struct UpgradePlan { - // metadata indicating save location of the underlying file - #[serde(skip)] - _save_path: Option, +pub(crate) fn upgrade_binary() -> Result<(), NymvisorError> { + // lock - current: UpgradeInfo, + /* + if binary already exist => swap symlink, write history and we're done + otherwise deal with download - next: VecDeque, -} - -impl UpgradePlan { - pub(crate) fn new(current: UpgradeInfo) -> Self { - UpgradePlan { - _save_path: None, - current, - next: VecDeque::new(), - } - } - - fn push_next_upgrade(&mut self, upgrade: UpgradeInfo) { - self.next.push_back(upgrade); - } - - fn update_on_disk(&self) -> Result<(), NymvisorError> { - // it should be impossible to update an existing upgrade plan that wasn't loaded from disk - assert!(self._save_path.is_some()); - - // safety: the except here is fine as this failure implies failure in the underlying logic of the code - // as opposed to user error - #[allow(clippy::expect_used)] - let save_path = self - ._save_path - .as_ref() - .expect("loaded upgrade plan does not have an associate save path!"); - - let file = OpenOptions::new() - .write(true) - .truncate(true) - .open(save_path) - .map_err(|source| NymvisorError::UpgradePlanSaveFailure { - path: save_path.to_path_buf(), - source, - })?; - - // we're not using any non-standard serializer and thus the serialization should not ever fail - #[allow(clippy::expect_used)] - serde_json::to_writer_pretty(file, self) - .expect("unexpected UpgradeInfo serialization failure"); - Ok(()) - } - - pub(crate) fn insert_new_upgrade(&mut self, upgrade: UpgradeInfo) -> Result<(), NymvisorError> { - self.push_next_upgrade(upgrade); - self.update_on_disk() - } - - pub(crate) fn current(&self) -> &UpgradeInfo { - &self.current - } - - pub(crate) fn next_upgrade(&self) -> Option<&UpgradeInfo> { - self.next.front() - } - - pub(crate) fn has_planned(&self, upgrade: &UpgradeInfo) -> bool { - for planned in &self.next { - if planned.version == upgrade.version { - if planned.name != upgrade.name { - // TODO: should we maybe return a hard error here instead? - error!("we have already a planned upgrade for version {} under name '{}' which differs from provided '{}'", planned.version, planned.name, upgrade.name); - } - return true; - } - } - false - } - - // pub(crate) fn update_current(&mut self) -> Result<(), NymvisorError> { - // - // } - - pub(crate) fn save_new>(&self, path: P) -> Result<(), NymvisorError> { - debug_assert!(self._save_path.is_none()); - - let path = path.as_ref(); - let file = OpenOptions::new() - .create_new(true) - .write(true) - .open(path) - .map_err(|source| NymvisorError::UpgradePlanSaveFailure { - path: path.to_path_buf(), - source, - })?; - - // we're not using any non-standard serializer and thus the serialization should not ever fail - #[allow(clippy::expect_used)] - serde_json::to_writer_pretty(file, self) - .expect("unexpected UpgradeInfo serialization failure"); - Ok(()) - } - - pub(crate) fn try_load>(path: P) -> Result { - let path = path.as_ref(); - let mut upgrade_plan: UpgradePlan = fs::File::open(path) - .and_then(|file| { - serde_json::from_reader(file) - .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) - }) - .map_err(|source| NymvisorError::UpgradePlanLoadFailure { - path: path.to_path_buf(), - source, - })?; - - upgrade_plan._save_path = Some(path.to_path_buf()); - Ok(upgrade_plan) - } -} - -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] -#[serde(rename_all = "lowercase")] -pub enum DigestAlgorithm { - Sha256, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "lowercase")] -pub struct DownloadUrl { - /// The checksum of the file behind the download url. - #[serde(with = "base64")] - pub checksum: Vec, - - /// The algorithm used for computing the checksum - pub checksum_algorithm: DigestAlgorithm, - - /// Download url for this particular platform - pub url: Url, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "lowercase")] -pub struct UpgradeInfo { - /// Specifies whether this upgrade requires manual intervention and cannot be done automatically by the nymvisor. - pub manual: bool, - - /// Name of this upgrade, for example `2023.4-galaxy` - pub name: String, - - /// Additional information about this release - pub notes: String, - - /// Optional rfc3339 datetime of the publish date of the release, - #[serde(with = "option_offsetdatetime")] - pub publish_date: Option, - - /// Version of this upgrade, for example `1.1.69` - pub version: String, - - /// Platform specific download urls, for example `linux-x86_64` - pub platforms: HashMap, - - /// Time when the upgrade should happen. - #[serde(with = "time::serde::rfc3339")] - pub upgrade_time: OffsetDateTime, - - /// Optional build information of the upgraded binary for additional verification - pub binary_details: Option, -} - -impl UpgradeInfo { - pub(crate) fn save>(&self, path: P) -> Result<(), NymvisorError> { - let path = path.as_ref(); - - // in case we're saving brand new upgrade info, make sure the parent directory exists - #[allow(clippy::expect_used)] - let parent = path - .parent() - .expect("attempted to save the upgrade info as the root of the fs"); - - fs::create_dir_all(parent).map_err(|source| NymvisorError::PathInitFailure { - path: parent.to_path_buf(), - source, - })?; - - let file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path) - .map_err(|source| NymvisorError::UpgradeInfoSaveFailure { - name: self.name.clone(), - path: path.to_path_buf(), - source, - })?; - - // we're not using any non-standard serializer and thus the serialization should not ever fail - #[allow(clippy::expect_used)] - serde_json::to_writer_pretty(file, self) - .expect("unexpected UpgradeInfo serialization failure"); - Ok(()) - } - - pub(crate) fn try_load>(path: P) -> Result { - let path = path.as_ref(); - std::fs::File::open(path) - .and_then(|file| { - serde_json::from_reader(file) - .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) - }) - .map_err(|source| NymvisorError::UpgradeInfoLoadFailure { - path: path.to_path_buf(), - source, - }) - } - - pub(crate) fn is_genesis(&self) -> bool { - self.name == GENESIS_DIR - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -pub struct UpgradeHistory(Vec); - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -pub struct UpgradeHistoryEntry { - #[serde(with = "time::serde::rfc3339")] - performed_at: OffsetDateTime, - info: UpgradeInfo, + + */ + + todo!() } diff --git a/tools/nymvisor/src/upgrades/types.rs b/tools/nymvisor/src/upgrades/types.rs new file mode 100644 index 0000000000..4fa5b97ce5 --- /dev/null +++ b/tools/nymvisor/src/upgrades/types.rs @@ -0,0 +1,249 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::serde_helpers::{base64, option_offsetdatetime}; +use crate::config::GENESIS_DIR; +use crate::error::NymvisorError; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, VecDeque}; +use std::fs::OpenOptions; +use std::path::{Path, PathBuf}; +use std::{fs, io}; +use time::OffsetDateTime; +use tracing::error; +use url::Url; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub struct UpgradePlan { + // metadata indicating save location of the underlying file + #[serde(skip)] + _save_path: Option, + + current: UpgradeInfo, + + // TODO: or maybe BTreeMap, would be more appropriate? + queued_up: VecDeque, +} + +impl UpgradePlan { + pub(crate) fn new(current: UpgradeInfo) -> Self { + UpgradePlan { + _save_path: None, + current, + queued_up: VecDeque::new(), + } + } + + fn push_next_upgrade(&mut self, upgrade: UpgradeInfo) { + self.queued_up.push_back(upgrade); + } + + fn update_on_disk(&self) -> Result<(), NymvisorError> { + // it should be impossible to update an existing upgrade plan that wasn't loaded from disk + assert!(self._save_path.is_some()); + + // safety: the except here is fine as this failure implies failure in the underlying logic of the code + // as opposed to user error + #[allow(clippy::expect_used)] + let save_path = self + ._save_path + .as_ref() + .expect("loaded upgrade plan does not have an associate save path!"); + + let file = OpenOptions::new() + .write(true) + .truncate(true) + .open(save_path) + .map_err(|source| NymvisorError::UpgradePlanSaveFailure { + path: save_path.to_path_buf(), + source, + })?; + + // we're not using any non-standard serializer and thus the serialization should not ever fail + #[allow(clippy::expect_used)] + serde_json::to_writer_pretty(file, self) + .expect("unexpected UpgradeInfo serialization failure"); + Ok(()) + } + + pub(crate) fn insert_new_upgrade(&mut self, upgrade: UpgradeInfo) -> Result<(), NymvisorError> { + self.push_next_upgrade(upgrade); + self.update_on_disk() + } + + pub(crate) fn current(&self) -> &UpgradeInfo { + &self.current + } + + pub(crate) fn next_upgrade(&self) -> Option<&UpgradeInfo> { + self.queued_up.front() + } + + pub(crate) fn has_planned(&self, upgrade: &UpgradeInfo) -> bool { + for planned in &self.queued_up { + if planned.version == upgrade.version { + if planned.name != upgrade.name { + // TODO: should we maybe return a hard error here instead? + error!("we have already a planned upgrade for version {} under name '{}' which differs from provided '{}'", planned.version, planned.name, upgrade.name); + } + return true; + } + } + false + } + + // pub(crate) fn update_current(&mut self) -> Result<(), NymvisorError> { + // + // } + + pub(crate) fn save_new>(&self, path: P) -> Result<(), NymvisorError> { + debug_assert!(self._save_path.is_none()); + + let path = path.as_ref(); + let file = OpenOptions::new() + .create_new(true) + .write(true) + .open(path) + .map_err(|source| NymvisorError::UpgradePlanSaveFailure { + path: path.to_path_buf(), + source, + })?; + + // we're not using any non-standard serializer and thus the serialization should not ever fail + #[allow(clippy::expect_used)] + serde_json::to_writer_pretty(file, self) + .expect("unexpected UpgradeInfo serialization failure"); + Ok(()) + } + + pub(crate) fn try_load>(path: P) -> Result { + let path = path.as_ref(); + let mut upgrade_plan: UpgradePlan = fs::File::open(path) + .and_then(|file| { + serde_json::from_reader(file) + .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) + }) + .map_err(|source| NymvisorError::UpgradePlanLoadFailure { + path: path.to_path_buf(), + source, + })?; + + upgrade_plan._save_path = Some(path.to_path_buf()); + Ok(upgrade_plan) + } +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +#[serde(rename_all = "lowercase")] +pub enum DigestAlgorithm { + Sha256, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub struct DownloadUrl { + /// The checksum of the file behind the download url. + #[serde(with = "base64")] + pub checksum: Vec, + + /// The algorithm used for computing the checksum + pub checksum_algorithm: DigestAlgorithm, + + /// Download url for this particular platform + pub url: Url, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub struct UpgradeInfo { + /// Specifies whether this upgrade requires manual intervention and cannot be done automatically by the nymvisor. + pub manual: bool, + + /// Name of this upgrade, for example `2023.4-galaxy` + pub name: String, + + /// Additional information about this release + pub notes: String, + + /// Optional rfc3339 datetime of the publish date of the release, + #[serde(with = "option_offsetdatetime")] + pub publish_date: Option, + + /// Version of this upgrade, for example `1.1.69` + pub version: String, + + /// Platform specific download urls, for example `linux-x86_64` + pub platforms: HashMap, + + /// Time when the upgrade should happen. + #[serde(with = "time::serde::rfc3339")] + pub upgrade_time: OffsetDateTime, + + /// Optional build information of the upgraded binary for additional verification + pub binary_details: Option, +} + +impl UpgradeInfo { + pub(crate) fn save>(&self, path: P) -> Result<(), NymvisorError> { + let path = path.as_ref(); + + // in case we're saving brand new upgrade info, make sure the parent directory exists + #[allow(clippy::expect_used)] + let parent = path + .parent() + .expect("attempted to save the upgrade info as the root of the fs"); + + fs::create_dir_all(parent).map_err(|source| NymvisorError::PathInitFailure { + path: parent.to_path_buf(), + source, + })?; + + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .map_err(|source| NymvisorError::UpgradeInfoSaveFailure { + name: self.name.clone(), + path: path.to_path_buf(), + source, + })?; + + // we're not using any non-standard serializer and thus the serialization should not ever fail + #[allow(clippy::expect_used)] + serde_json::to_writer_pretty(file, self) + .expect("unexpected UpgradeInfo serialization failure"); + Ok(()) + } + + pub(crate) fn try_load>(path: P) -> Result { + let path = path.as_ref(); + std::fs::File::open(path) + .and_then(|file| { + serde_json::from_reader(file) + .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) + }) + .map_err(|source| NymvisorError::UpgradeInfoLoadFailure { + path: path.to_path_buf(), + source, + }) + } + + pub(crate) fn is_genesis(&self) -> bool { + self.name == GENESIS_DIR + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub struct UpgradeHistory(Vec); + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub struct UpgradeHistoryEntry { + #[serde(with = "time::serde::rfc3339")] + performed_at: OffsetDateTime, + info: UpgradeInfo, +}