From 2dae886fd116b12c30aa573957be759e2cc72232 Mon Sep 17 00:00:00 2001 From: gitlawr Date: Wed, 13 May 2026 11:51:33 +0800 Subject: [PATCH] fix(gpu-service): use gpustack-{slug} namespace for org resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/atoms/user.ts | 73 +++++++++++++++++-- src/pages/cluster-management/config/types.ts | 3 + .../instances/forms/storage-volume.tsx | 7 +- src/pages/gpu-service/instances/index.tsx | 6 +- .../instances/services/use-create-instance.ts | 6 +- .../instances/services/use-update-instance.ts | 6 +- src/pages/gpu-service/storage/index.tsx | 6 +- .../storage/services/use-create-storage.ts | 6 +- .../storage/services/use-query-storage.ts | 6 +- .../storage/services/use-update-storage.ts | 6 +- 10 files changed, 103 insertions(+), 22 deletions(-) diff --git a/src/atoms/user.ts b/src/atoms/user.ts index e75d3067..67370036 100644 --- a/src/atoms/user.ts +++ b/src/atoms/user.ts @@ -28,13 +28,74 @@ export const initialPasswordAtom = atomWithStorage( '' ); -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; }; diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index ab05735e..c587c453 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -88,6 +88,9 @@ export interface ClusterListItem { state_message: string; worker_pools: NodePoolListItem[]; k8s_volume_mounts?: VolumeMount[]; + // Backend ClusterPublic carries this; admin-"All" namespace + // resolution falls back to the cluster's owner Org slug. + owner_principal_id?: number; } export interface ClusterFormData { diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index db15c367..ee867d44 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -1,7 +1,9 @@ -import { getCurrentOrganizationId } from '@/atoms/user'; +import { currentClusterAtom } from '@/atoms/gpuservice'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Button, Flex, Form, Radio } from 'antd'; +import { useAtomValue } from 'jotai'; import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormData as StorageFormData } from '../../storage/config/types'; @@ -29,7 +31,8 @@ const StorageVolume = () => { const form = Form.useFormInstance(); - const namespace = getCurrentOrganizationId(); + const currentCluster = useAtomValue(currentClusterAtom); + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); useEffect(() => { fetchStorage({}); diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index 051ad130..c892f9d6 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { PageAction } from '@/config'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import type { PageActionType } from '@/config/types'; @@ -33,9 +33,11 @@ import useUpdateInstance from './services/use-update-instance'; const GPUService: React.FC = () => { const intl = useIntl(); - const namespace = getCurrentOrganizationId(); const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom); const clusterID = currentCluster?.id; + // In admin "All" view there's no Org context, so the helper falls + // back to the selected cluster's owner Org slug. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const deleteInstance = useCallback( (id: number) => deleteGPUServiceInstance({ namespace, clusterID, id }), diff --git a/src/pages/gpu-service/instances/services/use-create-instance.ts b/src/pages/gpu-service/instances/services/use-create-instance.ts index a3196b10..efefffa8 100644 --- a/src/pages/gpu-service/instances/services/use-create-instance.ts +++ b/src/pages/gpu-service/instances/services/use-create-instance.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -12,9 +12,11 @@ interface CreateInstanceParams { } export default function useCreateInstance() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: CreateInstanceParams, option?: any) => diff --git a/src/pages/gpu-service/instances/services/use-update-instance.ts b/src/pages/gpu-service/instances/services/use-update-instance.ts index 239dd378..00cdcfb9 100644 --- a/src/pages/gpu-service/instances/services/use-update-instance.ts +++ b/src/pages/gpu-service/instances/services/use-update-instance.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -13,9 +13,11 @@ interface UpdateInstanceParams { } export default function useUpdateInstance() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: UpdateInstanceParams, option?: any) => diff --git a/src/pages/gpu-service/storage/index.tsx b/src/pages/gpu-service/storage/index.tsx index 74ce04e7..aa55e0f5 100644 --- a/src/pages/gpu-service/storage/index.tsx +++ b/src/pages/gpu-service/storage/index.tsx @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { PageAction } from '@/config'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import type { PageActionType } from '@/config/types'; @@ -34,9 +34,11 @@ import useUpdateStorage from './services/use-update-storage'; const GPUServiceStorage: React.FC = () => { const intl = useIntl(); - const namespace = getCurrentOrganizationId(); const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view falls back to the cluster's owner Org slug — + // see :func:`getCurrentOrgNamespace`. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const deleteStorage = useCallback( (id: number) => deleteGPUServiceStorage({ namespace, clusterID, id }), diff --git a/src/pages/gpu-service/storage/services/use-create-storage.ts b/src/pages/gpu-service/storage/services/use-create-storage.ts index b36c62b5..745e55ec 100644 --- a/src/pages/gpu-service/storage/services/use-create-storage.ts +++ b/src/pages/gpu-service/storage/services/use-create-storage.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -12,9 +12,11 @@ interface CreateStorageParams { } export default function useCreateStorage() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: CreateStorageParams, option?: any) => diff --git a/src/pages/gpu-service/storage/services/use-query-storage.ts b/src/pages/gpu-service/storage/services/use-query-storage.ts index 70f36d84..c49a868e 100644 --- a/src/pages/gpu-service/storage/services/use-query-storage.ts +++ b/src/pages/gpu-service/storage/services/use-query-storage.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -7,9 +7,11 @@ import { queryGPUServiceStorage } from '../apis'; import { ListItem } from '../config/types'; export default function useQueryStorage() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: Global.K8sSearchParams = {}, options?: any) => diff --git a/src/pages/gpu-service/storage/services/use-update-storage.ts b/src/pages/gpu-service/storage/services/use-update-storage.ts index d3645c23..47941f6d 100644 --- a/src/pages/gpu-service/storage/services/use-update-storage.ts +++ b/src/pages/gpu-service/storage/services/use-update-storage.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -13,9 +13,11 @@ interface UpdateStorageParams { } export default function useUpdateStorage() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: UpdateStorageParams, option?: any) =>