fix(gpu-service): use gpustack-{slug} namespace for org resources
The legacy ``getCurrentOrganizationId`` helper returned the raw numeric
org id from localStorage, which the GPU-service / storage call sites
then used as the K8s namespace path segment — producing requests like
``/v2/clusters/1/proxy/.../namespaces/5/instances``. The backend
creates a namespace named ``gpustack-{slug}`` (matching
``get_namespace_name``), so the request hit a namespace that doesn't
exist.
Replace the helper with ``getCurrentOrgNamespace`` that:
- prefers the Org the caller is acting under (numeric
``currentOrganizationId`` from localStorage, slug looked up in the
persisted ``organizationList`` or ``allOrganizations`` cache);
- falls back to the selected cluster's owner Org slug for the admin
"All" view, where there is no explicit Org context but the resource
still has to land in some Org's namespace — call sites pass
``currentCluster?.owner_principal_id`` through;
- falls back to ``gpustack-default`` only if neither path resolves a
slug (first load before any cache hydrates, etc.).
``ClusterListItem`` is widened with the optional ``owner_principal_id``
field so TypeScript accepts the fallback argument; the backend has
been returning it via ``ClusterPublic`` all along.
This commit is contained in:
+67
-6
@@ -28,13 +28,74 @@ export const initialPasswordAtom = atomWithStorage<string>(
|
||||
''
|
||||
);
|
||||
|
||||
export const getCurrentOrganizationId = (): string => {
|
||||
// Namespace the server creates for an Org's resources on each Kubernetes
|
||||
// cluster. The format must match the backend's ``get_namespace_name``
|
||||
// helper — ``gpustack-{slug}`` — because the GPU-instance / storage CRDs
|
||||
// (worker.gpustack.ai/v1) are namespaced and the server-side admission
|
||||
// keys off this exact name.
|
||||
//
|
||||
// Resolution path:
|
||||
// 1. The Org the caller is currently acting under — the enterprise
|
||||
// plugin persists ``currentOrganizationId`` (numeric) when the user
|
||||
// picks an Org via OrgSwitcher.
|
||||
// 2. The cluster's own owner Org — used in the platform-admin "All"
|
||||
// view, where the caller has no Org context but the resource still
|
||||
// has to land in *some* Org's namespace.
|
||||
// 3. ``gpustack-default`` as a last resort (first load before any
|
||||
// cache is hydrated, or a cluster whose owner Org is missing from
|
||||
// both caches).
|
||||
//
|
||||
// Called outside React (umi page utilities) so it reads localStorage
|
||||
// directly rather than going through a Jotai hook. The org caches are
|
||||
// kept fresh by the enterprise plugin's atomWithStorage atoms, and the
|
||||
// OrgSwitcher reloads the page on switch so we don't need in-process
|
||||
// reactivity here.
|
||||
export const getCurrentOrgNamespace = (
|
||||
clusterOwnerPrincipalId?: number | null
|
||||
): string => {
|
||||
return (
|
||||
lookupOrgNamespace(getStoredCurrentOrgId()) ??
|
||||
lookupOrgNamespace(clusterOwnerPrincipalId ?? null) ??
|
||||
'gpustack-default'
|
||||
);
|
||||
};
|
||||
|
||||
const getStoredCurrentOrgId = (): number | null => {
|
||||
try {
|
||||
const raw = localStorage.getItem('currentOrganizationId');
|
||||
if (raw) {
|
||||
const value = JSON.parse(raw);
|
||||
if (value != null) return String(value);
|
||||
if (!raw) return null;
|
||||
const value = JSON.parse(raw);
|
||||
return typeof value === 'number' ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Org caches the enterprise plugin persists. ``organizationList`` is
|
||||
// the caller's member orgs; ``allOrganizations`` is admin-only (every
|
||||
// Org on the platform) so admin sessions can resolve any owner Org id.
|
||||
// Both are checked because ``currentOrganizationId`` is null in the
|
||||
// admin "All" view but a member org's slug might still cover the
|
||||
// cluster-owner fallback.
|
||||
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
||||
|
||||
const lookupOrgNamespace = (id: number | null): string | null => {
|
||||
if (id == null) return null;
|
||||
// Normalise both sides to strings — the stored id type varies between
|
||||
// localStorage payloads (some writers stringify, others persist as a
|
||||
// JSON number); strict equality would silently miss those cases.
|
||||
const target = String(id);
|
||||
for (const key of ORG_CACHE_KEYS) {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) continue;
|
||||
const list = JSON.parse(raw) as Array<{ id: number; slug?: string }>;
|
||||
if (!Array.isArray(list)) continue;
|
||||
const match = list.find((item) => String(item?.id) === target);
|
||||
if (match?.slug) return `gpustack-${match.slug}`;
|
||||
} catch {
|
||||
// ignore malformed cache; continue checking other keys
|
||||
}
|
||||
} catch {}
|
||||
return 'gpustack-default';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user