admin migrate-database
This commit is contained in:
@@ -3,18 +3,21 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use chrono::{Duration as ChronoDuration, Utc};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use diesel::{dsl::exists, prelude::*, select};
|
||||
use diesel::{dsl::exists, pg::PgConnection, prelude::*, select};
|
||||
use diesel_migrations::MigrationHarness;
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
use reqwest::{Client, Method, StatusCode};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::task;
|
||||
use uuid::Uuid;
|
||||
|
||||
use papercrate::{
|
||||
auth::capability_sets::{ensure_capability_set, owner_capabilities},
|
||||
config::AppConfig,
|
||||
config::{redact_database_url, AppConfig},
|
||||
db::{self, PgPool},
|
||||
documents::search::ensure_quickwit_index,
|
||||
jobs::{enqueue_job, JOB_ANALYZE_DOCUMENT, JOB_DELETE_TENANT},
|
||||
migrations::MIGRATIONS,
|
||||
models::{
|
||||
DocumentAsset, DocumentAssetObject, MagicToken, MagicTokenKind, NewUser, NewUserMembership,
|
||||
Tenant, TenantStatus, User,
|
||||
@@ -112,6 +115,14 @@ enum Command {
|
||||
#[arg(long = "kind", value_enum, default_value_t = MagicTokenKindArg::EmailLogin)]
|
||||
kind: MagicTokenKindArg,
|
||||
},
|
||||
MigrateDatabase {
|
||||
#[arg(
|
||||
long = "database-url",
|
||||
value_name = "URL",
|
||||
help = "Override the migrations database URL (defaults to MIGRATIONS_DATABASE_URL or DATABASE_URL)"
|
||||
)]
|
||||
database_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, ValueEnum)]
|
||||
@@ -211,11 +222,34 @@ async fn main() -> Result<()> {
|
||||
} => {
|
||||
create_magic_token(&pool, &username, ttl_minutes, max_uses, kind.into())?;
|
||||
}
|
||||
Command::MigrateDatabase { database_url } => {
|
||||
let url = database_url.unwrap_or_else(|| config.migrations_database_url().to_string());
|
||||
migrate_database(url).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migrate_database(database_url: String) -> Result<()> {
|
||||
let redacted = redact_database_url(&database_url);
|
||||
tracing::info!(database_url = %redacted, "running pending migrations");
|
||||
|
||||
let result = task::spawn_blocking(move || -> Result<()> {
|
||||
let mut conn =
|
||||
PgConnection::establish(&database_url).context("failed to connect to database")?;
|
||||
conn.run_pending_migrations(MIGRATIONS)
|
||||
.map_err(|err| anyhow!("failed to run migrations: {err}"))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("migration task panicked")?;
|
||||
|
||||
result?;
|
||||
tracing::info!(database_url = %redacted, "migrations completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_user(pool: &PgPool, username: &str) -> Result<()> {
|
||||
let username = normalize_identifier(
|
||||
username,
|
||||
|
||||
+16
-1
@@ -10,6 +10,8 @@ use crate::db::DEFAULT_MAX_POOL_SIZE;
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub database_url: String,
|
||||
#[serde(default)]
|
||||
pub migrations_database_url: Option<String>,
|
||||
#[serde(default = "default_database_max_pool_size")]
|
||||
pub database_max_pool_size: u32,
|
||||
#[serde(default = "default_server_host")]
|
||||
@@ -86,6 +88,7 @@ impl AppConfig {
|
||||
tracing::info!(
|
||||
component,
|
||||
database_url = %config.redacted_database_url(),
|
||||
migrations_database_url = %config.redacted_migrations_database_url(),
|
||||
pool_size = config.database_max_pool_size,
|
||||
quickwit_enabled = config.quickwit_endpoint.is_some(),
|
||||
passkeys_enabled = config.webauthn_origin.is_some(),
|
||||
@@ -107,6 +110,18 @@ impl AppConfig {
|
||||
pub fn redacted_database_url(&self) -> String {
|
||||
redact_database_url(&self.database_url)
|
||||
}
|
||||
|
||||
pub fn redacted_migrations_database_url(&self) -> String {
|
||||
redact_database_url(self.migrations_database_url())
|
||||
}
|
||||
|
||||
pub fn migrations_database_url(&self) -> &str {
|
||||
if let Some(ref url) = self.migrations_database_url {
|
||||
url
|
||||
} else {
|
||||
&self.database_url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
@@ -244,7 +259,7 @@ fn default_webauthn_rp_name() -> String {
|
||||
"Papercrate".to_string()
|
||||
}
|
||||
|
||||
fn redact_database_url(raw: &str) -> String {
|
||||
pub fn redact_database_url(raw: &str) -> String {
|
||||
match Url::parse(raw) {
|
||||
Ok(mut parsed) => {
|
||||
if parsed.password().is_some() {
|
||||
|
||||
@@ -18,4 +18,5 @@ pub mod tenants;
|
||||
pub mod utils;
|
||||
pub mod workers;
|
||||
pub use workers::{default_handlers, Worker};
|
||||
pub mod migrations;
|
||||
pub mod test_support;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations};
|
||||
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
@@ -10,6 +10,7 @@ use crate::auth::capability_sets::{
|
||||
use crate::auth::jwt::{AccessTokenContext, JwtService, PrincipalKind};
|
||||
use crate::config::AppConfig;
|
||||
use crate::db::{self, PgPool};
|
||||
use crate::migrations::MIGRATIONS;
|
||||
use crate::models::{
|
||||
Job, NewUser, NewUserMembership, NewUserPasskey, NewUserSession, Tenant, TenantStatus, User,
|
||||
UserMembership,
|
||||
@@ -28,7 +29,7 @@ use diesel::connection::SimpleConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::OptionalExtension;
|
||||
use diesel::PgConnection;
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use diesel_migrations::MigrationHarness;
|
||||
use http_body_util::BodyExt;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
@@ -39,7 +40,6 @@ use tokio::sync::Mutex;
|
||||
use tower::util::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
const RESET_DATABASE_SQL: &str = "DROP SCHEMA IF EXISTS tenant CASCADE;\n\
|
||||
DROP SCHEMA IF EXISTS shared CASCADE;\n\
|
||||
DROP SCHEMA IF EXISTS public CASCADE;\n\
|
||||
@@ -170,6 +170,7 @@ impl TestApp {
|
||||
|
||||
let mut config = AppConfig {
|
||||
database_url: database_url.clone(),
|
||||
migrations_database_url: None,
|
||||
database_max_pool_size: db::DEFAULT_MAX_POOL_SIZE,
|
||||
server_host: "127.0.0.1".to_string(),
|
||||
server_port: 0,
|
||||
|
||||
@@ -641,6 +641,7 @@ mod tests {
|
||||
fn base_config() -> AppConfig {
|
||||
AppConfig {
|
||||
database_url: "postgres://test".to_string(),
|
||||
migrations_database_url: None,
|
||||
database_max_pool_size: 5,
|
||||
server_host: "127.0.0.1".to_string(),
|
||||
server_port: 0,
|
||||
|
||||
Reference in New Issue
Block a user