feat: k8s volume mounts

This commit is contained in:
jialin
2026-04-10 15:17:58 +08:00
committed by jialin
parent aeabdab781
commit f2a4ab2654
11 changed files with 668 additions and 49 deletions
+25 -1
View File
@@ -134,5 +134,29 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
'clusters.volume.mountPath.format': 'Path must start with /',
'clusters.volume.readOnly': 'Read Only',
'clusters.volume.sourceType': 'Source Type',
'clusters.volume.sourceType.hostPath': 'Host Path',
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
'clusters.volume.sourceType.configMap': 'ConfigMap',
'clusters.volume.hostPath.path': 'Host Path',
'clusters.volume.hostPath.type': 'Path Type',
'clusters.volume.hostPath.type.directory': 'Directory',
'clusters.volume.hostPath.type.directoryOrCreate':
'Directory (create if not exists)',
'clusters.volume.hostPath.type.file': 'File',
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
'clusters.volume.hostPath.type.socket': 'Socket',
'clusters.volume.hostPath.type.charDevice': 'Character Device',
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
'clusters.volume.pvc.claimName': 'PVC Name',
'clusters.volume.pvc.readOnly': 'Read Only',
'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount'
};
+25 -1
View File
@@ -134,7 +134,31 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
'clusters.volume.mountPath.format': 'Path must start with /',
'clusters.volume.readOnly': 'Read Only',
'clusters.volume.sourceType': 'Source Type',
'clusters.volume.sourceType.hostPath': 'Host Path',
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
'clusters.volume.sourceType.configMap': 'ConfigMap',
'clusters.volume.hostPath.path': 'Host Path',
'clusters.volume.hostPath.type': 'Path Type',
'clusters.volume.hostPath.type.directory': 'Directory',
'clusters.volume.hostPath.type.directoryOrCreate':
'Directory (create if not exists)',
'clusters.volume.hostPath.type.file': 'File',
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
'clusters.volume.hostPath.type.socket': 'Socket',
'clusters.volume.hostPath.type.charDevice': 'Character Device',
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
'clusters.volume.pvc.claimName': 'PVC Name',
'clusters.volume.pvc.readOnly': 'Read Only',
'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+25 -1
View File
@@ -135,7 +135,31 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
'clusters.volume.mountPath.format': 'Path must start with /',
'clusters.volume.readOnly': 'Read Only',
'clusters.volume.sourceType': 'Source Type',
'clusters.volume.sourceType.hostPath': 'Host Path',
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
'clusters.volume.sourceType.configMap': 'ConfigMap',
'clusters.volume.hostPath.path': 'Host Path',
'clusters.volume.hostPath.type': 'Path Type',
'clusters.volume.hostPath.type.directory': 'Directory',
'clusters.volume.hostPath.type.directoryOrCreate':
'Directory (create if not exists)',
'clusters.volume.hostPath.type.file': 'File',
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
'clusters.volume.hostPath.type.socket': 'Socket',
'clusters.volume.hostPath.type.charDevice': 'Character Device',
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
'clusters.volume.pvc.claimName': 'PVC Name',
'clusters.volume.pvc.readOnly': 'Read Only',
'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+36 -11
View File
@@ -31,7 +31,7 @@ export default {
'clusters.create.steps.selectProvider': 'Sağlayıcı Seç',
'clusters.create.configBasic': 'Temel Yapılandırma',
'clusters.create.execCommand': 'Komutu Çalıştır',
'clusters.create.supportedGpu': 'Desteklenen GPU\'lar',
'clusters.create.supportedGpu': "Desteklenen GPU'lar",
'clusters.create.skipfornow': 'Şimdilik Atla',
'clusters.create.noImages': 'Kullanılabilir imaj yok',
'clusters.create.noInstanceTypes': 'Kullanılabilir örnek türü yok',
@@ -70,16 +70,16 @@ export default {
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
'clusters.addworker.runCommand': 'Komutu Çalıştır',
'clusters.addworker.specifyWorkerIP': 'İşçi Düğüm IP\'si',
'clusters.addworker.detectWorkerIP': 'İşçi Düğüm IP\'sini Otomatik Algıla',
'clusters.addworker.specifyWorkerIP': "İşçi Düğüm IP'si",
'clusters.addworker.detectWorkerIP': "İşçi Düğüm IP'sini Otomatik Algıla",
'clusters.addworker.specifyWorkerAddress': 'İşçi Düğüm Harici Adresi',
'clusters.addworker.detectWorkerAddress': 'İşçi Düğüm Harici Adresi',
'clusters.addworker.detectWorkerAddress.tips':
'Belirtilmezse İşçi Düğüm IP\'si varsayılır.',
"Belirtilmezse İşçi Düğüm IP'si varsayılır.",
'clusters.addworker.externalIP.tips':
'VPC veya özel ağda çalıştırılıyorsa, lütfen GPUStack Sunucusuna erişilebilir İşçi Düğüm harici adresini belirtin.',
'clusters.addworker.enterWorkerIP': 'İşçi düğüm IP\'sini girin',
'clusters.addworker.enterWorkerIP.error': 'Lütfen işçi düğüm IP\'sini girin.',
'clusters.addworker.enterWorkerIP': "İşçi düğüm IP'sini girin",
'clusters.addworker.enterWorkerIP.error': "Lütfen işçi düğüm IP'sini girin.",
'clusters.addworker.enterWorkerAddress': 'İşçi düğüm harici adresini girin',
'clusters.addworker.enterWorkerAddress.error':
'Lütfen işçi düğüm harici adresini girin.',
@@ -89,7 +89,7 @@ export default {
'Model önbellek dizinini özelleştirmek istiryorsanız, bağlamak için yolu belirtebilirsiniz.',
'clusters.addworker.configSummary': 'Yapılandırma Özeti',
'clusters.addworker.gpuVendor': 'GPU Üreticisi',
'clusters.addworker.workerIP': 'İşçi Düğüm IP\'si',
'clusters.addworker.workerIP': "İşçi Düğüm IP'si",
'clusters.addworker.workerExternalIP': 'İşçi Düğüm Harici Adresi',
'clusters.addworker.notSpecified': 'Belirtilmedi',
'clusters.addworker.autoDetect': 'Otomatik',
@@ -105,7 +105,7 @@ export default {
'{count} yeni işçi düğüm kümeye eklendi.',
'clusters.addworker.message.success_multiple':
'{count} yeni işçi düğüm kümeye eklendi.',
'clusters.create.serverUrl': 'GPUStack Sunucu URL\'si',
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
'clusters.addworker.containerName.tips':
@@ -116,7 +116,7 @@ export default {
'clusters.table.ip.internal': 'Dahili',
'clusters.table.ip.external': 'Harici',
'clusters.form.serverUrl.tips':
'İşçi düğüm GPUStack Sunucusuna doğrudan erişemiyorsa, harici olarak erişilebilir bir GPUStack hizmet URL\'si belirtin.',
"İşçi düğüm GPUStack Sunucusuna doğrudan erişemiyorsa, harici olarak erişilebilir bir GPUStack hizmet URL'si belirtin.",
'clusters.form.setDefault': 'Varsayılan Olarak Ayarla',
'clusters.form.setDefault.tips': 'Dağıtım için varsayılan.',
'clusters.addworker.noClusters': 'Kullanılabilir Docker kümesi bulunamadı',
@@ -126,7 +126,8 @@ export default {
'clusters.create.dockerTips1': 'Sonraki adım, bu kümeye işçi düğüm ekleyin.',
'clusters.create.dockerTips2':
'Bu adımı atlayabilir ve daha sonra küme listesinden ekleyebilirsiniz.',
'clusters.create.k8sTips1': 'Sonraki adım, mevcut Kubernetes kümesini kaydedin.',
'clusters.create.k8sTips1':
'Sonraki adım, mevcut Kubernetes kümesini kaydedin.',
'clusters.create.k8sTips2':
'Bu adımı atlayabilir ve daha sonra küme listesinden kaydedebilirsiniz.',
'clusters.addworker.theadNotes':
@@ -134,5 +135,29 @@ export default {
'clusters.addworker.theadNotes-02':
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
'clusters.addworker.nvidiaNotes':
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.'
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.',
'clusters.volume.title': 'Volume Mounts',
'clusters.volume.name': 'Volume Name',
'clusters.volume.mountPath': 'Container Path',
'clusters.volume.mountPath.format': 'Path must start with /',
'clusters.volume.readOnly': 'Read Only',
'clusters.volume.sourceType': 'Source Type',
'clusters.volume.sourceType.hostPath': 'Host Path',
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
'clusters.volume.sourceType.configMap': 'ConfigMap',
'clusters.volume.hostPath.path': 'Host Path',
'clusters.volume.hostPath.type': 'Path Type',
'clusters.volume.hostPath.type.directory': 'Directory',
'clusters.volume.hostPath.type.directoryOrCreate':
'Directory (create if not exists)',
'clusters.volume.hostPath.type.file': 'File',
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
'clusters.volume.hostPath.type.socket': 'Socket',
'clusters.volume.hostPath.type.charDevice': 'Character Device',
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
'clusters.volume.pvc.claimName': 'PVC Name',
'clusters.volume.pvc.readOnly': 'Read Only',
'clusters.volume.configMap.name': 'ConfigMap Name',
'clusters.volume.configMap.optional': 'Optional',
'clusters.volume.add': 'Add Volume Mount'
};
+24 -1
View File
@@ -128,5 +128,28 @@ export default {
'clusters.addworker.theadNotes-02':
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
'clusters.addworker.nvidiaNotes':
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。'
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。',
'clusters.volume.title': '卷挂载',
'clusters.volume.name': '卷名称',
'clusters.volume.mountPath': '容器内路径',
'clusters.volume.mountPath.format': '路径必须以 / 开头',
'clusters.volume.readOnly': '只读',
'clusters.volume.sourceType': '存储来源',
'clusters.volume.sourceType.hostPath': '主机路径',
'clusters.volume.sourceType.pvc': '存储卷声明(PVC',
'clusters.volume.sourceType.configMap': '配置映射(ConfigMap',
'clusters.volume.hostPath.path': '宿主机路径',
'clusters.volume.hostPath.type': '路径类型',
'clusters.volume.hostPath.type.directory': '目录',
'clusters.volume.hostPath.type.directoryOrCreate': '目录(不存在则创建)',
'clusters.volume.hostPath.type.file': '文件',
'clusters.volume.hostPath.type.fileOrCreate': '文件(不存在则创建)',
'clusters.volume.hostPath.type.socket': '套接字',
'clusters.volume.hostPath.type.charDevice': '字符设备',
'clusters.volume.hostPath.type.blockDevice': '块设备',
'clusters.volume.pvc.claimName': 'PVC 名称',
'clusters.volume.pvc.readOnly': '只读',
'clusters.volume.configMap.name': '配置名称',
'clusters.volume.configMap.optional': '可选',
'clusters.volume.add': '添加卷挂载'
};
+1 -1
View File
@@ -259,7 +259,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
<div
key={name}
style={{
borderRadius: 'var(--ant-border-radius)',
borderRadius: 'var(--ant-border-radius-lg)',
border: '1px solid var(--ant-color-split)'
}}
>
@@ -14,6 +14,7 @@ import {
} from '../config/types';
import AdvanceConfig from '../step-forms/advance-config';
import CloudProvider from './cloud-provider-form';
import K8SVolumeMount from './k8s-volume-mount';
type AddModalProps = {
action: PageActionType;
@@ -60,7 +61,17 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
useEffect(() => {
if (currentData) {
form.setFieldsValue(currentData);
const volumeMounts = currentData?.k8s_volume_mounts || [];
const realVolumeList = (volumeMounts || []).map(
(item: any, index: number) => ({
...item,
sourceType: Object.keys(item.volumeSource || {})[0] || 'hostPath'
})
);
form.setFieldsValue({
...currentData,
k8s_volume_mounts: realVolumeList
});
}
}, [currentData]);
@@ -151,6 +162,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
credentialList={credentialList}
></CloudProvider>
)}
<Form.Item<FormData>
name="description"
rules={[{ required: false }]}
@@ -161,6 +173,12 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
label={intl.formatMessage({ id: 'common.table.description' })}
></SealTextArea>
</Form.Item>
{provider === ProviderValueMap.Kubernetes && (
<K8SVolumeMount
action={action}
currentData={currentData}
></K8SVolumeMount>
)}
<CollapsePanel
accordion={false}
activeKey={activeKey}
@@ -0,0 +1,432 @@
import CollapsibleContainer from '@/components/collapse-container';
import SealCheckbox from '@/components/seal-form/seal-checkbox';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import SealSwitch from '@/components/seal-form/seal-switch';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Flex, Form } from 'antd';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { hostTypeOptions, sourceTypeOptions } from '../config';
const Label = styled.span`
display: flex;
align-items: center;
gap: 4px;
color: var(--ant-color-text-secondary);
`;
const Title = styled.div`
position: sticky;
top: -16px;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--ant-color-bg-container);
font-weight: 600;
padding-top: 8px;
padding-bottom: 8px;
`;
const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
const form = Form.useFormInstance();
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const [collapseKey, setCollapseKey] = useState<Set<number | string>>(
new Set([0])
);
const volumeList = [
{
name: 'volume-1',
mountPath: '',
readOnly: false,
sourceType: 'hostPath',
volumeSource: {
hostPath: {
path: '',
type: 'DirectoryOrCreate'
}
}
}
];
useEffect(() => {
if (action === PageAction.CREATE) {
form.setFieldValue('k8s_volume_mounts', volumeList);
}
}, [action]);
const onToggle = (open: boolean, key: number) => {
setCollapseKey(open ? new Set([key]) : new Set());
};
const handleAdd = async () => {
try {
await form.validateFields(['k8s_volume_mounts'], {
recursive: true
});
const list = form.getFieldValue('k8s_volume_mounts') || [];
form.setFieldValue('k8s_volume_mounts', [
...list,
{
name: `volume-${list.length + 1}`,
mountPath: '',
readOnly: false,
sourceType: 'hostPath',
volumeSource: {
hostPath: {
path: '',
type: 'DirectoryOrCreate'
}
}
}
]);
setTimeout(() => {
setCollapseKey(new Set([list.length]));
}, 100);
} catch (e: any) {
const errorIndex = e?.errorFields?.[0]?.name?.[1];
if (typeof errorIndex === 'number') {
setCollapseKey(new Set([errorIndex]));
}
}
};
const handleSourceChange = (value: string, index: number) => {
const list = form.getFieldValue('k8s_volume_mounts') || [];
const updated = list.map((item: any, i: number) => {
if (i !== index) return item;
let volumeSource: any = {};
if (value === 'hostPath') {
volumeSource = {
hostPath: { path: '', type: 'DirectoryOrCreate' }
};
} else if (value === 'pvc') {
volumeSource = {
persistentVolumeClaim: { claimName: '', readOnly: false }
};
} else if (value === 'configMap') {
volumeSource = {
configMap: { name: '', optional: false }
};
}
return {
...item,
sourceType: value,
volumeSource
};
});
form.setFieldValue('k8s_volume_mounts', updated);
};
return (
<>
<Title>
<div className="flex-center gap-8">
<span>{intl.formatMessage({ id: 'clusters.volume.title' })}</span>
<Button type="link" onClick={handleAdd}>
<PlusOutlined /> {intl.formatMessage({ id: 'clusters.volume.add' })}
</Button>
</div>
</Title>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
marginBottom: '24px'
}}
>
<Form.List name="k8s_volume_mounts">
{(fields, { remove }) => {
const list = form.getFieldValue('k8s_volume_mounts') || [];
return fields.map(({ name }) => {
const item = list[name] || {};
return (
<div
key={name}
style={{
border: '1px solid var(--ant-color-split)',
borderRadius: 'var(--ant-border-radius-lg)'
}}
>
<CollapsibleContainer
collapsible={true}
showExpandIcon={true}
open={collapseKey.has(name)}
onToggle={(open) => onToggle(open, name)}
styles={{
body: collapseKey.has(name) ? { padding: 16 } : {},
content: { paddingTop: 0 },
header: {
backgroundColor: 'unset'
}
}}
title={
<Label>
<span>
{intl.formatMessage({ id: 'clusters.volume.name' })}:
</span>
<span>{list[name]?.name}</span>
</Label>
}
right={
<Button
size="small"
shape="circle"
onClick={() => remove(name)}
>
<MinusOutlined />
</Button>
}
>
<Form.Item
name={[name, 'name']}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'clusters.volume.name'
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'clusters.volume.name'
})}
required
></SealInput.Input>
</Form.Item>
<Flex style={{ gap: 16, width: '100%' }}>
{/* Mount Path */}
<div style={{ flex: 1 }}>
<Form.Item
name={[name, 'mountPath']}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'clusters.volume.mountPath'
)
}
]}
>
<SealInput.Input
required
placeholder={intl.formatMessage({
id: 'clusters.volume.mountPath.format'
})}
label={intl.formatMessage({
id: 'clusters.volume.mountPath'
})}
></SealInput.Input>
</Form.Item>
</div>
{/* ReadOnly */}
<Form.Item
style={{ width: 240 }}
name={[name, 'readOnly']}
valuePropName="checked"
>
<SealSwitch
label={intl.formatMessage({
id: 'clusters.volume.readOnly'
})}
/>
</Form.Item>
</Flex>
{/* Source Type */}
<Form.Item
name={[name, 'sourceType']}
rules={[
{
required: true,
message: getRuleMessage(
'select',
'clusters.volume.sourceType'
)
}
]}
>
<SealSelect
required
label={intl.formatMessage({
id: 'clusters.volume.sourceType'
})}
onChange={(value) => handleSourceChange(value, name)}
options={sourceTypeOptions}
value={item.sourceType}
></SealSelect>
</Form.Item>
{/* Dynamic Source Form */}
{item.sourceType === 'hostPath' && (
<Flex style={{ gap: 16 }}>
<div style={{ flex: 1 }}>
<Form.Item
name={[name, 'volumeSource', 'hostPath', 'path']}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'clusters.volume.hostPath.path'
)
}
]}
>
<SealInput.Input
required
placeholder={intl.formatMessage({
id: 'clusters.volume.mountPath.format'
})}
label={intl.formatMessage({
id: 'clusters.volume.sourceType.hostPath'
})}
></SealInput.Input>
</Form.Item>
</div>
<Form.Item
style={{ width: 240 }}
name={[name, 'volumeSource', 'hostPath', 'type']}
rules={[
{
required: true,
message: getRuleMessage(
'select',
'clusters.volume.hostPath.type'
)
}
]}
>
<SealSelect
required
options={hostTypeOptions}
label={intl.formatMessage({
id: 'clusters.volume.hostPath.type'
})}
></SealSelect>
</Form.Item>
</Flex>
)}
{item.sourceType === 'pvc' && (
<Flex style={{ gap: 16 }}>
<div style={{ flex: 1 }}>
<Form.Item
name={[
name,
'volumeSource',
'persistentVolumeClaim',
'claimName'
]}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'clusters.volume.pvc.claimName'
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'clusters.volume.pvc.claimName'
})}
required
/>
</Form.Item>
</div>
<Form.Item
style={{ width: 240 }}
name={[
name,
'volumeSource',
'persistentVolumeClaim',
'readOnly'
]}
valuePropName="checked"
>
<SealSwitch
label={intl.formatMessage({
id: 'clusters.volume.pvc.readOnly'
})}
/>
</Form.Item>
</Flex>
)}
{item.sourceType === 'configMap' && (
<Flex style={{ gap: 16 }}>
<div style={{ flex: 1 }}>
<Form.Item
name={[name, 'volumeSource', 'configMap', 'name']}
rules={[
{
required: true,
message: getRuleMessage(
'input',
'clusters.volume.configMap.name'
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'clusters.volume.configMap.name'
})}
required
/>
</Form.Item>
</div>
<Form.Item
style={{ width: 240 }}
name={[name, 'volumeSource', 'configMap', 'optional']}
valuePropName="checked"
>
<SealCheckbox
label={intl.formatMessage({
id: 'clusters.volume.configMap.optional'
})}
/>
</Form.Item>
</Flex>
)}
</CollapsibleContainer>
</div>
);
});
}}
</Form.List>
</div>
</>
);
};
export default VolumeMountsForm;
@@ -97,3 +97,55 @@ export const CloudOptionItems = [
key: 'volumes'
}
];
export const hostTypeOptions = [
{
label: 'clusters.volume.hostPath.type.directory',
locale: true,
value: 'Directory'
},
{
label: 'clusters.volume.hostPath.type.directoryOrCreate',
locale: true,
value: 'DirectoryOrCreate'
},
{
label: 'clusters.volume.hostPath.type.file',
locale: true,
value: 'File'
},
{
label: 'clusters.volume.hostPath.type.fileOrCreate',
locale: true,
value: 'FileOrCreate'
},
{
label: 'clusters.volume.hostPath.type.socket',
locale: true,
value: 'Socket'
},
{
label: 'clusters.volume.hostPath.type.charDevice',
locale: true,
value: 'CharDevice'
},
{
label: 'clusters.volume.hostPath.type.blockDevice',
locale: true,
value: 'BlockDevice'
}
];
export const sourceTypeOptions = [
{
label: 'clusters.volume.sourceType.hostPath',
locale: true,
value: 'hostPath'
},
{ label: 'clusters.volume.sourceType.pvc', locale: true, value: 'pvc' },
{
label: 'clusters.volume.sourceType.configMap',
locale: true,
value: 'configMap'
}
];
@@ -48,6 +48,25 @@ export interface NodePoolListItem extends NodePoolFormData {
updated_at: string;
cluster_id: number;
}
export interface VolumeMount {
name: string;
mountPath: string;
readOnly: boolean;
volumeSource: {
hostPath: {
path: string;
type: string;
};
persistentVolumeClaim: {
claimName: string;
readOnly: boolean;
};
configMap: {
name: string;
optional: boolean;
};
};
}
export interface ClusterListItem {
name: string;
@@ -68,6 +87,7 @@ export interface ClusterListItem {
state: ClusterStatusType;
state_message: string;
worker_pools: NodePoolListItem[];
k8s_volume_mounts?: VolumeMount[];
}
export interface ClusterFormData {
@@ -81,6 +101,7 @@ export interface ClusterFormData {
server_url?: string;
worker_config?: Record<string, any>;
worker_pools?: NodePoolFormData[];
k8s_volume_mounts?: VolumeMount[];
}
export interface SystemConfig {
@@ -1,20 +1,8 @@
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import styled from 'styled-components';
import ClusterForm from '../components/cluster-form';
import { ProviderType } from '../config';
const Title = styled.span`
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
font-size: 14px;
.text {
font-size: 20px;
}
`;
interface BasicFormProps {
provider: ProviderType;
action: PageActionType;
@@ -23,7 +11,6 @@ interface BasicFormProps {
}
const BasicForm = forwardRef((props: BasicFormProps, ref) => {
const intl = useIntl();
const { provider, credentialList, action, currentData } = props;
const formRef = useRef<any>(null);
@@ -38,25 +25,14 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
}));
return (
<div>
{/* <PageTools
marginBottom={16}
left={
<Title>
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
</Title>
}
marginTop={0}
></PageTools> */}
<ClusterForm
provider={provider}
action={action}
ref={formRef}
credentialList={credentialList}
onFinish={handleOnFinish}
currentData={currentData}
></ClusterForm>
</div>
<ClusterForm
provider={provider}
action={action}
ref={formRef}
credentialList={credentialList}
onFinish={handleOnFinish}
currentData={currentData}
></ClusterForm>
);
});