This commit is contained in:
2025-10-29 22:27:32 +01:00
parent 7a6ac5f0be
commit 5fe0da1a65
4 changed files with 83 additions and 87 deletions
@@ -0,0 +1,2 @@
ALTER TABLE tenants
ADD CONSTRAINT tenants_name_key UNIQUE (name);
@@ -0,0 +1,2 @@
ALTER TABLE tenants
DROP CONSTRAINT IF EXISTS tenants_name_key;
+78 -68
View File
@@ -44,23 +44,23 @@ enum Command {
quickwit_index: Option<String>,
},
DeleteTenant {
name: String,
tenant_id: Uuid,
},
AddUserToTenant {
username: String,
name: String,
tenant_id: Uuid,
},
RemoveUserFromTenant {
username: String,
name: String,
tenant_id: Uuid,
},
ReanalyzeDocuments {
name: String,
tenant_id: Uuid,
},
ListTenants,
DeleteAssets(String),
QuickwitCreate(String),
QuickwitDelete(String),
DeleteAssets(Uuid),
QuickwitCreate(Uuid),
QuickwitDelete(Uuid),
}
impl Command {
@@ -71,14 +71,19 @@ impl Command {
list-users\n\
delete-user <username>\n\
create-tenant <name> [storage_root] [quickwit_index]\n\
delete-tenant <name>\n\
add-user-to-tenant <username> <name>\n\
remove-user-from-tenant <username> <name>\n\
reanalyze-documents <name>\n\
delete-tenant <tenant-id>\n\
add-user-to-tenant <username> <tenant-id>\n\
remove-user-from-tenant <username> <tenant-id>\n\
reanalyze-documents <tenant-id>\n\
list-tenants\n\
delete-assets <name>\n\
quickwit-create-index <name>\n\
quickwit-delete-index <name>"
delete-assets <tenant-id>\n\
quickwit-create-index <tenant-id>\n\
quickwit-delete-index <tenant-id>"
}
fn parse_tenant_id(arg: Option<String>) -> Result<Uuid> {
let raw = arg.ok_or_else(|| anyhow!("tenant id required"))?;
Uuid::parse_str(&raw).map_err(|_| anyhow!("invalid tenant id: {}", raw))
}
fn parse() -> Result<Self> {
@@ -102,29 +107,27 @@ impl Command {
quickwit_index: args.next(),
}),
Some("delete-tenant") => Ok(Self::DeleteTenant {
name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
tenant_id: Self::parse_tenant_id(args.next())?,
}),
Some("add-user-to-tenant") => Ok(Self::AddUserToTenant {
username: args.next().ok_or_else(|| anyhow!("username required"))?,
name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
tenant_id: Self::parse_tenant_id(args.next())?,
}),
Some("remove-user-from-tenant") => Ok(Self::RemoveUserFromTenant {
username: args.next().ok_or_else(|| anyhow!("username required"))?,
name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
tenant_id: Self::parse_tenant_id(args.next())?,
}),
Some("reanalyze-documents") => Ok(Self::ReanalyzeDocuments {
name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
tenant_id: Self::parse_tenant_id(args.next())?,
}),
Some("list-tenants") => Ok(Self::ListTenants),
Some("delete-assets") => Ok(Self::DeleteAssets(
args.next().ok_or_else(|| anyhow!("tenant name required"))?,
)),
Some("quickwit-create-index") => Ok(Self::QuickwitCreate(
args.next().ok_or_else(|| anyhow!("tenant name required"))?,
)),
Some("quickwit-delete-index") => Ok(Self::QuickwitDelete(
args.next().ok_or_else(|| anyhow!("tenant name required"))?,
)),
Some("delete-assets") => Ok(Self::DeleteAssets(Self::parse_tenant_id(args.next())?)),
Some("quickwit-create-index") => {
Ok(Self::QuickwitCreate(Self::parse_tenant_id(args.next())?))
}
Some("quickwit-delete-index") => {
Ok(Self::QuickwitDelete(Self::parse_tenant_id(args.next())?))
}
_ => Err(anyhow!(Self::usage())),
}
}
@@ -147,19 +150,25 @@ async fn main() -> Result<()> {
storage_root,
quickwit_index,
} => create_tenant(&pool, &name, storage_root, quickwit_index)?,
Command::DeleteTenant { name } => delete_tenant(&pool, &name)?,
Command::AddUserToTenant { username, name } => add_user_to_tenant(&pool, &username, &name)?,
Command::RemoveUserFromTenant { username, name } => {
remove_user_from_tenant(&pool, &username, &name)?
}
Command::ReanalyzeDocuments { name } => reanalyze_documents(&pool, &name)?,
Command::DeleteTenant { tenant_id } => delete_tenant(&pool, tenant_id)?,
Command::AddUserToTenant {
username,
tenant_id,
} => add_user_to_tenant(&pool, &username, tenant_id)?,
Command::RemoveUserFromTenant {
username,
tenant_id,
} => remove_user_from_tenant(&pool, &username, tenant_id)?,
Command::ReanalyzeDocuments { tenant_id } => reanalyze_documents(&pool, tenant_id)?,
Command::ListTenants => list_tenants(&pool)?,
Command::DeleteAssets(name) => delete_assets_for_tenant(&config, &pool, &name).await?,
Command::QuickwitCreate(name) => {
quickwit_index(&config, &pool, &name, Method::POST).await?
Command::DeleteAssets(tenant_id) => {
delete_assets_for_tenant(&config, &pool, tenant_id).await?
}
Command::QuickwitDelete(name) => {
quickwit_index(&config, &pool, &name, Method::DELETE).await?
Command::QuickwitCreate(tenant_id) => {
quickwit_index(&config, &pool, tenant_id, Method::POST).await?
}
Command::QuickwitDelete(tenant_id) => {
quickwit_index(&config, &pool, tenant_id, Method::DELETE).await?
}
}
@@ -297,29 +306,29 @@ fn create_tenant(
Ok(())
}
fn delete_tenant(pool: &PgPool, name: &str) -> Result<()> {
fn delete_tenant(pool: &PgPool, tenant_id: Uuid) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table
.filter(tenants::name.eq(name))
.find(tenant_id)
.first(&mut conn)
.optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;
let member_exists: bool = select(exists(
user_memberships::table.filter(user_memberships::tenant_id.eq(tenant.id)),
))
.get_result(&mut conn)?;
if member_exists {
bail!("tenant '{}' still has user memberships", name);
bail!("tenant '{}' still has user memberships", tenant.name);
}
diesel::delete(tenants::table.filter(tenants::id.eq(tenant.id))).execute(&mut conn)?;
println!("deleted tenant '{}'", name);
println!("deleted tenant '{}'", tenant.name);
Ok(())
}
fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> {
fn add_user_to_tenant(pool: &PgPool, username: &str, tenant_id: Uuid) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?;
let user: User = users::table
@@ -329,10 +338,10 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> {
.ok_or_else(|| anyhow!("user '{}' not found", username))?;
let tenant: Tenant = tenants::table
.filter(tenants::name.eq(name))
.find(tenant_id)
.first(&mut conn)
.optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;
let membership = NewUserMembership {
id: Uuid::new_v4(),
@@ -346,11 +355,11 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> {
.do_nothing()
.execute(&mut conn)?;
println!("added user '{}' to tenant '{}'", username, name);
println!("added user '{}' to tenant '{}'", username, tenant.name);
Ok(())
}
fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> {
fn remove_user_from_tenant(pool: &PgPool, username: &str, tenant_id: Uuid) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?;
let user: User = users::table
@@ -360,10 +369,10 @@ fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result<
.ok_or_else(|| anyhow!("user '{}' not found", username))?;
let tenant: Tenant = tenants::table
.filter(tenants::name.eq(name))
.find(tenant_id)
.first(&mut conn)
.optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;
let removed = diesel::delete(
user_memberships::table
@@ -373,21 +382,24 @@ fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result<
.execute(&mut conn)?;
if removed == 0 {
println!("user '{}' was not a member of tenant '{}'", username, name);
println!(
"user '{}' was not a member of tenant '{}'",
username, tenant.name
);
} else {
println!("removed user '{}' from tenant '{}'", username, name);
println!("removed user '{}' from tenant '{}'", username, tenant.name);
}
Ok(())
}
fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> {
fn reanalyze_documents(pool: &PgPool, tenant_id: Uuid) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table
.filter(tenants::name.eq(name))
.find(tenant_id)
.first(&mut conn)
.optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;
let targets: Vec<(Uuid, Uuid)> = documents::table
.filter(documents::tenant_id.eq(tenant.id))
@@ -396,7 +408,7 @@ fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> {
.load(&mut conn)?;
if targets.is_empty() {
println!("tenant '{}' has no active documents", name);
println!("tenant '{}' has no active documents", tenant.name);
return Ok(());
}
@@ -419,7 +431,7 @@ fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> {
println!(
"queued {} documents for re-analysis in tenant '{}'",
queued, name
queued, tenant.name
);
Ok(())
}
@@ -437,7 +449,7 @@ fn list_tenants(pool: &PgPool) -> Result<()> {
}
for tenant in tenants {
println!("{} ({})", tenant.name, tenant.id);
println!("{} {}", tenant.id, tenant.name);
}
Ok(())
@@ -446,7 +458,7 @@ fn list_tenants(pool: &PgPool) -> Result<()> {
async fn delete_assets_for_tenant(
config: &AppConfig,
pool: &PgPool,
tenant_name: &str,
tenant_id: Uuid,
) -> Result<()> {
let s3_client = s3::build_client(config).await?;
let storage: Arc<dyn ObjectStorage> =
@@ -454,11 +466,10 @@ async fn delete_assets_for_tenant(
let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table
.filter(tenants::name.eq(tenant_name))
.find(tenant_id)
.first(&mut conn)
.optional()
.context("failed to load tenant")?
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_name))?;
.optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;
let tenant_storage = TenantStorage::new(Arc::clone(&storage), &tenant)
.with_context(|| format!("missing storage root for tenant {}", tenant.name))?;
@@ -516,7 +527,7 @@ async fn delete_assets_for_tenant(
async fn quickwit_index(
config: &AppConfig,
pool: &PgPool,
name: &str,
tenant_id: Uuid,
method: Method,
) -> Result<()> {
let endpoint = config
@@ -526,11 +537,10 @@ async fn quickwit_index(
let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table
.filter(tenants::name.eq(name))
.find(tenant_id)
.first(&mut conn)
.optional()
.context("failed to query tenants")?
.ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
.optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;
let client = Client::new();
let index_id = format!("documents-{}", tenant.id);
+1 -19
View File
@@ -1,9 +1,4 @@
use diesel::{
dsl::{exists, select},
pg::PgConnection,
prelude::*,
sql_types::Text,
};
use diesel::{pg::PgConnection, prelude::*, sql_types::Text};
use serde_json::json;
use uuid::Uuid;
@@ -51,10 +46,6 @@ impl TenantService {
Ok(tenant)
}
pub fn tenant_id_for_name(&self, name: &str) -> AppResult<Uuid> {
Ok(self.get_by_name(name)?.id)
}
pub fn create_tenant(
&self,
name: &str,
@@ -74,15 +65,6 @@ impl TenantService {
AppError::internal("database pool error")
})?;
let exists: bool =
select(exists(dsl::tenants.filter(dsl::name.eq(name)))).get_result(&mut conn)?;
if exists {
return Err(AppError::conflict(format!(
"tenant '{}' already exists",
name
)));
}
let id = Uuid::new_v4();
let storage_root = normalize_storage_root(storage_root, id);
let quickwit_index = normalize_quickwit_index(quickwit_index, id);