mod args;
mod audit;
mod migrate_storage;
mod migrations;
mod roles;
mod scaffold;
mod server;
mod tenants;
mod users;
pub mod api;
use std::io::{self, Write};
use std::path::Path;
use super::error::TenancyError;
use super::pools::TenantPools;
pub type InitTenancyFn = fn(&Path) -> Result<super::bootstrap::InitTenancyReport, TenancyError>;
pub async fn run(
pools: &TenantPools,
registry_url: &str,
dir: &Path,
args: impl IntoIterator<Item = String>,
) -> Result<(), TenancyError> {
let mut stdout = io::stdout();
run_with_writer(pools, registry_url, dir, args, &mut stdout).await
}
pub async fn run_with_init(
pools: &TenantPools,
registry_url: &str,
dir: &Path,
args: impl IntoIterator<Item = String>,
init_fn: InitTenancyFn,
) -> Result<(), TenancyError> {
let mut stdout = io::stdout();
run_with_writer_and_init(pools, registry_url, dir, args, &mut stdout, init_fn).await
}
pub async fn run_with_writer<W: Write + Send>(
pools: &TenantPools,
registry_url: &str,
dir: &Path,
args: impl IntoIterator<Item = String>,
writer: &mut W,
) -> Result<(), TenancyError> {
run_with_writer_and_init(
pools,
registry_url,
dir,
args,
writer,
super::bootstrap::init_tenancy,
)
.await
}
pub async fn run_with_writer_and_init<W: Write + Send>(
pools: &TenantPools,
registry_url: &str,
dir: &Path,
args: impl IntoIterator<Item = String>,
writer: &mut W,
init_fn: InitTenancyFn,
) -> Result<(), TenancyError> {
let args: Vec<String> = args.into_iter().collect();
let cmd = args.first().map_or("", String::as_str);
match cmd {
"" | "help" | "--help" | "-h" => {
write_help(writer)?;
Ok(())
}
"create-tenant" => {
tenants::create_tenant(pools, registry_url, dir, &args[1..], writer).await
}
"drop-tenant" => tenants::drop_tenant(pools, &args[1..], writer).await,
"purge-tenant" => tenants::purge_tenant(pools, &args[1..], writer).await,
"list-tenants" => tenants::list_tenants(pools, writer).await,
"prewarm-pools" => {
let report = pools.prewarm_database_tenants().await?;
writeln!(
writer,
"prewarm: {} active tenants — warmed {}, failed {}, skipped (cap) {}",
report.total_active, report.warmed, report.failed, report.skipped_cap
)?;
Ok(())
}
"migrate-tenants" => {
migrations::migrate_tenants_cmd(pools, registry_url, dir, writer).await
}
"migrate-registry" => migrations::migrate_registry_cmd(pools, dir, writer).await,
"migrate-tenant-storage" => {
migrate_storage::migrate_tenant_storage_cmd(pools, registry_url, &args[1..], writer)
.await
}
"create-operator" => users::create_operator_cmd(pools, &args[1..], writer).await,
"create-user" => users::create_user_cmd(pools, registry_url, &args[1..], writer).await,
"create-superuser" => {
users::create_superuser_cmd(pools, registry_url, &args[1..], writer).await
}
"set-superuser" => users::set_superuser_cmd(pools, registry_url, &args[1..], writer).await,
"reset-password" => {
users::reset_password_cmd(pools, registry_url, &args[1..], writer).await
}
"reset-operator-password" => {
users::reset_operator_password_cmd(pools, &args[1..], writer).await
}
"change-password" => {
users::change_password_cmd(pools, registry_url, &args[1..], writer).await
}
"change-operator-password" => {
users::change_operator_password_cmd(pools, &args[1..], writer).await
}
"run-server" | "runserver" => {
server::run_server_cmd(pools, registry_url, &args[1..], writer).await
}
"init-tenancy" => migrations::init_tenancy_cmd_with(dir, writer, init_fn),
"audit-cleanup" => audit::audit_cleanup_cmd(pools, &args[1..], writer).await,
"create-role" => roles::create_role_cmd(pools, &args[1..], writer).await,
"list-roles" => roles::list_roles_cmd(pools, &args[1..], writer).await,
"assign-role" => roles::assign_role_cmd(pools, &args[1..], writer).await,
"revoke-role" => roles::revoke_role_cmd(pools, &args[1..], writer).await,
"grant-perm" => roles::grant_perm_cmd(pools, &args[1..], writer).await,
"revoke-perm" => roles::revoke_perm_cmd(pools, &args[1..], writer).await,
"create-api-key" => roles::create_api_key_cmd(pools, &args[1..], writer).await,
"startapp" => scaffold::startapp_cmd(&args[1..], writer),
"migrate" => {
migrations::migrate_all_cmd(pools, registry_url, dir, &args[1..], writer).await
}
_ => rustango::migrate::manage::run_with_writer(pools.registry(), dir, args, writer)
.await
.map_err(TenancyError::Migrate),
}
}
pub fn write_help<W: Write>(w: &mut W) -> Result<(), TenancyError> {
writeln!(w, "rustango manage CLI — tenancy-aware dispatcher")?;
writeln!(w)?;
writeln!(w, "USAGE:")?;
writeln!(w, " cargo run -- <verb> [args]")?;
writeln!(w)?;
writeln!(w, "TENANT MANAGEMENT:")?;
writeln!(
w,
" create-tenant <slug> [--display-name <s>] [--mode schema|database]"
)?;
writeln!(
w,
" [--host-pattern <s>] [--database-url <s>] [--no-migrate]"
)?;
writeln!(
w,
" Provision a new tenant. Schema mode (default) gives the"
)?;
writeln!(
w,
" tenant its own Postgres schema; database mode points at"
)?;
writeln!(
w,
" a fully separate DB via --database-url."
)?;
writeln!(w, " drop-tenant <slug> [--confirm <slug>]")?;
writeln!(
w,
" Soft-delete (active=false). Data preserved."
)?;
writeln!(
w,
" purge-tenant <slug> [--confirm <slug>] [--purge-database]"
)?;
writeln!(
w,
" HARD-delete: drops schema (or DB with --purge-database)."
)?;
writeln!(
w,
" list-tenants Print every Org row in the registry."
)?;
writeln!(w)?;
writeln!(w, "USER / OPERATOR MANAGEMENT:")?;
writeln!(
w,
" create-operator <username> [--password <p> | --generate]"
)?;
writeln!(
w,
" Operator-level account; signs into the apex /login."
)?;
writeln!(
w,
" create-user <slug> <username> [--password <p> | --generate] [--superuser]"
)?;
writeln!(
w,
" Tenant-scoped user; signs into <slug>.<apex>/__login."
)?;
writeln!(w, " create-superuser <slug> <username> [--password <p>]")?;
writeln!(
w,
" Tenant-scoped superuser (alias for create-user --superuser)."
)?;
writeln!(w, " set-superuser <slug> <username> [--on|--off]")?;
writeln!(
w,
" Promote / demote an existing tenant user."
)?;
writeln!(
w,
" reset-password <slug> <username> [--password <p> | --generate]"
)?;
writeln!(
w,
" Reset a tenant user's password without their old one."
)?;
writeln!(
w,
" reset-operator-password <username> [--password <p> | --generate]"
)?;
writeln!(
w,
" Reset an operator's password without their old one."
)?;
writeln!(
w,
" change-password <slug> <username> [--current <p>] [--password <p> | --generate]"
)?;
writeln!(
w,
" Rotate a tenant user's password; verifies current first."
)?;
writeln!(
w,
" change-operator-password <username> [--current <p>] [--password <p> | --generate]"
)?;
writeln!(
w,
" Rotate an operator's password; verifies current first."
)?;
writeln!(w)?;
writeln!(w, "MIGRATIONS:")?;
writeln!(
w,
" init-tenancy Materialize bootstrap migrations into ./migrations/."
)?;
writeln!(
w,
" makemigrations Diff models against latest snapshot, emit a new JSON file."
)?;
writeln!(
w,
" migrate Apply registry-scoped, then tenant-scoped, migrations."
)?;
writeln!(
w,
" migrate --fake <name> Insert <name> into registry ledger WITHOUT running its SQL"
)?;
writeln!(
w,
" (recovery: \"relation X already exists\" 42P07 errors)."
)?;
writeln!(
w,
" migrate-registry Apply registry-scoped migrations only."
)?;
writeln!(
w,
" migrate-tenants Apply tenant-scoped migrations to every active org."
)?;
writeln!(
w,
" prewarm-pools Eagerly build pools for every active database-mode tenant"
)?;
writeln!(
w,
" (drops first-request latency on the hot path)."
)?;
writeln!(
w,
" migrate-tenant-storage <slug> --to schema|database [--database-url <s>] [--schema-name <s>] [--dry-run]"
)?;
writeln!(
w,
" Move a tenant between storage modes via pg_dump → psql."
)?;
writeln!(
w,
" showmigrations List which migrations are applied / pending."
)?;
writeln!(
w,
" downgrade Roll back the most recent migration."
)?;
writeln!(w)?;
writeln!(w, "ROLES & PERMISSIONS:")?;
writeln!(w, " create-role <slug> <name> [--description <s>]")?;
writeln!(w, " Create a named role on a tenant.")?;
writeln!(
w,
" list-roles <slug> List roles and permission counts."
)?;
writeln!(w, " assign-role <slug> <username> <role>")?;
writeln!(w, " Add a user to a role.")?;
writeln!(w, " revoke-role <slug> <username> <role>")?;
writeln!(w, " Remove a user from a role.")?;
writeln!(w, " grant-perm <slug> <name> <codename> [--role]")?;
writeln!(
w,
" Grant a codename to a user (default) or role (--role)."
)?;
writeln!(w, " revoke-perm <slug> <name> <codename> [--role]")?;
writeln!(
w,
" Revoke a codename from a user or role."
)?;
writeln!(
w,
" create-api-key <slug> <username> [--label <s>] [--expires-days <N>]"
)?;
writeln!(
w,
" Issue a Bearer API key for a tenant user."
)?;
writeln!(w)?;
writeln!(w, "AUDIT:")?;
writeln!(
w,
" audit-cleanup --days <N> Delete entries older than N days (all active tenants)."
)?;
writeln!(
w,
" audit-cleanup --keep-last <N> Keep N most recent entries per row."
)?;
writeln!(
w,
" [--tenant <s>] Scope to one tenant instead of all."
)?;
writeln!(w)?;
writeln!(w, "SERVER:")?;
writeln!(w, " run-server [--bind <addr>]")?;
writeln!(
w,
" Boot the HTTP server with admin + operator console."
)?;
writeln!(w)?;
writeln!(w, "SCAFFOLDING:")?;
writeln!(
w,
" startapp <name> [--into <dir>] [--with-manage-bin] [--with-bootstrap-migration]"
)?;
writeln!(
w,
" Scaffold a Django-shape app module."
)?;
writeln!(w)?;
writeln!(w, "EXAMPLES:")?;
writeln!(w, " cargo run -- migrate")?;
writeln!(w, " cargo run -- create-operator admin --password letmein")?;
writeln!(
w,
" cargo run -- create-tenant acme --display-name 'ACME Corp'"
)?;
writeln!(
w,
" cargo run -- create-user acme alice --password hunter2 --superuser"
)?;
writeln!(w, " cargo run -- list-tenants")?;
writeln!(w, " cargo run -- audit-cleanup --days 90")?;
writeln!(
w,
" cargo run -- audit-cleanup --keep-last 50 --tenant acme"
)?;
writeln!(w)?;
writeln!(
w,
"Run any verb with --help for verb-specific flags + details."
)?;
Ok(())
}