From 6446822bf291211d4375627e37e7bd215b50f32f Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Fri, 13 Sep 2024 14:44:51 -0700 Subject: [PATCH] add 'db migrate' subcommand --- src/cli.rs | 33 ++++++++++++++++++++++++++ src/cli/migrate_db.rs | 55 +++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 46 +++++++++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/cli/migrate_db.rs diff --git a/src/cli.rs b/src/cli.rs index c997ad67..5c0824d7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,6 +9,7 @@ use clap::{Parser, Subcommand}; use crate::error; +mod migrate_db; mod serve; /// Command line arguments @@ -26,6 +27,10 @@ pub(crate) struct Args { pub(crate) enum Command { /// Run the server. Serve(ServeArgs), + + /// Commands for interacting with the database. + #[clap(subcommand)] + Db(DbCommand), } /// Wrapper for the `--config` arg. @@ -57,10 +62,38 @@ pub(crate) struct ServeArgs { pub(crate) config: ConfigArg, } +#[derive(Subcommand)] +pub(crate) enum DbCommand { + /// Migrate database from one server implementation to another. + /// + /// This command is not protected against symlink-swapping attacks. Do not + /// use it when any subdirectories or parents of the `--in`, `--out`, or + /// `--inplace` directories may be written by an untrusted user during + /// execution. + Migrate(MigrateDbArgs), +} + +#[derive(clap::Args)] +pub(crate) struct MigrateDbArgs { + #[clap(flatten)] + config: ConfigArg, + + /// Path to read database from. + #[clap(long = "in", short)] + pub(crate) in_path: PathBuf, + + /// Path to write migrated database to. + #[clap(long = "out", short)] + pub(crate) out_path: PathBuf, +} + impl Args { pub(crate) async fn run(self) -> Result<(), error::Main> { match self.command { Command::Serve(args) => serve::run(args).await?, + Command::Db(DbCommand::Migrate(args)) => { + migrate_db::run(args).await?; + } } Ok(()) } diff --git a/src/cli/migrate_db.rs b/src/cli/migrate_db.rs new file mode 100644 index 00000000..8953ae45 --- /dev/null +++ b/src/cli/migrate_db.rs @@ -0,0 +1,55 @@ +use super::MigrateDbArgs; +use crate::{ + config, database::KeyValueDatabase, error, observability, + service::globals::DbVersion, services, utils::copy_dir, Services, +}; + +pub(crate) async fn run( + args: MigrateDbArgs, +) -> Result<(), error::MigrateDbCommand> { + use error::MigrateDbCommand as Error; + + let mut config = config::load(args.config.config.as_ref()).await?; + // mutating the config like this is ugly, but difficult to avoid. Currently + // the database is very tightly coupled with service code, which reads the + // path only from the config. + args.out_path + .to_str() + .ok_or(Error::InvalidUnicodeOutPath)? + .clone_into(&mut config.database.path); + + let (_guard, reload_handles) = observability::init(&config)?; + + copy_dir(&args.in_path, &args.out_path).await.map_err(Error::Copy)?; + + let db = Box::leak(Box::new( + KeyValueDatabase::load_or_create(&config).map_err(Error::LoadDb)?, + )); + + Services::build(db, config, reload_handles) + .map_err(Error::InitializeServices)? + .install(); + + services().globals.err_if_server_name_changed()?; + + // Migrate from a grapevine-compatible db to a conduit-0.8.0-compatible db + + let version = + services().globals.database_version().map_err(Error::MigrateDb)?; + if version < DbVersion::Grapevine(0) { + return Err(Error::DbVersionTooOld(version)); + } else if version != DbVersion::Grapevine(0) { + return Err(Error::DbVersionUnsupported(version)); + }; + + // Undo Conduit(13) -> Grapevine(0) + // + // This is a no-op that only changes the db version namespace. Setting the + // version to Conduit(_) will restore the original state. + services() + .globals + .bump_database_version(DbVersion::Conduit(13)) + .map_err(Error::MigrateDb)?; + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index a0955b30..a07671ed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,7 +4,7 @@ use std::{fmt, iter, path::PathBuf}; use thiserror::Error; -use crate::config::ListenConfig; +use crate::{config::ListenConfig, service::globals::DbVersion}; /// Formats an [`Error`][0] and its [`source`][1]s with a separator /// @@ -42,6 +42,9 @@ impl fmt::Display for DisplayWithSources<'_> { pub(crate) enum Main { #[error(transparent)] ServeCommand(#[from] ServeCommand), + + #[error(transparent)] + MigrateDbCommand(#[from] MigrateDbCommand), } /// Errors returned from the `serve` CLI subcommand. @@ -85,6 +88,47 @@ pub(crate) enum ServerNameChanged { Renamed, } +/// Top-level errors from the `db migrate` subcommand. +// Missing docs are allowed here since that kind of information should be +// encoded in the error messages themselves anyway. +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub(crate) enum MigrateDbCommand { + #[error("output path is not valid unicode")] + InvalidUnicodeOutPath, + + #[error("failed to copy existing database directory")] + Copy(#[source] CopyDir), + + #[error("failed to initialize observability")] + Observability(#[from] Observability), + + #[error("failed to load configuration")] + Config(#[from] Config), + + #[error("failed to load database")] + LoadDb(#[source] crate::utils::error::Error), + + #[error("failed to initialize services")] + InitializeServices(#[source] crate::utils::error::Error), + + #[error("`server_name` change check failed")] + ServerNameChanged(#[from] ServerNameChanged), + + #[error("failed to migrate database")] + MigrateDb(#[source] crate::utils::error::Error), + + #[error( + "initial database version is too old for migration: {_0:?}. Try \ + loading the database with the latest conduit release and then \ + attempting migration again." + )] + DbVersionTooOld(DbVersion), + + #[error("initial database version is not supported for migration: {_0:?}")] + DbVersionUnsupported(DbVersion), +} + /// Errors copying a directory recursively. /// /// Returned by the [`crate::utils::copy_dir`] function.