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