Suspendedremoval display in Frontend
All checks were successful
Build and Push / build (push) Successful in 1m28s
All checks were successful
Build and Push / build (push) Successful in 1m28s
This commit is contained in:
@@ -2,7 +2,10 @@ import { getSessionUser, canMutate } from "@/lib/session";
|
|||||||
import { getTranslations, getFormatter } from "next-intl/server";
|
import { getTranslations, getFormatter } from "next-intl/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { listTenants } from "@/lib/k8s";
|
import { listTenants } from "@/lib/k8s";
|
||||||
import { listActiveTenantRequestsByOrgId } from "@/lib/db";
|
import {
|
||||||
|
listActiveTenantRequestsByOrgId,
|
||||||
|
syncProvisioningStatuses,
|
||||||
|
} from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
listVisibleTenants,
|
listVisibleTenants,
|
||||||
canSeeInflightRequests,
|
canSeeInflightRequests,
|
||||||
@@ -160,6 +163,23 @@ export default async function DashboardPage() {
|
|||||||
|
|
||||||
// Pending/in-flight requests are only shown to roles that can act on
|
// Pending/in-flight requests are only shown to roles that can act on
|
||||||
// them. `user`-role customers see no request cards.
|
// them. `user`-role customers see no request cards.
|
||||||
|
//
|
||||||
|
// syncProvisioningStatuses runs on every dashboard load: it walks
|
||||||
|
// active and provisioning rows and reconciles them against the
|
||||||
|
// current cluster state. Without this, the operator-initiated
|
||||||
|
// 60-day TTL deletion (Bug 37b) leaves the portal showing "Your
|
||||||
|
// assistant is ready!" cards for tenants that no longer exist —
|
||||||
|
// the operator deletes the CR, but the DB row stays at active=true
|
||||||
|
// until something updates it. Running the sync at every dashboard
|
||||||
|
// load keeps the portal eventually consistent with the cluster
|
||||||
|
// without needing a separate cron/job.
|
||||||
|
//
|
||||||
|
// Cost: one K8s GET per row in (active, provisioning) status. At
|
||||||
|
// pilot scale this is small; if it grows we'd cache or move to a
|
||||||
|
// periodic background job.
|
||||||
|
if (canSeeInflightRequests(user)) {
|
||||||
|
await syncProvisioningStatuses();
|
||||||
|
}
|
||||||
const orgRequests = canSeeInflightRequests(user)
|
const orgRequests = canSeeInflightRequests(user)
|
||||||
? await listActiveTenantRequestsByOrgId(user.orgId)
|
? await listActiveTenantRequestsByOrgId(user.orgId)
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -639,8 +639,26 @@ export async function deleteTenantRequest(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync provisioning statuses: for all requests with status "provisioning",
|
* Reconcile the portal's tenant_requests table against actual cluster
|
||||||
* check if the PiecedTenant CR has reached "Ready" and update to "active".
|
* 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):
|
||||||
|
*
|
||||||
|
* 1. provisioning → active: when a tenant CR's phase reaches Ready
|
||||||
|
* or Running, the portal flips the row to active so the
|
||||||
|
* "provisioning…" card transitions into the running tenant view.
|
||||||
|
*
|
||||||
|
* 2. active/provisioning → deleted: when the corresponding CR no
|
||||||
|
* longer exists in the cluster (404), or is mid-deletion (has
|
||||||
|
* metadata.deletionTimestamp set), the row gets flipped to
|
||||||
|
* `deleted`. The DB is otherwise blind to operator-initiated
|
||||||
|
* deletions — when the 60-day TTL fires (Bug 37b) and the
|
||||||
|
* operator deletes a suspended tenant, the portal would happily
|
||||||
|
* keep showing the "Your assistant is ready!" card forever.
|
||||||
|
* Without this reconciliation the dashboard drifts from reality.
|
||||||
|
*
|
||||||
|
* Errors are tolerated per-row: a transient API hiccup on one tenant
|
||||||
|
* shouldn't fail the whole sweep. Skipped rows get retried next call.
|
||||||
*
|
*
|
||||||
* Slice 3 note: with multi-tenant per org, this iterates each row
|
* Slice 3 note: with multi-tenant per org, this iterates each row
|
||||||
* individually (keyed by its own tenant_name), so multiple in-flight
|
* individually (keyed by its own tenant_name), so multiple in-flight
|
||||||
@@ -648,24 +666,47 @@ export async function deleteTenantRequest(id: string): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
export async function syncProvisioningStatuses(): Promise<void> {
|
export async function syncProvisioningStatuses(): Promise<void> {
|
||||||
await ensureSchema();
|
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.
|
||||||
const result = await getPool().query<TenantRequest>(
|
const result = await getPool().query<TenantRequest>(
|
||||||
"SELECT * FROM tenant_requests WHERE status = 'provisioning'"
|
"SELECT * FROM tenant_requests WHERE status IN ('provisioning', 'active') AND tenant_name IS NOT NULL"
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
const mapped = mapRow(row);
|
const mapped = mapRow(row);
|
||||||
if (!mapped.tenantName) continue;
|
if (!mapped.tenantName) continue;
|
||||||
|
|
||||||
|
let tenant: Awaited<ReturnType<typeof getTenant>> = null;
|
||||||
try {
|
try {
|
||||||
const tenant = await getTenant(mapped.tenantName);
|
tenant = await getTenant(mapped.tenantName);
|
||||||
if (
|
|
||||||
tenant?.status?.phase === "Ready" ||
|
|
||||||
tenant?.status?.phase === "Running"
|
|
||||||
) {
|
|
||||||
await updateTenantRequestStatus(mapped.id, "active");
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Tenant might not exist yet — skip
|
// Transient API error — skip this row, retry on next sweep.
|
||||||
|
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'.
|
||||||
|
if (!tenant || tenant.metadata.deletionTimestamp) {
|
||||||
|
await markTenantRequestDeletedByTenantName(mapped.tenantName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CR exists and is healthy. Promote provisioning → active when
|
||||||
|
// the operator reports the tenant has reached steady state.
|
||||||
|
// Keep `active` rows on `active` regardless of phase — a
|
||||||
|
// temporarily-Reconfiguring tenant is still active from the
|
||||||
|
// portal's billing/visibility perspective.
|
||||||
|
if (
|
||||||
|
mapped.status === "provisioning" &&
|
||||||
|
(tenant.status?.phase === "Ready" || tenant.status?.phase === "Running")
|
||||||
|
) {
|
||||||
|
await updateTenantRequestStatus(mapped.id, "active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ export interface PiecedTenant {
|
|||||||
name: string;
|
name: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
creationTimestamp?: string;
|
creationTimestamp?: string;
|
||||||
|
/**
|
||||||
|
* Set by the API server when something issues a Delete on the CR.
|
||||||
|
* The CR continues to exist while finalizers run cleanup; once
|
||||||
|
* they all remove themselves, the API server permanently removes
|
||||||
|
* the CR. Used by the portal's status sync to detect tenants
|
||||||
|
* being torn down — the customer should see "Deleted" rather
|
||||||
|
* than "Ready" while the cleanup runs.
|
||||||
|
*/
|
||||||
|
deletionTimestamp?: string;
|
||||||
labels?: Record<string, string>;
|
labels?: Record<string, string>;
|
||||||
annotations?: Record<string, string>;
|
annotations?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user