From ded270ff255b81bc40786827edb0a396723b8896 Mon Sep 17 00:00:00 2001 From: Yuxing Deng Date: Wed, 10 Jun 2026 11:53:13 +0800 Subject: [PATCH] feat: support deploying cpu workers for k8s cluster --- src/locales/en-US/clusters.ts | 4 ++ src/locales/ja-JP/clusters.ts | 4 ++ src/locales/ru-RU/clusters.ts | 4 ++ src/locales/tr-TR/clusters.ts | 4 ++ src/locales/zh-CN/clusters.ts | 4 ++ .../components/add-worker/add-worker-step.tsx | 24 +++++--- .../add-worker/check-environment.tsx | 57 ++++++++++++------- .../components/add-worker/select-vendor.tsx | 15 ++--- .../components/check-env-command.tsx | 13 ++++- .../components/provider-catalog.tsx | 31 ++++++++++ .../components/support-gpus.tsx | 2 + 11 files changed, 124 insertions(+), 38 deletions(-) diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index b217004e..fe9b6d49 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -70,7 +70,11 @@ export default { 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', 'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU.multiTag': 'Multi-select', + 'clusters.addworker.selectGPU.subtitle': + 'You can select multiple GPU Vendors or none for CPU-only clusters', 'clusters.addworker.checkEnv': 'Check Environment', + 'clusters.addworker.checkEnv.cpuOnlyTips': + 'Use the following command to verify that the Kubernetes cluster has at least one ready node. You are registering a CPU-only cluster.', 'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.dtkVersion': 'DTK Version', 'clusters.addworker.runCommand': 'Run Command', diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index 60a668c7..098dd2af 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -70,7 +70,11 @@ export default { 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', 'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU.multiTag': 'Multi-select', + 'clusters.addworker.selectGPU.subtitle': + '複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です', 'clusters.addworker.checkEnv': 'Check Environment', + 'clusters.addworker.checkEnv.cpuOnlyTips': + '以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。', 'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.dtkVersion': 'DTK バージョン', 'clusters.addworker.runCommand': 'Run Command', diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index aed4a17e..9b5f28e4 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -70,7 +70,11 @@ export default { 'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.', 'clusters.addworker.selectGPU': 'Выбрать производителя GPU', 'clusters.addworker.selectGPU.multiTag': 'Multi-select', + 'clusters.addworker.selectGPU.subtitle': + 'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU', 'clusters.addworker.checkEnv': 'Проверить окружение', + 'clusters.addworker.checkEnv.cpuOnlyTips': + 'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.', 'clusters.addworker.specifyArgs': 'Указать аргументы', 'clusters.addworker.dtkVersion': 'Версия DTK', 'clusters.addworker.runCommand': 'Выполнить команду', diff --git a/src/locales/tr-TR/clusters.ts b/src/locales/tr-TR/clusters.ts index f5f84ddc..676726eb 100644 --- a/src/locales/tr-TR/clusters.ts +++ b/src/locales/tr-TR/clusters.ts @@ -70,7 +70,11 @@ export default { 'Docker dışı kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.', 'clusters.addworker.selectGPU': 'GPU Üreticisi Seç', 'clusters.addworker.selectGPU.multiTag': 'Multi-select', + 'clusters.addworker.selectGPU.subtitle': + 'Birden fazla GPU Üreticisi seçebilir veya yalnızca CPU kümeleri için hiçbirini seçmeyebilirsiniz', 'clusters.addworker.checkEnv': 'Ortamı Kontrol Et', + 'clusters.addworker.checkEnv.cpuOnlyTips': + 'Kubernetes kümesinde en az bir hazır düğüm olduğunu doğrulamak için aşağıdaki komutu kullanın. Yalnızca CPU kümelerini kaydediyorsunuz.', 'clusters.addworker.specifyArgs': 'Argümanları Belirle', 'clusters.addworker.dtkVersion': 'DTK Sürümü', 'clusters.addworker.runCommand': 'Komutu Çalıştır', diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index d12d070b..7afe3698 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -68,7 +68,11 @@ export default { '非 Docker 集群请前往集群页面注册集群或管理节点池。', 'clusters.addworker.selectGPU': '选择 GPU 厂商', 'clusters.addworker.selectGPU.multiTag': '可多选', + 'clusters.addworker.selectGPU.subtitle': + '可选择多个 GPU 厂商,或不选择以用于仅 CPU 的集群', 'clusters.addworker.checkEnv': '检查环境', + 'clusters.addworker.checkEnv.cpuOnlyTips': + '使用以下命令验证 Kubernetes 集群中至少有一个就绪节点。你正在注册仅 CPU 的集群。', 'clusters.addworker.specifyArgs': '指定参数', 'clusters.addworker.dtkVersion': 'DTK 版本', 'clusters.addworker.runCommand': '运行指令', diff --git a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx index 5445e0d8..e8e6e436 100644 --- a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx +++ b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx @@ -99,24 +99,30 @@ const AddWorkerSteps: React.FC = (props) => { // Downstream steps (check env, run command, ...) only make sense after a // GPU vendor has been chosen. If the user toggled off every vendor in // multi-select, gate them shut so the wrong panel can't be opened. + // Exception: for K8s clusters, Run Command is always accessible (CPU-only + // workers don't need a GPU vendor). const selectedGPUs = (summary.get('selectedGPUs') as string[] | undefined) || []; const currentGPU = (summary.get('currentGPU') as string | undefined) || ''; const noVendorSelected = !currentGPU && selectedGPUs.length === 0; - const downstreamDisabled = disabled || noVendorSelected; + const isK8s = provider === ProviderValueMap.Kubernetes; + // For non-K8s providers, all downstream steps are gated by vendor selection. + // For K8s, both CheckEnv and RunCommand are always accessible — CheckEnv + // shows a ready-nodes check when no vendor is selected (CPU-only workers). + const downstreamDisabled = disabled || (noVendorSelected && !isK8s); React.useEffect(() => { - // If the user just deselected everything, collapse any downstream - // panel back to the GPU step so they aren't left looking at a stale - // disabled-but-open command. Functional updater so we don't have to - // depend on `collapseKey` and re-run the effect on every toggle. - if (!noVendorSelected) return; + // If the user just deselected everything on a non-K8s provider, collapse + // any downstream panel back to the GPU step so they aren't left looking at + // a stale disabled-but-open command. For K8s, CheckEnv stays open with a + // ready-nodes check, so no collapsing needed. + if (!noVendorSelected || isK8s) return; setCollapseKey((prev) => prev.has(StepNamesMap.SelectGPU) ? prev : new Set([StepNamesMap.SelectGPU]) ); - }, [noVendorSelected]); + }, [noVendorSelected, isK8s]); return ( = (props) => { !stepList.includes(StepNamesMap.SelectCluster)) && ( <> - + {(isK8s || !noVendorSelected) && ( + + )} {provider === ProviderValueMap.Kubernetes && ( diff --git a/src/pages/cluster-management/components/add-worker/check-environment.tsx b/src/pages/cluster-management/components/add-worker/check-environment.tsx index f8f36844..1c1ec6ec 100644 --- a/src/pages/cluster-management/components/add-worker/check-environment.tsx +++ b/src/pages/cluster-management/components/add-worker/check-environment.tsx @@ -1,6 +1,7 @@ import { BulbOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Alert, Typography } from 'antd'; +import { ProviderValueMap } from '../../config'; import CheckEnvCommand from '../check-env-command'; import { useAddWorkerContext } from './add-worker-context'; import { AddWorkerStepProps, StepNamesMap } from './config'; @@ -10,8 +11,8 @@ import StepCollapse from './step-collapse'; const CheckEnvironment: React.FC = ({ disabled }) => { const { stepList, summary, provider } = useAddWorkerContext(); const intl = useIntl(); - const currentGPU = summary.get('currentGPU'); - const currentGPUs: string[] = summary.get('selectedGPUs') || []; + const currentGPU = summary.get('currentGPU') as string; + const currentGPUs: string[] = (summary.get('selectedGPUs') as string[]) || []; const workerCommand = summary.get('workerCommand') || { label: '', link: '', @@ -19,6 +20,8 @@ const CheckEnvironment: React.FC = ({ disabled }) => { }; const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1; + const isK8s = provider === ProviderValueMap.Kubernetes; + const isCPUOnly = isK8s && !currentGPU && currentGPUs.length === 0; return ( = ({ disabled }) => { } > - } - style={{ - marginBottom: 8 - }} - title={ - + {intl.formatMessage({ + id: 'clusters.addworker.checkEnv.cpuOnlyTips' + })} + + ) : ( + <> + } + style={{ + marginBottom: 8 }} - > - } - > - - {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })} - + title={ + + } + > + + {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })} + + + )} = ({ disabled }) => { }; useEffect(() => { - // init a default selection + // K8s clusters allow selecting none (CPU-only workers), so skip default + // selection. Other providers still default to NVIDIA. + if (multiCapable) return; handleSelect(GPUDriverMap.NVIDIA, { label: 'NVIDIA', hiddenTitle: true, @@ -116,18 +117,18 @@ const SelectVendor: React.FC = ({ disabled }) => { {stepIndex}.{' '} {intl.formatMessage({ id: 'clusters.addworker.selectGPU' })} {multiCapable && ( - {intl.formatMessage({ - id: 'clusters.addworker.selectGPU.multiTag' + id: 'clusters.addworker.selectGPU.subtitle' })} - + )} } diff --git a/src/pages/cluster-management/components/check-env-command.tsx b/src/pages/cluster-management/components/check-env-command.tsx index 379ef787..2d97560b 100644 --- a/src/pages/cluster-management/components/check-env-command.tsx +++ b/src/pages/cluster-management/components/check-env-command.tsx @@ -1,7 +1,12 @@ import { addWorkerGuide } from '@/pages/resources/config'; import { HighlightCode } from '@gpustack/core-ui'; import React from 'react'; -import { ProviderType } from '../config'; +import { ProviderType, ProviderValueMap } from '../config'; + +// CPU-only (no GPU vendor selected) check for Kubernetes: verify at least one +// ready node exists in the target cluster. +const K8S_CPU_ONLY_CHECK = + 'kubectl get nodes -o jsonpath=\'{.items[*].status.conditions[?(@.type=="Ready")].status}\' 2>/dev/null | grep -q "True" && echo "Ready nodes found. You are registering a CPU-only cluster." || (echo "No ready nodes found"; exit 1)'; type ViewModalProps = { provider: ProviderType; @@ -25,6 +30,12 @@ const AddWorkerCommand: React.FC = ({ : currentGPU ? [currentGPU] : []; + + // CPU-only K8s flow: no GPU vendor selected, check for ready nodes instead. + if (!keys.length && provider === ProviderValueMap.Kubernetes) { + return K8S_CPU_ONLY_CHECK; + } + if (!keys.length) return ''; const lines = keys .map((k) => configs.checkEnvCommand(k)?.[provider || '']) diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx index 49b4a0fe..3b409b96 100644 --- a/src/pages/cluster-management/components/provider-catalog.tsx +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -1,3 +1,4 @@ +import { CheckOutlined } from '@ant-design/icons'; import { IconFont, TemplateCard } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Tooltip } from 'antd'; @@ -62,6 +63,29 @@ const Header = styled.div` const CardBox = styled.div` display: flex; overflow: hidden; + position: relative; +`; + +const ActiveBadge = styled.div` + position: absolute; + right: 0; + bottom: 0; + width: 20px; + height: 20px; + background-color: var(--ant-color-primary); + clip-path: path('M 20 0 L 20 15 Q 20 20 15 20 L 0 20 Z'); + z-index: 1; + display: flex; + align-items: flex-end; + justify-content: flex-end; + pointer-events: none; + + .anticon { + color: #fff; + font-size: 10px; + margin-right: 2px; + margin-bottom: 2px; + } `; interface ProviderCatalogProps { @@ -160,6 +184,13 @@ const ProviderCatalog: React.FC = ({ header={renderTitle(action)} icon={action.icon} > + {(Array.isArray(current) + ? current.includes(action.key) + : current === action.key) && ( + + + + )} ))} diff --git a/src/pages/cluster-management/components/support-gpus.tsx b/src/pages/cluster-management/components/support-gpus.tsx index 30acd5f2..ec5a844a 100644 --- a/src/pages/cluster-management/components/support-gpus.tsx +++ b/src/pages/cluster-management/components/support-gpus.tsx @@ -24,6 +24,7 @@ const Box = styled.div` background-color: rgba(255, 255, 255, 0.1); &.active { background-color: var(--ant-color-fill-tertiary); + border-color: var(--ant-color-primary); } .template-card-icon { margin-right: 0; @@ -57,6 +58,7 @@ const Box = styled.div` } &.active { background-color: var(--ant-color-bg-solid); + border-color: var(--ant-color-primary); } } }