Feature #1357 - add upgrade CLI. (#2102)

This commit is contained in:
Finn Bear 2023-06-08 13:57:02 -07:00 committed by GitHub
parent f000e9232f
commit 2237afb21a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 208 additions and 3 deletions

15
Cargo.lock generated
View file

@ -1226,7 +1226,7 @@ dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"memoffset 0.8.0",
"scopeguard",
]
@ -2536,6 +2536,15 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.8.0"
@ -2675,6 +2684,8 @@ dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset 0.7.1",
"pin-utils",
"static_assertions",
]
@ -4277,6 +4288,7 @@ dependencies = [
"ipnet",
"jsonwebtoken",
"log",
"nix",
"once_cell",
"opentelemetry",
"opentelemetry-otlp",
@ -4291,6 +4303,7 @@ dependencies = [
"serde_json",
"surrealdb",
"temp-env",
"tempfile",
"thiserror",
"tokio",
"tokio-stream",

View file

@ -54,6 +54,7 @@ serde_cbor = "0.11.2"
serde_pack = { version = "1.1.1", package = "rmp-serde" }
serde_json = "1.0.96"
surrealdb = { path = "lib", features = ["protocol-http", "protocol-ws", "rustls"] }
tempfile = "3.5.0"
thiserror = "1.0.40"
tokio = { version = "1.28.1", features = ["macros", "signal"] }
tokio-util = { version = "0.7.8", features = ["io"] }
@ -65,6 +66,9 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
urlencoding = "2.1.2"
warp = { version = "0.3.5", features = ["compression", "tls", "websocket"] }
[target.'cfg(unix)'.dependencies]
nix = "0.26.2"
[dev-dependencies]
rcgen = "0.10.0"
tonic = "0.8.3"

View file

@ -6,14 +6,15 @@ mod import;
mod isready;
mod sql;
mod start;
mod upgrade;
pub(crate) mod validator;
mod version;
pub use config::CF;
use self::upgrade::UpgradeCommandArguments;
use crate::cnf::LOGO;
use backup::BackupCommandArguments;
use clap::{Parser, Subcommand};
pub use config::CF;
use export::ExportCommandArguments;
use import::ImportCommandArguments;
use isready::IsReadyCommandArguments;
@ -57,6 +58,8 @@ enum Commands {
Export(ExportCommandArguments),
#[command(about = "Output the command-line tool version information")]
Version,
#[command(about = "Upgrade to the latest stable version")]
Upgrade(UpgradeCommandArguments),
#[command(about = "Start an SQL REPL in your terminal with pipe support")]
Sql(SqlCommandArguments),
#[command(
@ -74,6 +77,7 @@ pub async fn init() -> ExitCode {
Commands::Import(args) => import::init(args).await,
Commands::Export(args) => export::init(args).await,
Commands::Version => version::init(),
Commands::Upgrade(args) => upgrade::init(args).await,
Commands::Sql(args) => sql::init(args).await,
Commands::IsReady(args) => isready::init(args).await,
};

184
src/cli/upgrade.rs Normal file
View file

@ -0,0 +1,184 @@
use crate::cnf::PKG_VERSION;
use crate::err::Error;
use clap::Args;
use std::borrow::Cow;
use std::fs;
use std::io::{Error as IoError, ErrorKind};
use std::ops::Deref;
use std::path::Path;
use std::process::Command;
use surrealdb::env::{arch, os};
const LATEST_STABLE_VERSION: &str = "https://version.surrealdb.com/";
const ROOT: &str = "https://download.surrealdb.com";
#[derive(Args, Debug)]
pub struct UpgradeCommandArguments {
/// Install the latest nightly version
#[arg(long, conflicts_with = "version")]
nightly: bool,
/// Install a specific version
#[arg(long, conflicts_with = "nightly")]
version: Option<String>,
/// Don't actually replace the executable
#[arg(long)]
dry_run: bool,
}
impl UpgradeCommandArguments {
/// Get the version string to download based on the user preference
async fn version(&self) -> Result<Cow<'_, str>, Error> {
Ok(if self.nightly {
Cow::Borrowed("nightly")
} else if let Some(version) = self.version.as_ref() {
Cow::Borrowed(version)
} else {
let response = reqwest::get(LATEST_STABLE_VERSION).await?;
if !response.status().is_success() {
return Err(Error::Io(IoError::new(
ErrorKind::Other,
format!("received status {} when fetching version", response.status()),
)));
}
Cow::Owned(response.text().await?.trim().to_owned())
})
}
}
pub async fn init(args: UpgradeCommandArguments) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level("error").init();
// Upgrading overwrites the existing executable
let exe = std::env::current_exe()?;
// Check if we have write permissions
let metadata = fs::metadata(&exe)?;
let permissions = metadata.permissions();
if permissions.readonly() {
return Err(Error::Io(IoError::new(
ErrorKind::PermissionDenied,
"executable is read-only",
)));
}
#[cfg(unix)]
if std::os::unix::fs::MetadataExt::uid(&metadata) == 0
&& !nix::unistd::Uid::effective().is_root()
{
return Err(Error::Io(IoError::new(
ErrorKind::PermissionDenied,
"executable is owned by root; try again with sudo",
)));
}
// Compare old and new versions
let old_version = PKG_VERSION.deref().clone();
let new_version = args.version().await?;
if old_version == new_version {
println!("{old_version} is already installed");
return Ok(());
}
let arch = arch();
let os = os();
println!("current version is {old_version} for {os} on {arch}",);
let download_arch = match arch {
"aarch64" => "arm64",
"x86_64" => "amd64",
_ => {
return Err(Error::Io(IoError::new(
ErrorKind::Unsupported,
format!("unsupported arch {arch}"),
)));
}
};
let (download_os, download_ext) = match os {
"linux" => ("linux", "tgz"),
"macos" => ("darwin", "tgz"),
"windows" => ("windows", "exe"),
_ => {
return Err(Error::Io(IoError::new(
ErrorKind::Unsupported,
format!("unsupported OS {os}"),
)));
}
};
println!("downloading {new_version} for {download_os} on {download_arch}");
let download_filename =
format!("surreal-{new_version}.{download_os}-{download_arch}.{download_ext}");
let url = format!("{ROOT}/{new_version}/{download_filename}");
let response = reqwest::get(&url).await?;
if !response.status().is_success() {
return Err(Error::Io(IoError::new(
ErrorKind::Other,
format!("received status {} when downloading from {url}", response.status()),
)));
}
let binary = response.bytes().await?;
// Create a temporary file path
let tmp_dir = tempfile::tempdir()?;
let mut tmp_path = tmp_dir.path().join(download_filename);
// Download to a temp file to avoid writing to a running exe file
fs::write(&tmp_path, &*binary)?;
// Preserve permissions
fs::set_permissions(&tmp_path, permissions)?;
// Unarchive
if download_ext == "tgz" {
let output = Command::new("tar")
.arg("-zxf")
.arg(&tmp_path)
.arg("-C")
.arg(tmp_dir.path())
.output()?;
if !output.status.success() {
return Err(Error::Io(IoError::new(
ErrorKind::Other,
format!("failed to unarchive: {}", output.status),
)));
}
// focus on the extracted path
tmp_path = tmp_dir.path().join("surreal");
}
println!("installing at {}", exe.display());
// Replace the running executable
if args.dry_run {
println!("Dry run successfully completed")
} else {
replace_exe(&tmp_path, &exe)?;
println!("SurrealDB successfully upgraded");
}
Ok(())
}
/// Replace exe at `to` with contents of `from`
fn replace_exe(from: &Path, to: &Path) -> Result<(), IoError> {
if cfg!(windows) {
fs::rename(to, to.with_extension("old.exe"))?;
} else {
fs::remove_file(to)?;
}
// Rename works when from and to are on the same file system/device, but
// fall back to copy if they're not
fs::rename(from, to).or_else(|_| {
// Don't worry about deleting the file as the tmp directory will
// be deleted automatically
fs::copy(from, to).map(|_| ())
})
}