Add version check for web with short timeout (#3599)
This commit is contained in:
parent
e06cd111cf
commit
4471433a78
4 changed files with 144 additions and 22 deletions
|
@ -7,12 +7,16 @@ mod isready;
|
||||||
mod ml;
|
mod ml;
|
||||||
mod sql;
|
mod sql;
|
||||||
mod start;
|
mod start;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
mod upgrade;
|
mod upgrade;
|
||||||
mod validate;
|
mod validate;
|
||||||
pub(crate) mod validator;
|
pub(crate) mod validator;
|
||||||
mod version;
|
mod version;
|
||||||
|
mod version_client;
|
||||||
|
|
||||||
use crate::cnf::LOGO;
|
use crate::cli::version_client::VersionClient;
|
||||||
|
use crate::cnf::{LOGO, PKG_VERSION};
|
||||||
use crate::env::RELEASE;
|
use crate::env::RELEASE;
|
||||||
use backup::BackupCommandArguments;
|
use backup::BackupCommandArguments;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
@ -21,9 +25,12 @@ use export::ExportCommandArguments;
|
||||||
use import::ImportCommandArguments;
|
use import::ImportCommandArguments;
|
||||||
use isready::IsReadyCommandArguments;
|
use isready::IsReadyCommandArguments;
|
||||||
use ml::MlCommand;
|
use ml::MlCommand;
|
||||||
|
use semver::Version;
|
||||||
use sql::SqlCommandArguments;
|
use sql::SqlCommandArguments;
|
||||||
use start::StartCommandArguments;
|
use start::StartCommandArguments;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
use std::time::Duration;
|
||||||
use upgrade::UpgradeCommandArguments;
|
use upgrade::UpgradeCommandArguments;
|
||||||
use validate::ValidateCommandArguments;
|
use validate::ValidateCommandArguments;
|
||||||
use version::VersionCommandArguments;
|
use version::VersionCommandArguments;
|
||||||
|
@ -48,6 +55,10 @@ We would love it if you could star the repository (https://github.com/surrealdb/
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
|
#[arg(help = "Whether to allow web check for client version upgrades at start")]
|
||||||
|
#[arg(env = "SURREAL_ONLINE_VERSION_CHECK", long)]
|
||||||
|
#[arg(default_value_t = true)]
|
||||||
|
online_version_check: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
@ -88,7 +99,23 @@ pub async fn init() -> ExitCode {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Parse the CLI arguments
|
// Parse the CLI arguments
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
// Run the respective command
|
// After parsing arguments, we check the version online
|
||||||
|
if args.online_version_check {
|
||||||
|
let client = version_client::new(Some(Duration::from_millis(500))).unwrap();
|
||||||
|
if let Err(opt_version) = check_upgrade(&client, PKG_VERSION.deref()).await {
|
||||||
|
match opt_version {
|
||||||
|
None => {
|
||||||
|
warn!("A new version of SurrealDB may be available.");
|
||||||
|
}
|
||||||
|
Some(new_version) => {
|
||||||
|
warn!("A new version of SurrealDB is available: {}", new_version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO ansi_term crate?
|
||||||
|
warn!("You can upgrade using the {} command", "surreal upgrade");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// After version warning we can run the respective command
|
||||||
let output = match args.command {
|
let output = match args.command {
|
||||||
Commands::Start(args) => start::init(args).await,
|
Commands::Start(args) => start::init(args).await,
|
||||||
Commands::Backup(args) => backup::init(args).await,
|
Commands::Backup(args) => backup::init(args).await,
|
||||||
|
@ -125,3 +152,25 @@ pub async fn init() -> ExitCode {
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if there is a newer version
|
||||||
|
/// Ok = No upgrade needed
|
||||||
|
/// Err = Upgrade needed, returns the new version if it is available
|
||||||
|
async fn check_upgrade<C: VersionClient>(
|
||||||
|
client: &C,
|
||||||
|
pkg_version: &str,
|
||||||
|
) -> Result<(), Option<Version>> {
|
||||||
|
if let Ok(version) = client.fetch("latest").await {
|
||||||
|
// Request was successful, compare against current
|
||||||
|
let old_version = upgrade::parse_version(pkg_version).unwrap();
|
||||||
|
let new_version = upgrade::parse_version(&version).unwrap();
|
||||||
|
if old_version < new_version {
|
||||||
|
return Err(Some(new_version));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Request failed, check against date
|
||||||
|
// TODO: We don't have an "expiry" set per-version, so this is a todo
|
||||||
|
// It would return Err(None) if the version is too old
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
23
src/cli/test.rs
Normal file
23
src/cli/test.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use crate::cli::check_upgrade;
|
||||||
|
use crate::cli::version_client::MapVersionClient;
|
||||||
|
use crate::err::Error;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
pub async fn test_version_upgrade() {
|
||||||
|
let mut client = MapVersionClient {
|
||||||
|
fetch_mock: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
client
|
||||||
|
.fetch_mock
|
||||||
|
.insert("latest".to_string(), || -> Result<String, Error> { Ok("1.0.0".to_string()) });
|
||||||
|
check_upgrade(&client, "1.0.0")
|
||||||
|
.await
|
||||||
|
.expect("Expected the versions to be the same and not require an upgrade");
|
||||||
|
check_upgrade(&client, "0.9.0")
|
||||||
|
.await
|
||||||
|
.expect_err("Expected the versions to be different and require an upgrade");
|
||||||
|
check_upgrade(&client, "1.1.0")
|
||||||
|
.await
|
||||||
|
.expect("Expected the versions to be illogical, and not require and upgrade");
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::cli::version_client;
|
||||||
|
use crate::cli::version_client::VersionClient;
|
||||||
use crate::cnf::PKG_VERSION;
|
use crate::cnf::PKG_VERSION;
|
||||||
use crate::err::Error;
|
use crate::err::Error;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
@ -10,7 +12,10 @@ use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use surrealdb::env::{arch, os};
|
use surrealdb::env::{arch, os};
|
||||||
|
|
||||||
const ROOT: &str = "https://download.surrealdb.com";
|
pub(crate) const ROOT: &str = "https://download.surrealdb.com";
|
||||||
|
const BETA: &str = "beta";
|
||||||
|
const LATEST: &str = "latest";
|
||||||
|
const NIGHTLY: &str = "nightly";
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct UpgradeCommandArguments {
|
pub struct UpgradeCommandArguments {
|
||||||
|
@ -31,27 +36,26 @@ pub struct UpgradeCommandArguments {
|
||||||
impl UpgradeCommandArguments {
|
impl UpgradeCommandArguments {
|
||||||
/// Get the version string to download based on the user preference
|
/// Get the version string to download based on the user preference
|
||||||
async fn version(&self) -> Result<Cow<'_, str>, Error> {
|
async fn version(&self) -> Result<Cow<'_, str>, Error> {
|
||||||
let nightly = "nightly";
|
|
||||||
let beta = "beta";
|
|
||||||
// Convert the version to lowercase, if supplied
|
// Convert the version to lowercase, if supplied
|
||||||
let version = self.version.as_deref().map(str::to_ascii_lowercase);
|
let version = self.version.as_deref().map(str::to_ascii_lowercase);
|
||||||
|
let client = version_client::new(None)?;
|
||||||
|
|
||||||
if self.nightly || version.as_deref() == Some(nightly) {
|
if self.nightly || version.as_deref() == Some(NIGHTLY) {
|
||||||
Ok(Cow::Borrowed(nightly))
|
Ok(Cow::Borrowed(NIGHTLY))
|
||||||
} else if self.beta || version.as_deref() == Some(beta) {
|
} else if self.beta || version.as_deref() == Some(BETA) {
|
||||||
fetch(beta).await
|
client.fetch(BETA).await
|
||||||
} else if let Some(version) = version {
|
} else if let Some(version) = version {
|
||||||
// Parse the version string to make sure it's valid, return an error if not
|
// Parse the version string to make sure it's valid, return an error if not
|
||||||
let version = parse_version(&version)?;
|
let version = parse_version(&version)?;
|
||||||
// Return the version, ensuring it's prefixed by `v`
|
// Return the version, ensuring it's prefixed by `v`
|
||||||
Ok(Cow::Owned(format!("v{version}")))
|
Ok(Cow::Owned(format!("v{version}")))
|
||||||
} else {
|
} else {
|
||||||
fetch("latest").await
|
client.fetch(LATEST).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_version(input: &str) -> Result<Version, Error> {
|
pub(crate) fn parse_version(input: &str) -> Result<Version, Error> {
|
||||||
// Remove the `v` prefix, if supplied
|
// Remove the `v` prefix, if supplied
|
||||||
let version = input.strip_prefix('v').unwrap_or(input);
|
let version = input.strip_prefix('v').unwrap_or(input);
|
||||||
// Parse the version
|
// Parse the version
|
||||||
|
@ -76,17 +80,6 @@ fn parse_version(input: &str) -> Result<Version, Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch(version: &str) -> Result<Cow<'_, str>, Error> {
|
|
||||||
let response = reqwest::get(format!("{ROOT}/{version}.txt")).await?;
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(Error::Io(IoError::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
format!("received status {} when fetching version", response.status()),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(Cow::Owned(response.text().await?.trim().to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init(args: UpgradeCommandArguments) -> Result<(), Error> {
|
pub async fn init(args: UpgradeCommandArguments) -> Result<(), Error> {
|
||||||
// Initialize opentelemetry and logging
|
// Initialize opentelemetry and logging
|
||||||
crate::telemetry::builder().with_log_level("error").init();
|
crate::telemetry::builder().with_log_level("error").init();
|
||||||
|
|
57
src/cli/version_client.rs
Normal file
57
src/cli/version_client.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
// This is used in format! macro
|
||||||
|
use crate::cli::upgrade::ROOT;
|
||||||
|
use crate::err::Error;
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
#[cfg(test)]
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::io::Error as IoError;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub(crate) trait VersionClient {
|
||||||
|
async fn fetch(&self, version: &str) -> Result<Cow<'static, str>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ReqwestVersionClient {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new(timeout: Option<Duration>) -> Result<ReqwestVersionClient, Error> {
|
||||||
|
let mut client = Client::builder();
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
client = client.timeout(timeout);
|
||||||
|
}
|
||||||
|
let client = client.build()?;
|
||||||
|
Ok(ReqwestVersionClient {
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VersionClient for ReqwestVersionClient {
|
||||||
|
async fn fetch(&self, version: &str) -> Result<Cow<'static, str>, Error> {
|
||||||
|
let request = self.client.get(format!("{ROOT}/{version}.txt")).build().unwrap();
|
||||||
|
let response = self.client.execute(request).await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(Error::Io(IoError::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
format!("received status {} when fetching version", response.status()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Cow::Owned(response.text().await?.trim().to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) struct MapVersionClient {
|
||||||
|
pub(crate) fetch_mock: BTreeMap<String, fn() -> Result<String, Error>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl VersionClient for MapVersionClient {
|
||||||
|
async fn fetch(&self, version: &str) -> Result<Cow<'static, str>, Error> {
|
||||||
|
let found = self.fetch_mock.get(version).unwrap();
|
||||||
|
found().map(Cow::Owned)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue