fix: add worker, create cluster

This commit is contained in:
jialin
2025-12-17 14:59:07 +08:00
parent 5e48d75ead
commit bd6f4b3f76
14 changed files with 234 additions and 103 deletions
+1
View File
@@ -45,6 +45,7 @@ export function useUpdateChunkedList(options: {
}, 200); }, 200);
}; };
const updateChunkedList = (data: ChunkedCollection) => { const updateChunkedList = (data: ChunkedCollection) => {
console.log('updateChunkedList data:', data);
let collections = data?.collection || []; let collections = data?.collection || [];
if (options?.computedID) { if (options?.computedID) {
collections = collections?.map((item: any) => { collections = collections?.map((item: any) => {
+10 -1
View File
@@ -6,7 +6,8 @@ import {
CredentialFormData, CredentialFormData,
CredentialListItem, CredentialListItem,
NodePoolFormData, NodePoolFormData,
NodePoolListItem NodePoolListItem,
SystemConfig
} from '../config/types'; } from '../config/types';
export const CREDENTIALS_API = '/cloud-credentials'; export const CREDENTIALS_API = '/cloud-credentials';
@@ -19,6 +20,8 @@ export const CLUSTER_TOKEN = 'registration-token';
export const PROVIDER_PROXY_API = '/provider-proxy'; export const PROVIDER_PROXY_API = '/provider-proxy';
export const SYSTEM_CONFIG_API = '/config';
// ============= DigitalOcean start ===================== // ============= DigitalOcean start =====================
export const REGIONS_API = '/v2/regions'; export const REGIONS_API = '/v2/regions';
@@ -189,3 +192,9 @@ export async function deleteWorkerPool(id: number) {
method: 'DELETE' method: 'DELETE'
}); });
} }
export async function querySystemConfig() {
return request<SystemConfig>(`${SYSTEM_CONFIG_API}`, {
method: 'GET'
});
}
@@ -272,7 +272,7 @@ const ClusterCreate = () => {
const breadcrumbItems = [ const breadcrumbItems = [
{ {
title: <a>{intl.formatMessage({ id: 'clusters.title' })}</a>, title: <a>{intl.formatMessage({ id: 'clusters.title' })}</a>,
onClick: () => navigate(-1) onClick: () => handleCancel()
}, },
{ {
title: intl.formatMessage({ id: 'common.button.create' }) title: intl.formatMessage({ id: 'common.button.create' })
@@ -2,6 +2,7 @@ import AlertInfoBlock from '@/components/alert-info/block';
import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message'; import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { ExclamationCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../../config'; import { ProviderType, ProviderValueMap } from '../../config';
@@ -29,6 +30,7 @@ const Container = styled.div`
* clusterList and onClusterChange are only required when from worker page. * clusterList and onClusterChange are only required when from worker page.
*/ */
type AddWorkerProps = { type AddWorkerProps = {
isModal?: boolean;
provider: ProviderType; provider: ProviderType;
clusterList?: Global.BaseOption<number, ClusterListItem>[]; clusterList?: Global.BaseOption<number, ClusterListItem>[];
clusterLoading?: boolean; clusterLoading?: boolean;
@@ -49,6 +51,7 @@ type AddWorkerProps = {
*/ */
const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => { const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
const { const {
isModal = false,
registrationInfo, registrationInfo,
provider, provider,
clusterList, clusterList,
@@ -63,7 +66,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
); );
const startWatchRef = React.useRef(false); const startWatchRef = React.useRef(false);
const { update, summary, register } = useSummaryStatus(); const { update, summary, register } = useSummaryStatus();
const { contextHolder, createModelsChunkRequest } = useAddWorkerMessage({ const { addedCount, createModelsChunkRequest } = useAddWorkerMessage({
startWatch: startWatchRef startWatch: startWatchRef
}); });
@@ -84,8 +87,10 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
}, [stepList]); }, [stepList]);
React.useEffect(() => { React.useEffect(() => {
if (!isModal) {
createModelsChunkRequest(); createModelsChunkRequest();
}, []); }
}, [isModal]);
return ( return (
<AddWorkerContext.Provider <AddWorkerContext.Provider
@@ -139,8 +144,18 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
)} )}
</> </>
)} )}
{addedCount > 0 && (
<Alert
message={intl.formatMessage(
{
id: 'clusters.addworker.message.success'
},
{ count: addedCount }
)}
type="warning"
/>
)}
</Container> </Container>
{contextHolder}
</AddWorkerContext.Provider> </AddWorkerContext.Provider>
); );
}; };
@@ -1,11 +1,22 @@
import ScrollerModal from '@/components/scroller-modal'; import ScrollerModal from '@/components/scroller-modal';
import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message';
import { useIntl } from '@umijs/max';
import { Alert, Button } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import styled from 'styled-components';
import { queryClusterToken } from '../../apis'; import { queryClusterToken } from '../../apis';
import { ProviderType } from '../../config'; import { ProviderType } from '../../config';
import { ClusterListItem } from '../../config/types'; import { ClusterListItem } from '../../config/types';
import AddWorkerStep from './add-worker-step'; import AddWorkerStep from './add-worker-step';
import { StepName } from './config'; import { StepName } from './config';
const Footer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
`;
type AddWorkerProps = { type AddWorkerProps = {
open: boolean; open: boolean;
provider: ProviderType; provider: ProviderType;
@@ -40,6 +51,11 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
clusterLoading, clusterLoading,
stepList = [] stepList = []
} = props || {}; } = props || {};
const intl = useIntl();
const startWatchRef = React.useRef(false);
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage({
startWatch: startWatchRef
});
const firstLoad = React.useRef(true); const firstLoad = React.useRef(true);
const [registrationInfo, setRegistrationInfo] = React.useState<{ const [registrationInfo, setRegistrationInfo] = React.useState<{
token: string; token: string;
@@ -61,6 +77,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
...data, ...data,
cluster_id: value cluster_id: value
}); });
startWatchRef.current = true;
} catch (error) { } catch (error) {
firstLoad.current = false; firstLoad.current = false;
} }
@@ -75,6 +92,12 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
}; };
}, [open, cluster_id]); }, [open, cluster_id]);
useEffect(() => {
if (open) {
createModelsChunkRequest();
}
}, [open]);
return ( return (
<ScrollerModal <ScrollerModal
title={title} title={title}
@@ -88,9 +111,35 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
width={865} width={865}
style={{}} style={{}}
maxContentHeight={'max(calc(100vh - 200px), 600px)'} maxContentHeight={'max(calc(100vh - 200px), 600px)'}
footer={null} footer={
<Footer>
<span>
{addedCount > 0 && (
<Alert
message={intl.formatMessage(
{
id: 'clusters.addworker.message.success'
},
{ count: addedCount }
)}
type="warning"
/>
)}
</span>
<Button
type="primary"
onClick={onCancel}
style={{
minWidth: 88
}}
>
{intl.formatMessage({ id: 'common.button.done' })}
</Button>
</Footer>
}
> >
<AddWorkerStep <AddWorkerStep
isModal={true}
stepList={stepList} stepList={stepList}
provider={provider} provider={provider}
clusterList={clusterList} clusterList={clusterList}
@@ -134,7 +134,7 @@ const SummaryData: React.FC = () => {
tips={ tips={
workerIPConfig.enable workerIPConfig.enable
? workerIPConfig.ip ? workerIPConfig.ip
? workerIPConfig.ip ? ''
: intl.formatMessage({ id: 'clusters.addworker.notSpecified' }) : intl.formatMessage({ id: 'clusters.addworker.notSpecified' })
: intl.formatMessage({ id: 'clusters.addworker.autoDetect' }) : intl.formatMessage({ id: 'clusters.addworker.autoDetect' })
} }
@@ -1,4 +1,5 @@
import { fromClusterCreationAtom } from '@/atoms/clusters'; import { fromClusterCreationAtom } from '@/atoms/clusters';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
@@ -11,6 +12,7 @@ import React, { useEffect } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ClusterFormData as FormData } from '../config/types'; import { ClusterFormData as FormData } from '../config/types';
import { useProviderRegions } from '../hooks/use-provider-regions'; import { useProviderRegions } from '../hooks/use-provider-regions';
import useSystemConfig from '../services/use-system-config';
const OptionItem = styled.div` const OptionItem = styled.div`
display: flex; display: flex;
@@ -74,6 +76,8 @@ const optionRender = (option: any): React.ReactNode => {
const CloudProvider: React.FC<CloudProviderProps> = (props) => { const CloudProvider: React.FC<CloudProviderProps> = (props) => {
const { credentialList, action, credentialID } = props; const { credentialList, action, credentialID } = props;
const intl = useIntl(); const intl = useIntl();
const { systemConfig } = useSystemConfig();
const form = Form.useFormInstance<FormData>();
const { const {
getRegions, getRegions,
@@ -105,6 +109,10 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
} }
}, [credentialID]); }, [credentialID]);
useEffect(() => {
form.setFieldValue('server_url', systemConfig?.server_external_url || '');
}, [systemConfig]);
const labelRender = (props: { const labelRender = (props: {
label: React.ReactNode; label: React.ReactNode;
value: string | number; value: string | number;
@@ -176,6 +184,21 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
})} })}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
<Form.Item<FormData>
name="server_url"
rules={[
{
required: true,
message: getRuleMessage('input', 'clusters.create.serverUrl')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'clusters.create.serverUrl' })}
required={true}
trim={true}
></SealInput.Input>
</Form.Item>
</> </>
); );
}; };
@@ -40,9 +40,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
form.setFieldsValue(currentData); form.setFieldsValue(currentData);
if (advanceConfigRef.current) { if (advanceConfigRef.current) {
const workerConfigYaml = json2Yaml({ const workerConfigYaml = json2Yaml(currentData.worker_config || {});
worker_config: currentData.worker_config || {}
});
advanceConfigRef.current?.setYamlValue(workerConfigYaml); advanceConfigRef.current?.setYamlValue(workerConfigYaml);
} }
} }
@@ -69,7 +67,9 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
return { return {
...values, ...values,
worker_config: {
...workerConfig ...workerConfig
}
}; };
} }
})); }));
@@ -129,6 +129,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
children: ( children: (
<AdvanceConfig <AdvanceConfig
action={action} action={action}
provider={provider}
ref={advanceConfigRef} ref={advanceConfigRef}
></AdvanceConfig> ></AdvanceConfig>
) )
@@ -79,3 +79,9 @@ export interface ClusterFormData {
worker_config?: Record<string, any>; worker_config?: Record<string, any>;
worker_pools?: NodePoolFormData[]; worker_pools?: NodePoolFormData[];
} }
export interface SystemConfig {
debug: boolean;
server_external_url: string;
system_default_container_registry: string;
}
@@ -36,7 +36,7 @@
"gateway_mode": { "gateway_mode": {
"type": "string", "type": "string",
"description": "Gateway mode", "description": "Gateway mode",
"enum": ["worker", "gateway", "disabled"] "enum": ["worker", "gateway", "disabled", "auto"]
}, },
"gateway_kubeconfig": { "gateway_kubeconfig": {
"type": "string", "type": "string",
@@ -88,9 +88,7 @@
"resources": { "resources": {
"type": "object", "type": "object",
"description": "Additional resource definitions", "description": "Additional resource definitions",
"additionalProperties": { "additionalProperties": {}
"type": "object"
}
}, },
"pipx_path": { "pipx_path": {
"type": "string", "type": "string",
@@ -1,53 +1,54 @@
export default `# This is a template for worker_config. export default `# This is a template for worker_config.
worker_config: # debug: false
debug: true
# directories # ========= directories ===========
cache_dir:
log_dir:
bin_dir:
# container & image # cache_dir: "/var/lib/gpustack/cache"
system_default_container_registry: # log_dir: "/var/lib/gpustack/log"
image_repo: gpustack/worker # bin_dir: "/var/lib/gpustack/bin"
image_name_override: ""
# gateway # ========= container & image ===========
gateway_mode:
gateway_concurrency: 0
gateway_kubeconfig: ""
# service & networking # system_default_container_registry: "docker.io"
service_discovery_name: # image_name_override: "gpustack/gpustack:main"
namespace: default # image_repo: "gpustack/gpustack"
worker_port:
worker_metrics_port:
service_port_range:
ray_port_range:
# resources # ========= gateway ===========
resources:
gpu:
count:
type:
cpu:
limit:
memory:
limit:
# huggingface # gateway_mode: "auto"
huggingface_token: "" # gateway_concurrency: 16
enable_hf_transfer: true # gateway_kubeconfig: "/var/lib/gpustack/higress/kubeconfig"
enable_hf_xet: true
# metrics # ========= service & networking ===========
disable_worker_metrics: false
# tools & runtime # service_discovery_name: "worker"
pipx_path: # namespace: "gpustack-system"
tools_download_base_url: # worker_port: 10150
# worker_metrics_port: 10150
# service_port_range: "40000-40063"
# ray_port_range: "41000-41999"
# proxy # ========= resources ===========
proxy_mode: worker
# resources:
# ========= huggingface ===========
# huggingface_token:
# enable_hf_transfer: false
# enable_hf_xet: false
# ========= metrics ===========
# disable_worker_metrics: false
# ========= tools & runtime ===========
# pipx_path: "/usr/local/bin/pipx"
# tools_download_base_url:
# ========= proxy ===========
# proxy_mode: worker
`; `;
@@ -25,6 +25,7 @@ export default function useAddWorkerMessage(params: {
onCreate: (newItems: any) => { onCreate: (newItems: any) => {
if (params.startWatch?.current) { if (params.startWatch?.current) {
newItemsRef.current = newItemsRef.current.concat(newItems); newItemsRef.current = newItemsRef.current.concat(newItems);
console.log('newItemsRef.current:', newItemsRef.current);
showAddWorkerMessage(); showAddWorkerMessage();
} }
} }
@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { querySystemConfig } from '../apis';
import { SystemConfig } from '../config/types';
export default function useSystemConfig() {
const [systemConfig, setSystemConfig] = useState<SystemConfig>(
{} as SystemConfig
);
const fetchSystemConfig = async () => {
try {
const data = await querySystemConfig();
setSystemConfig(data);
} catch (error) {
// handle error if needed
setSystemConfig({} as SystemConfig);
}
};
useEffect(() => {
fetchSystemConfig();
}, []);
return { systemConfig };
}
@@ -4,16 +4,20 @@ import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import { ProviderType, ProviderValueMap } from '../config';
import { ClusterFormData as FormData } from '../config/types'; import { ClusterFormData as FormData } from '../config/types';
import schema from '../config/worker-config.json'; import schema from '../config/worker-config.json';
import yamlTemplate from '../config/yaml-template'; import yamlTemplate from '../config/yaml-template';
const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> = const ClusterAdvanceConfig: React.FC<{
forwardRef(({ action }, ref) => { action: PageActionType;
provider: ProviderType;
ref?: any;
}> = forwardRef(({ action, provider }, ref) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const editorRef = React.useRef<any>(null); const editorRef = React.useRef<any>(null);
const [fileContent, setFileContent] = React.useState<string>(''); const [fileContent, setFileContent] = React.useState<string>(yamlTemplate);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getYamlValue: () => { getYamlValue: () => {
@@ -27,6 +31,7 @@ const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> =
return ( return (
<> <>
{provider !== ProviderValueMap.DigitalOcean && (
<Form.Item<FormData> <Form.Item<FormData>
name="server_url" name="server_url"
rules={[ rules={[
@@ -42,6 +47,7 @@ const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> =
trim={true} trim={true}
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
)}
<Form.Item<FormData> <Form.Item<FormData>
hidden hidden
name="worker_config" name="worker_config"
@@ -52,10 +58,7 @@ const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> =
} }
]} ]}
> >
<SealInput.TextArea <SealInput.TextArea required={false} trim={false}></SealInput.TextArea>
required={false}
trim={false}
></SealInput.TextArea>
</Form.Item> </Form.Item>
<YamlEditor <YamlEditor
ref={editorRef} ref={editorRef}
@@ -66,7 +69,6 @@ const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> =
setFileContent(content); setFileContent(content);
}} }}
schema={schema} schema={schema}
placeholder={yamlTemplate}
></YamlEditor> ></YamlEditor>
</> </>
); );