use std::path::Path;
use sqlx::Database;
use crate::core::Column as _;
use crate::sql::{Auto, FetcherPool};
use crate::tenancy::auth::{Operator, User};
use crate::tenancy::error::TenancyError;
use crate::tenancy::org::{Org, StorageMode};
use crate::tenancy::pools::TenantPools;
use crate::tenancy::provision::{self, DatabaseSource, TenantStorage};
pub struct CreateTenantOpts {
pub mode: StorageMode,
pub backend: Option<crate::tenancy::BackendKind>,
pub display_name: Option<String>,
pub schema_name: Option<String>,
pub database_url: Option<String>,
pub database_server: Option<String>,
pub host_pattern: Option<String>,
pub port: Option<i32>,
pub path_prefix: Option<String>,
pub no_migrate: bool,
pub first_admin: Option<crate::tenancy::FirstAdmin>,
}
impl Default for CreateTenantOpts {
fn default() -> Self {
Self {
mode: StorageMode::Schema,
backend: None,
display_name: None,
schema_name: None,
database_url: None,
database_server: None,
host_pattern: None,
port: None,
path_prefix: None,
no_migrate: false,
first_admin: None,
}
}
}
impl CreateTenantOpts {
fn storage(&self) -> Result<TenantStorage, TenancyError> {
let bad = |m: &str| Err(TenancyError::Validation(format!("create_tenant: {m}")));
match (self.mode, &self.database_url, &self.database_server) {
(StorageMode::Schema, None, None) => {
Ok(TenantStorage::Schema(self.schema_name.clone()))
}
(StorageMode::Schema, _, _) => bad("schema mode takes no database URL or server"),
(StorageMode::Database, Some(url), None) => {
Ok(TenantStorage::Database(DatabaseSource::Url(url.clone())))
}
(StorageMode::Database, None, Some(server)) => Ok(TenantStorage::Database(
DatabaseSource::Server(server.clone()),
)),
(StorageMode::Database, Some(_), Some(_)) => {
bad("give database_url or database_server, not both")
}
(StorageMode::Database, None, None) => {
bad("mode=Database requires database_url or database_server")
}
}
}
}
pub async fn find_org<DB: Database>(
pools: &TenantPools<DB>,
slug: &str,
) -> Result<Option<Org>, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let mut rows: Vec<Org> = Org::objects()
.where_(Org::slug.eq(slug.to_owned()))
.fetch(&pools.registry_pool())
.await?;
Ok(rows.pop())
}
pub async fn create_tenant<DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
migrations_dir: &Path,
slug: &str,
opts: CreateTenantOpts,
) -> Result<Org, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let pools = pools.fresh();
let (url, dir, slug) = (
registry_url.to_owned(),
migrations_dir.to_owned(),
slug.to_owned(),
);
tokio::spawn(async move { create_tenant_inner(&pools, &url, &dir, &slug, opts).await })
.await
.map_err(|e| TenancyError::Validation(format!("the create task stopped: {e}")))?
}
async fn create_tenant_inner<DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
migrations_dir: &Path,
slug: &str,
opts: CreateTenantOpts,
) -> Result<Org, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let request = provision::ProvisionRequest {
backend: opts.backend,
display_name: opts.display_name.clone(),
host_pattern: opts.host_pattern.clone(),
port: opts.port,
path_prefix: opts.path_prefix.clone(),
run_migrations: !opts.no_migrate,
first_admin: opts.first_admin.clone(),
..provision::ProvisionRequest::new(slug, opts.storage()?)
};
let (_run, outcome) = provision::provision_tenant_recorded(
pools,
registry_url,
migrations_dir,
&request,
None,
None,
None,
)
.await?;
created_org(pools, slug, outcome).await
}
async fn created_org<DB: Database>(
pools: &TenantPools<DB>,
slug: &str,
outcome: provision::ProvisionOutcome,
) -> Result<Org, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
if let provision::MigrationsOutcome::Failed(e) = outcome.migrations {
return Err(TenancyError::Validation(format!(
"tenant `{slug}` migrations failed; it stays inactive: {e}"
)));
}
find_org(pools, slug).await?.ok_or_else(|| {
TenancyError::Validation(format!(
"create_tenant: tenant `{slug}` vanished after INSERT"
))
})
}
pub async fn create_tenant_if_missing<DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
migrations_dir: &Path,
slug: &str,
opts: CreateTenantOpts,
) -> Result<Org, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let Some(existing) = find_org(pools, slug).await? else {
return create_tenant(pools, registry_url, migrations_dir, slug, opts).await;
};
let registry = pools.registry_pool();
let org_id = existing.id.get().copied().unwrap_or_default();
if existing.active
|| !crate::tenancy::provision_store::org_left_by_failed_run(®istry, org_id).await?
{
return Ok(existing);
}
let mode = StorageMode::parse(&existing.storage_mode)
.map_err(|m| TenancyError::Validation(format!("unknown storage mode `{m}`")))?;
let storage = match mode {
StorageMode::Schema => TenantStorage::Schema(existing.schema_name.clone()),
StorageMode::Database => TenantStorage::Database(DatabaseSource::Url(
existing.database_url.clone().unwrap_or_default(),
)),
};
let request = provision::ProvisionRequest {
run_migrations: !opts.no_migrate,
first_admin: opts.first_admin.clone(),
..provision::ProvisionRequest::new(slug, storage)
};
let run = crate::tenancy::provision_store::open_run(
®istry,
slug,
&existing.storage_mode,
&existing.backend_kind,
None,
None,
None,
)
.await?;
let run_id = run.id.get().copied().unwrap_or_default();
let outcome = provision::provision_tenant_in_run(
pools,
registry_url,
migrations_dir,
&request,
None,
run_id,
)
.await?;
let org = created_org(pools, slug, outcome).await?;
let forget = crate::tenancy::provision_store::forget_unsucceeded_runs(org_id)?;
crate::sql::update_pool(®istry, &forget).await?;
Ok(org)
}
pub async fn create_operator_if_missing<DB: Database>(
pools: &TenantPools<DB>,
username: &str,
password: &str,
) -> Result<Operator, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let registry = pools.registry_pool();
let mut existing: Vec<Operator> = Operator::objects()
.where_(Operator::username.eq(username.to_owned()))
.fetch(®istry)
.await?;
if let Some(op) = existing.pop() {
return Ok(op);
}
let mut op = Operator {
id: Auto::default(),
username: username.to_owned(),
password_hash: crate::tenancy::password::hash_async(password).await?,
active: true,
created_at: chrono::Utc::now(),
password_changed_at: None,
sessions_revoked_at: None,
};
op.insert_pool(®istry).await?;
Ok(op)
}
pub async fn create_user_if_missing<DB: Database>(
pools: &TenantPools<DB>,
slug: &str,
username: &str,
password: &str,
superuser: bool,
) -> Result<User, TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let org = find_org(pools, slug)
.await?
.ok_or_else(|| TenancyError::Validation(format!("no tenant with slug `{slug}`")))?;
let scoped = pools.scoped_pool_dyn(&org).await?;
let mut existing: Vec<User> = User::objects()
.where_(User::username.eq(username.to_owned()))
.fetch(&scoped)
.await?;
if let Some(u) = existing.pop() {
return Ok(u);
}
crate::tenancy::auth::NewUser {
username,
password_hash: crate::tenancy::password::hash_async(password).await?,
is_superuser: superuser,
email: None,
}
.insert(&scoped)
.await
}