admin migrate-database

This commit is contained in:
2025-11-11 15:54:57 +01:00
parent 6f34a0a4fa
commit 31db4ede38
6 changed files with 60 additions and 5 deletions
+36 -2
View File
@@ -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
View File
@@ -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() {
+1
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
use diesel_migrations::{embed_migrations, EmbeddedMigrations};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
+3 -2
View File
@@ -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,
+1
View File
@@ -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,