Suspendedremoval display in Frontend

This commit is contained in:
2026-05-01 21:48:25 +02:00
parent b12bca8818
commit e331ef6b8f

View File

@@ -63,9 +63,14 @@ const MIGRATION_SQL = `
CREATE INDEX IF NOT EXISTS idx_tenant_requests_status ON tenant_requests(status);
CREATE INDEX IF NOT EXISTS idx_tenant_requests_org_id ON tenant_requests(zitadel_org_id);
CREATE INDEX IF NOT EXISTS idx_tenant_requests_org_status ON tenant_requests(zitadel_org_id, status);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_tenant_requests_tenant_name
ON tenant_requests(tenant_name)
WHERE tenant_name IS NOT NULL;
-- Note: the unique constraint on tenant_name is NOT created here.
-- Pre-Bug-37 we had a non-partial UNIQUE on tenant_name, which is
-- incompatible with resume requests (same tenant_name, different
-- request_type). The new partial unique indexes are created
-- further down in the migration block, after the request_type
-- column has been added and backfilled. This bootstrap section
-- only creates indexes that are safe regardless of request_type
-- semantics.
-- Idempotent column adds for existing databases
ALTER TABLE tenant_requests ADD COLUMN IF NOT EXISTS encrypted_secrets BYTEA;
@@ -640,9 +645,7 @@ export async function deleteTenantRequest(id: string): Promise<void> {
/**
* Reconcile the portal's tenant_requests table against actual cluster
* state. Two passes, both walking only rows with `tenant_name` set
* (rows in pending/rejected/cancelled state don't have one and are
* irrelevant to this reconciliation):
* state. Three passes, walking only rows with `tenant_name` set:
*
* 1. provisioning → active: when a tenant CR's phase reaches Ready
* or Running, the portal flips the row to active so the
@@ -657,6 +660,15 @@ export async function deleteTenantRequest(id: string): Promise<void> {
* keep showing the "Your assistant is ready!" card forever.
* Without this reconciliation the dashboard drifts from reality.
*
* 3. pending resume → cancelled: when a pending resume request's
* tenant is no longer suspended (admin resumed it directly,
* tenant was deleted, or it was never suspended in the first
* place), the request is moot. Flip to 'cancelled' so the
* pending-resume unique index releases for any future genuine
* resume request. We pick `cancelled` over `rejected` because
* the customer didn't do anything wrong — circumstances just
* changed.
*
* Errors are tolerated per-row: a transient API hiccup on one tenant
* shouldn't fail the whole sweep. Skipped rows get retried next call.
*
@@ -666,12 +678,19 @@ export async function deleteTenantRequest(id: string): Promise<void> {
*/
export async function syncProvisioningStatuses(): Promise<void> {
await ensureSchema();
// Pull every row that *might* be reconcilable in one query — the
// status filter narrows to ones whose CR-vs-DB consistency is
// worth checking. Pending/rejected/cancelled rows have no
// tenant_name to compare against; deleted rows are terminal.
// Active+provisioning rows: status reflects "the tenant should
// exist and be running".
// Pending resume rows: status reflects "the tenant is suspended,
// awaiting reactivation".
// Both need cluster-side validation; we fetch them in one query
// and dispatch on (status, request_type).
const result = await getPool().query<TenantRequest>(
"SELECT * FROM tenant_requests WHERE status IN ('provisioning', 'active') AND tenant_name IS NOT NULL"
`SELECT * FROM tenant_requests
WHERE tenant_name IS NOT NULL
AND (
status IN ('provisioning', 'active')
OR (status = 'pending' AND request_type = 'resume')
)`
);
for (const row of result.rows) {
@@ -686,12 +705,36 @@ export async function syncProvisioningStatuses(): Promise<void> {
continue;
}
// CR gone, or mid-deletion. Flip the row to 'deleted'. The
// `markTenantRequestDeletedByTenantName` helper also nulls the
// tenant_name column so any future tenant created with the same
// name (unlikely given UUID-suffixed naming, but possible) won't
// collide with the unique index on (tenant_name) WHERE
// request_type = 'provision'.
// Pending resume request: validity hinges on tenant being suspended.
if (
mapped.status === "pending" &&
mapped.requestType === "resume"
) {
// Tenant doesn't exist or is being deleted: cancel the resume
// request (it can never be fulfilled). Don't fall through to
// the "deleted" branch below — that would also flip the
// provision row, which is the right thing for a CR-level
// deletion but we want this resume row specifically resolved
// here.
if (!tenant || tenant.metadata.deletionTimestamp) {
await updateTenantRequestStatus(mapped.id, "cancelled");
continue;
}
// Tenant is no longer suspended: the request is moot.
// Cancel it (the customer didn't do anything wrong; the
// condition the request was about no longer applies).
if (!tenant.spec.suspend) {
await updateTenantRequestStatus(mapped.id, "cancelled");
continue;
}
// Tenant still suspended, request still relevant. Leave as-is.
continue;
}
// Active or provisioning row: CR gone, or mid-deletion. Flip the
// row to 'deleted'. `markTenantRequestDeletedByTenantName` flips
// every row with this tenant_name (provision + any resume rows),
// which is the right thing for a CR-level deletion.
if (!tenant || tenant.metadata.deletionTimestamp) {
await markTenantRequestDeletedByTenantName(mapped.tenantName);
continue;