- New k8s_pod_spec form sections (image credentials, node selector, gpu vendor overrides) under k8s_options, replacing the legacy flat k8s_volume_mounts list. UI keys aligned to the backend wire shape (snake_case k8s_options + camelCase inside). - System default container registry is pre-filled into the first image credential when creating a new cluster; empty username/password are coerced to null on submit to match the Optional[str] backend schema. - Register cluster flow supports multi-runtime selection gated by the cluster's gpuVendorOverrides: non-override vendors stay single-select with an inline hint; multi-add only opens once an override vendor is picked, and non-override cards become disabled in that state. - Manifest URL emits multiple ?runtime= params; check-env step combines per-vendor commands; downstream steps are disabled when no vendor is selected. - Pre-validate gpuVendorOverrides at save time (non-empty selector, no duplicates across vendors, no key clash with base nodeSelector) so the user sees the error before hitting the manifest endpoint. - Misc: dark-mode background of the k8s_pod_spec / volume mount titles no longer clashes with the drawer; cluster Steps no longer leaks the internal showModules/showForms props to the DOM.
207 lines
6.8 KiB
TypeScript
207 lines
6.8 KiB
TypeScript
import {
|
|
AddWorkerDockerNotes,
|
|
GPUDriverMap,
|
|
GPUsConfigs
|
|
} from '@/pages/resources/config/gpu-driver';
|
|
import { BulbOutlined } from '@ant-design/icons';
|
|
import { useIntl } from '@umijs/max';
|
|
import { Alert, Tag } from 'antd';
|
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { queryClusterItem } from '../../apis';
|
|
import { ProviderValueMap } from '../../config';
|
|
import { ClusterListItem } from '../../config/types';
|
|
import SupportedGPUs from '../support-gpus';
|
|
import { useAddWorkerContext } from './add-worker-context';
|
|
import { AddWorkerStepProps, StepNamesMap } from './config';
|
|
import { Title } from './constainers';
|
|
import StepCollapse from './step-collapse';
|
|
|
|
const buildWorkerCommand = (
|
|
driverKey: string,
|
|
itemHint?: { label?: string; link?: string }
|
|
) => ({
|
|
label: itemHint?.label || GPUsConfigs[driverKey]?.label || driverKey,
|
|
link: itemHint?.link || '',
|
|
notes: AddWorkerDockerNotes[driverKey] || []
|
|
});
|
|
|
|
const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|
const { stepList, registerField, updateField, provider, registrationInfo } =
|
|
useAddWorkerContext();
|
|
const intl = useIntl();
|
|
|
|
const stepIndex = stepList.indexOf(StepNamesMap.SelectGPU) + 1;
|
|
|
|
// Pull the cluster so we know whether gpuVendorOverrides was configured.
|
|
// The K8s register flow gates multi-select on that being present, and
|
|
// restricts available runtimes to its keys.
|
|
const [overrideRuntimes, setOverrideRuntimes] = useState<string[]>([]);
|
|
useEffect(() => {
|
|
const id = registrationInfo?.cluster_id;
|
|
if (!id) {
|
|
// Reset when the cluster context goes away so we don't leak the
|
|
// previous cluster's overrides into the next session.
|
|
setOverrideRuntimes([]);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
queryClusterItem({ id })
|
|
.then((c) => {
|
|
if (cancelled) return;
|
|
const overrides = (c as ClusterListItem)?.k8s_options
|
|
?.gpuVendorOverrides;
|
|
setOverrideRuntimes(overrides ? Object.keys(overrides) : []);
|
|
})
|
|
.catch((err) => {
|
|
if (cancelled) return;
|
|
console.error('Failed to query cluster for vendor overrides:', err);
|
|
setOverrideRuntimes([]);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [registrationInfo?.cluster_id]);
|
|
|
|
// Set of GPU *driver keys* (e.g. "cuda", "cann") that match the cluster's
|
|
// override runtimes. Used to detect when multi-add becomes available.
|
|
const overrideKeys = useMemo(() => {
|
|
const set = new Set<string>();
|
|
if (!overrideRuntimes.length) return set;
|
|
Object.values(GPUsConfigs).forEach((cfg) => {
|
|
if (cfg.gpuVendor && overrideRuntimes.includes(cfg.gpuVendor)) {
|
|
set.add(cfg.value);
|
|
}
|
|
});
|
|
return set;
|
|
}, [overrideRuntimes]);
|
|
|
|
// Multi-add is only meaningful when the cluster has 2+ vendor overrides
|
|
// AND the current selection already includes one. Until both hold, the
|
|
// picker behaves like a single-select (so the user can freely land on
|
|
// any vendor — including ones outside the override list).
|
|
const multiCapable =
|
|
provider === ProviderValueMap.Kubernetes && overrideKeys.size >= 2;
|
|
|
|
const [selectedKeys, setSelectedKeys] = useState<string[]>([
|
|
GPUDriverMap.NVIDIA
|
|
]);
|
|
|
|
const isMultiActive = useMemo(
|
|
() => multiCapable && selectedKeys.some((k) => overrideKeys.has(k)),
|
|
[multiCapable, selectedKeys, overrideKeys]
|
|
);
|
|
|
|
// In multi-active mode, non-override vendors are disabled — picking one
|
|
// would break the backend invariant that a multi-vendor manifest must
|
|
// only target configured runtimes. Otherwise everything stays enabled.
|
|
const availableKeys = isMultiActive ? overrideKeys : undefined;
|
|
|
|
// Cache vendor metadata (label/link from SupportedGPUs items) so we can
|
|
// rebuild workerCommand on toggle without re-clicking the card.
|
|
const itemMetaRef = useRef<Record<string, { label: string; link: string }>>(
|
|
{}
|
|
);
|
|
|
|
// Push current selection into the shared summary so consumers
|
|
// (K8sRunCommand, CheckEnvironment, VendorNotes) can read it.
|
|
useEffect(() => {
|
|
const primary = selectedKeys[0] || '';
|
|
updateField('currentGPU', primary);
|
|
updateField('selectedGPUs', selectedKeys);
|
|
updateField(
|
|
'workerCommand',
|
|
primary ? buildWorkerCommand(primary, itemMetaRef.current[primary]) : null
|
|
);
|
|
}, [selectedKeys]);
|
|
|
|
useEffect(() => {
|
|
const unregister1 = registerField('currentGPU');
|
|
const unregister2 = registerField('workerCommand');
|
|
const unregister3 = registerField('selectedGPUs');
|
|
return () => {
|
|
unregister1();
|
|
unregister2();
|
|
unregister3();
|
|
};
|
|
}, []);
|
|
|
|
const handleSelect = (key: string, item: any) => {
|
|
// Disabled cards are already blocked by TemplateCard; this is a
|
|
// defensive check for the multi-active case (only override runtimes
|
|
// can be added once multi-add is open).
|
|
if (availableKeys && !availableKeys.has(key)) return;
|
|
if (item) {
|
|
itemMetaRef.current[key] = {
|
|
label: item.label,
|
|
link: item.link
|
|
};
|
|
}
|
|
setSelectedKeys((prev) => {
|
|
const has = prev.includes(key);
|
|
if (has) {
|
|
// Clicking a selected card always toggles it off.
|
|
return prev.filter((v) => v !== key);
|
|
}
|
|
// Multi-add only when the current state already includes an override
|
|
// pick. Otherwise (still in single-select land), replace.
|
|
if (isMultiActive) return [...prev, key];
|
|
return [key];
|
|
});
|
|
};
|
|
|
|
// Warn when multi-select is possible on this cluster but the user's
|
|
// current pick lands outside the override list — they're effectively
|
|
// locked into single-select until they switch to an override vendor.
|
|
const showSingleOnlyHint =
|
|
multiCapable &&
|
|
selectedKeys.length > 0 &&
|
|
selectedKeys.every((k) => !overrideKeys.has(k));
|
|
|
|
return (
|
|
<StepCollapse
|
|
disabled={disabled}
|
|
name={StepNamesMap.SelectGPU}
|
|
title={
|
|
<Title>
|
|
{stepIndex}.{' '}
|
|
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
|
|
{multiCapable && (
|
|
<Tag
|
|
color="blue"
|
|
style={{
|
|
marginLeft: 8,
|
|
fontWeight: 400,
|
|
borderRadius: 4
|
|
}}
|
|
>
|
|
{intl.formatMessage({
|
|
id: 'clusters.addworker.selectGPU.multiTag'
|
|
})}
|
|
</Tag>
|
|
)}
|
|
</Title>
|
|
}
|
|
>
|
|
{showSingleOnlyHint && (
|
|
<Alert
|
|
type="info"
|
|
showIcon
|
|
icon={<BulbOutlined />}
|
|
style={{ marginBottom: 8 }}
|
|
message={intl.formatMessage({
|
|
id: 'clusters.addworker.selectGPU.singleOnly'
|
|
})}
|
|
/>
|
|
)}
|
|
<SupportedGPUs
|
|
onSelect={handleSelect}
|
|
current={selectedKeys}
|
|
availableKeys={availableKeys}
|
|
clickable={true}
|
|
/>
|
|
</StepCollapse>
|
|
);
|
|
};
|
|
|
|
export default SelectVendor;
|