feat: gate GPU Service on Kubernetes cluster availability

GPU Service today only schedules on Kubernetes clusters; Docker / cloud
clusters can't host the CRDs. Without any awareness of that, Org
members whose Org has no K8s cluster (and no cluster_access grant on
one) saw a menu they couldn't use and a form that bottomed out with
backend errors.

Two changes lock the UX down:

- Boot probes the caller's cluster list once and stashes
  hasKubernetesCluster in initialState. A new canSeeGpuService
  predicate gates the menu — admins and Org owners always see it
  (they can add the cluster); everyone else only sees it when a
  reachable K8s cluster actually exists.

- The instances page filters its own cluster list to Kubernetes
  before deciding what to show. With nothing reachable we render the
  Deployments-style 'No clusters available. Add a Kubernetes cluster
  to get started.' empty state and hide the create-instance CTA;
  admins and Org owners additionally get the 'Add cluster' button
  that jumps to cluster management.
This commit is contained in:
gitlawr
2026-05-27 13:41:32 +08:00
committed by jialin
parent 099b419e34
commit 6b47bacfac
11 changed files with 109 additions and 9 deletions
+25 -2
View File
@@ -3,6 +3,8 @@ import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
import { setAtomStorage } from '@/atoms/utils';
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
import { COLOR_PRIMARY } from '@/config/theme/constants';
import { queryClusterList } from '@/pages/cluster-management/apis';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
import { GPUStackPluginManager } from '@/plugins/manager';
import { requestConfig } from '@/request-config';
@@ -34,11 +36,28 @@ const checkDefaultPage = async (userInfo: any) => {
}
};
// Probes the caller's cluster list once so access predicates can gate
// GPU Service (Kubernetes-only). Cheap (one list request) and never
// blocks login — any failure just falls back to `undefined`, which
// the predicate treats as "unknown / don't restrict beyond role".
const probeHasKubernetesCluster = async (): Promise<boolean | undefined> => {
try {
const res = await queryClusterList({ page: -1 });
return (res?.items ?? []).some(
(c) => c?.provider === ProviderValueMap.Kubernetes
);
} catch (error) {
console.error('probeHasKubernetesCluster error', error);
return undefined;
}
};
// runtime configuration
export async function getInitialState(): Promise<{
fetchUserInfo: () => Promise<Global.UserInfo>;
currentUser?: Global.UserInfo;
pluginData?: Record<string, any>;
hasKubernetesCluster?: boolean;
}> {
const { location } = history;
@@ -122,12 +141,16 @@ export async function getInitialState(): Promise<{
getAppVersionInfo();
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
const userInfo = await fetchUserInfo();
const [userInfo, hasKubernetesCluster] = await Promise.all([
fetchUserInfo(),
probeHasKubernetesCluster()
]);
checkDefaultPage(userInfo);
return {
fetchUserInfo,
currentUser: userInfo,
pluginData
pluginData,
hasKubernetesCluster
};
}
return {