This commit is contained in:
2025-11-10 23:06:53 +01:00
parent 8dddb0b940
commit 8788cd3e25
11 changed files with 1336 additions and 1177 deletions
+155 -155
View File
@@ -1,4 +1,5 @@
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use chrono::Utc;
@@ -18,7 +19,11 @@ use crate::models::{NewUserMembership, TenantStatus};
use crate::schema::{tenants, user_memberships};
use crate::state::AppState;
use crate::tenants::TenantRepository;
use crate::workers::{JobExecution, JobHandler};
use crate::workers::{
job_execution_from_task_error,
taskflow::{BoxedTask, Task, TaskContext, TaskError, TaskExecutor, TaskPlanner, TaskResult},
JobExecution, JobHandler,
};
pub struct ProvisionTenantJob;
@@ -40,77 +45,120 @@ impl JobHandler for ProvisionTenantJob {
job: crate::models::Job,
_storage: crate::storage::TenantStorage,
) -> JobExecution {
let mut conn = match state.db_unscoped() {
Ok(conn) => conn,
Err(err) => {
warn!(job_id = %job.id, error = ?err, "failed to get connection for tenant provisioning");
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "database connection unavailable".into(),
};
}
};
let members = ProvisionPayload::from_job(&job).unwrap_or_default();
let mut context = ProvisionContext::new(
job.id,
JOB_PROVISION_TENANT,
job.tenant_id,
state.clone(),
members,
);
let tenant = match TenantRepository::get_by_id(&mut conn, job.tenant_id) {
Ok(tenant) => tenant,
Err(err) => {
warn!(job_id = %job.id, error = ?err, "tenant not found for provisioning");
return JobExecution::Failed {
error: "tenant not found".into(),
};
}
};
let planner = ProvisionPlanner;
match TaskExecutor::run(&planner, &mut context).await {
Ok(()) => JobExecution::Success,
Err(err) => job_execution_from_task_error(err),
}
}
}
struct ProvisionContext {
job_id: Uuid,
job_type: &'static str,
tenant_id: Uuid,
state: Arc<AppState>,
members: Vec<Uuid>,
}
impl ProvisionContext {
fn new(
job_id: Uuid,
job_type: &'static str,
tenant_id: Uuid,
state: Arc<AppState>,
members: Vec<Uuid>,
) -> Self {
Self {
job_id,
job_type,
tenant_id,
state,
members,
}
}
}
impl TaskContext for ProvisionContext {
fn job_id(&self) -> Uuid {
self.job_id
}
fn job_type(&self) -> &'static str {
self.job_type
}
}
struct ProvisionPlanner;
#[async_trait]
impl TaskPlanner<ProvisionContext> for ProvisionPlanner {
async fn plan(
&self,
_ctx: &mut ProvisionContext,
) -> TaskResult<Vec<BoxedTask<ProvisionContext>>> {
Ok(vec![Box::new(ProvisionTask)])
}
}
struct ProvisionTask;
#[async_trait]
impl Task<ProvisionContext> for ProvisionTask {
fn name(&self) -> &'static str {
"provision-tenant"
}
async fn execute(&self, ctx: &mut ProvisionContext) -> TaskResult<()> {
let mut conn = ctx
.state
.db_unscoped()
.map_err(|err| TaskError::retry(Duration::from_secs(30), format!("{err:?}")))?;
let tenant = TenantRepository::get_by_id(&mut conn, ctx.tenant_id).map_err(|err| {
TaskError::fail(format!("tenant not found for provisioning: {err:?}"))
})?;
drop(conn);
let mut conn = match state.db_for_tenant(tenant.id) {
Ok(conn) => conn,
Err(err) => {
warn!(job_id = %job.id, error = ?err, "failed to scope connection for tenant provisioning");
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "tenant connection unavailable".into(),
};
}
};
let mut conn = ctx
.state
.db_for_tenant(tenant.id)
.map_err(|err| TaskError::retry(Duration::from_secs(30), format!("{err:?}")))?;
if tenant.status == TenantStatus::Active {
warn!(
job_id = %job.id,
job_id = %ctx.job_id(),
tenant_id = %tenant.id,
"tenant already active; skipping provisioning"
);
return JobExecution::Success;
return Ok(());
}
if tenant.status != TenantStatus::Creating {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
status = %tenant.status.as_str(),
"tenant not in creating state; skipping provisioning"
);
return JobExecution::Failed {
error: format!(
"tenant status '{}' not eligible for provisioning",
tenant.status.as_str()
),
};
return Err(TaskError::fail(format!(
"tenant status '{}' not eligible for provisioning",
tenant.status.as_str()
)));
}
let endpoint = match &state.config.quickwit_endpoint {
Some(endpoint) => endpoint.trim_end_matches('/').to_owned(),
None => {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
"quickwit endpoint not configured; retrying"
);
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "quickwit endpoint not configured".into(),
};
}
};
let endpoint = ctx
.state
.config
.quickwit_endpoint
.as_ref()
.map(|value| value.trim_end_matches('/').to_owned())
.ok_or_else(|| {
TaskError::retry(Duration::from_secs(30), "quickwit endpoint not configured")
})?;
let index_id = tenant
.quickwit_index
@@ -119,117 +167,69 @@ impl JobHandler for ProvisionTenantJob {
.unwrap_or_else(|| format!("documents-{}", tenant.id));
let client = Client::new();
if let Err(err) = ensure_quickwit_index(&client, &endpoint, &index_id).await {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
error = %err,
"failed to ensure quickwit index"
);
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: err.to_string(),
};
}
ensure_quickwit_index(&client, &endpoint, &index_id)
.await
.map_err(|err| TaskError::retry(Duration::from_secs(30), err.to_string()))?;
let owner_capability_set_id =
match ensure_capability_set(&mut conn, tenant.id, owner_capabilities()) {
Ok(set) => set.id,
Err(err) => {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
error = ?err,
"failed to ensure owner capability set during provisioning"
);
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "owner capability set unavailable".into(),
};
}
ensure_capability_set(&mut conn, tenant.id, owner_capabilities())
.map_err(|_| {
TaskError::retry(Duration::from_secs(30), "owner capability set unavailable")
})?
.id;
ensure_capability_set(&mut conn, tenant.id, user_capabilities()).map_err(|_| {
TaskError::retry(Duration::from_secs(30), "user capability set unavailable")
})?;
ensure_capability_set(&mut conn, tenant.id, readonly_capabilities()).map_err(|_| {
TaskError::retry(
Duration::from_secs(30),
"readonly capability set unavailable",
)
})?;
ensure_capability_set(&mut conn, tenant.id, webdav_capabilities()).map_err(|_| {
TaskError::retry(Duration::from_secs(30), "webdav capability set unavailable")
})?;
for member in &ctx.members {
let new_membership = NewUserMembership {
id: Uuid::new_v4(),
user_id: *member,
tenant_id: tenant.id,
capability_set_id: Some(owner_capability_set_id),
};
if let Err(err) = ensure_capability_set(&mut conn, tenant.id, user_capabilities()) {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
error = ?err,
"failed to ensure user capability set during provisioning"
);
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "user capability set unavailable".into(),
};
}
if let Err(err) = ensure_capability_set(&mut conn, tenant.id, readonly_capabilities()) {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
error = ?err,
"failed to ensure readonly capability set during provisioning"
);
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "readonly capability set unavailable".into(),
};
}
if let Err(err) = ensure_capability_set(&mut conn, tenant.id, webdav_capabilities()) {
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
error = ?err,
"failed to ensure webdav capability set during provisioning"
);
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: "webdav capability set unavailable".into(),
};
}
if let Some(members) = ProvisionPayload::from_job(&job) {
for member in members {
let new_membership = NewUserMembership {
id: Uuid::new_v4(),
user_id: member,
tenant_id: tenant.id,
capability_set_id: Some(owner_capability_set_id),
};
if let Err(err) = diesel::insert_into(user_memberships::table)
.values(&new_membership)
.on_conflict((user_memberships::user_id, user_memberships::tenant_id))
.do_nothing()
.execute(&mut conn)
{
warn!(
job_id = %job.id,
tenant_id = %tenant.id,
user_id = %member,
error = %err,
"failed to assign initial membership"
);
}
if let Err(err) = diesel::insert_into(user_memberships::table)
.values(&new_membership)
.on_conflict((user_memberships::user_id, user_memberships::tenant_id))
.do_nothing()
.execute(&mut conn)
{
warn!(
job_id = %ctx.job_id(),
tenant_id = %tenant.id,
user_id = %member,
error = %err,
"failed to assign initial membership"
);
}
}
if let Err(err) = diesel::update(tenants::table.find(tenant.id))
diesel::update(tenants::table.find(tenant.id))
.set((
tenants::status.eq(TenantStatus::Active),
tenants::quickwit_index.eq(Some(index_id)),
tenants::updated_at.eq(Utc::now().naive_utc()),
))
.execute(&mut conn)
{
warn!(job_id = %job.id, error = %err, "failed to activate tenant");
return JobExecution::Retry {
delay: std::time::Duration::from_secs(30),
error: format!("failed to update tenant status: {err}"),
};
}
.map_err(|err| {
TaskError::retry(
Duration::from_secs(30),
format!("failed to update tenant status: {err}"),
)
})?;
JobExecution::Success
Ok(())
}
}