initial run loop
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -115,6 +115,7 @@ pub(crate) struct ExecutingDaemon {
|
||||
interrupt_sent: bool,
|
||||
interrupt_handle: Option<Arc<Notify>>,
|
||||
|
||||
// TODO: can we maybe get rid of that dynamic dispatch here in favour of concrete types?
|
||||
// interrupted: Option<Pin<Box<Notified<'static>>>>,
|
||||
interrupted: Pin<Box<dyn Future<Output = ()> + Send + Sync>>,
|
||||
kill_timeout: Option<Pin<Box<Sleep>>>,
|
||||
|
||||
@@ -1,37 +1,2 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<T>
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
abort_handle: AbortHandle,
|
||||
// join_handle: JoinHandle<F::Output>,
|
||||
join_handle: JoinHandle<Result<T, Aborted>>,
|
||||
}
|
||||
|
||||
impl<T> TaskHandle<T>
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
abort_handle: AbortHandle,
|
||||
join_handle: JoinHandle<Result<T, Aborted>>,
|
||||
) -> Self {
|
||||
TaskHandle {
|
||||
abort_handle,
|
||||
join_handle,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: change return type
|
||||
pub(crate) async fn abort_and_finalize(self) -> Result<Result<T, Aborted>, JoinError> {
|
||||
self.abort_handle.abort();
|
||||
self.join_handle.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>) -> 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<String>) -> Result<bool, NymvisorError> {
|
||||
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<Duration> {
|
||||
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<String>,
|
||||
@@ -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/<DAEMON_NAME>/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!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NotifyResult<()>>), NymvisorError> {
|
||||
) -> Result<(FileWatcherEventReceiver, JoinHandle<NotifyResult<()>>), 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))
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
@@ -1,251 +1,20 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<PathBuf>,
|
||||
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<UpgradeInfo>,
|
||||
}
|
||||
|
||||
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
|
||||
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<u8>,
|
||||
|
||||
/// 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<OffsetDateTime>,
|
||||
|
||||
/// Version of this upgrade, for example `1.1.69`
|
||||
pub version: String,
|
||||
|
||||
/// Platform specific download urls, for example `linux-x86_64`
|
||||
pub platforms: HashMap<String, DownloadUrl>,
|
||||
|
||||
/// 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<BinaryBuildInformationOwned>,
|
||||
}
|
||||
|
||||
impl UpgradeInfo {
|
||||
pub(crate) fn save<P: AsRef<Path>>(&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<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
|
||||
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<UpgradeHistoryEntry>);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UpgradeHistoryEntry {
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
performed_at: OffsetDateTime,
|
||||
info: UpgradeInfo,
|
||||
|
||||
*/
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<PathBuf>,
|
||||
|
||||
current: UpgradeInfo,
|
||||
|
||||
// TODO: or maybe BTreeMap<OffsetDateTime, UpgradeInfo>, would be more appropriate?
|
||||
queued_up: VecDeque<UpgradeInfo>,
|
||||
}
|
||||
|
||||
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
|
||||
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<u8>,
|
||||
|
||||
/// 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<OffsetDateTime>,
|
||||
|
||||
/// Version of this upgrade, for example `1.1.69`
|
||||
pub version: String,
|
||||
|
||||
/// Platform specific download urls, for example `linux-x86_64`
|
||||
pub platforms: HashMap<String, DownloadUrl>,
|
||||
|
||||
/// 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<BinaryBuildInformationOwned>,
|
||||
}
|
||||
|
||||
impl UpgradeInfo {
|
||||
pub(crate) fn save<P: AsRef<Path>>(&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<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
|
||||
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<UpgradeHistoryEntry>);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UpgradeHistoryEntry {
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
performed_at: OffsetDateTime,
|
||||
info: UpgradeInfo,
|
||||
}
|
||||
Reference in New Issue
Block a user