Skip to content

Commit ce5ce97

Browse files
committed
deploy model from pvc
1 parent 194ee40 commit ce5ce97

File tree

9 files changed

+320
-6
lines changed

9 files changed

+320
-6
lines changed

frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/ConnectionSection.tsx

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@ import {
3939
import { isModelPathValid } from '#~/pages/modelServing/screens/projects/utils';
4040
import DashboardPopupIconButton from '#~/concepts/dashboard/DashboardPopupIconButton';
4141
import { AccessTypes } from '#~/pages/projects/dataConnections/const';
42-
import { SupportedArea, useIsAreaAvailable } from '#~/concepts/areas/index.ts';
42+
import { SupportedArea, useIsAreaAvailable } from '#~/concepts/areas/index';
43+
import { PersistentVolumeClaimKind } from '#~/k8sTypes';
44+
import { ServingRuntimePlatform } from '#~/types';
4345
import ConnectionS3FolderPathField from './ConnectionS3FolderPathField';
4446
import ConnectionOciPathField from './ConnectionOciPathField';
4547
import { ConnectionOciAlert } from './ConnectionOciAlert';
48+
import { PvcSelect } from './PVCSelect';
4649

4750
type ExistingConnectionFieldProps = {
4851
connectionTypes: ConnectionTypeConfigMapObj[];
@@ -81,7 +84,6 @@ const ExistingModelConnectionField: React.FC<ExistingConnectionFieldProps> = ({
8184
!!selectedConnection && isModelPathValid(selectedConnection, folderPath, modelUri),
8285
);
8386
}, [folderPath, modelUri, selectedConnection, setIsConnectionValid]);
84-
8587
return (
8688
<>
8789
<ExistingConnectionField
@@ -244,7 +246,9 @@ type Props = {
244246
loaded?: boolean;
245247
loadError?: Error | undefined;
246248
connections?: LabeledConnection[];
249+
pvcs?: PersistentVolumeClaimKind[];
247250
connectionTypeFilter?: (ct: ConnectionTypeConfigMapObj) => boolean;
251+
platform?: ServingRuntimePlatform;
248252
};
249253

250254
// todo convert 'data' into a generic 'modelLocation' obj
@@ -260,15 +264,17 @@ export const ConnectionSection: React.FC<Props> = ({
260264
loaded,
261265
loadError,
262266
connections,
267+
pvcs,
263268
connectionTypeFilter = () => true,
269+
platform,
264270
}) => {
265271
const [modelServingConnectionTypes] = useWatchConnectionTypes(true);
272+
266273
const connectionTypes = React.useMemo(
267274
() => modelServingConnectionTypes.filter(connectionTypeFilter),
268275
// eslint-disable-next-line react-hooks/exhaustive-deps
269276
[modelServingConnectionTypes],
270277
);
271-
272278
const hasImagePullSecret = React.useMemo(() => !!data.imagePullSecrets, [data.imagePullSecrets]);
273279
const pvcServingEnabled = useIsAreaAvailable(SupportedArea.PVCSERVING).status;
274280
const selectedConnection = React.useMemo(
@@ -279,6 +285,11 @@ export const ConnectionSection: React.FC<Props> = ({
279285
[connections, data.storage.dataConnection],
280286
);
281287

288+
const selectedPVC = React.useMemo(
289+
() => pvcs?.find((pvc) => pvc.metadata.name === data.storage.dataConnection),
290+
[pvcs, data.storage.dataConnection],
291+
);
292+
282293
React.useEffect(() => {
283294
if (selectedConnection && !connection) {
284295
setConnection(selectedConnection.connection);
@@ -296,11 +307,41 @@ export const ConnectionSection: React.FC<Props> = ({
296307
if (!loaded) {
297308
return <Skeleton />;
298309
}
299-
300310
return (
301311
<>
302-
{pvcServingEnabled && (
303-
<Radio label="PVC Serving" name="pvc-serving-radio" id="pvc-serving-radio" />
312+
{pvcServingEnabled && platform === ServingRuntimePlatform.SINGLE && (
313+
<Radio
314+
label="Existing cluster storage"
315+
name="pvc-serving-radio"
316+
id="pvc-serving-radio"
317+
isChecked={data.storage.type === InferenceServiceStorageType.PVC_STORAGE}
318+
onChange={() => {
319+
setConnection(undefined);
320+
setData('storage', {
321+
...data.storage,
322+
type: InferenceServiceStorageType.PVC_STORAGE,
323+
uri: undefined,
324+
alert: undefined,
325+
});
326+
}}
327+
body={
328+
data.storage.type === InferenceServiceStorageType.PVC_STORAGE && (
329+
<PvcSelect
330+
pvcs={pvcs}
331+
setModelUri={(uri) => setData('storage', { ...data.storage, uri })}
332+
modelUri={data.storage.uri}
333+
selectedPVC={selectedPVC}
334+
onSelect={(selection) => {
335+
setData('storage', {
336+
...data.storage,
337+
dataConnection: getResourceNameFromK8sResource(selection),
338+
});
339+
}}
340+
setIsConnectionValid={setIsConnectionValid}
341+
/>
342+
)
343+
}
344+
/>
304345
)}
305346
{existingUriOption && !hasImagePullSecret && (
306347
<Radio

frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/ManageInferenceServiceModal.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { EitherOrNone } from '@openshift/dynamic-plugin-sdk';
1313
import {
1414
getCreateInferenceServiceLabels,
15+
getProjectModelServingPlatform,
1516
submitInferenceServiceResourceWithDryRun,
1617
useCreateInferenceServiceObject,
1718
} from '#~/pages/modelServing/screens/projects/utils';
@@ -32,6 +33,7 @@ import {
3233
ModelServingCompatibleTypes,
3334
} from '#~/concepts/connectionTypes/utils';
3435
import { useModelDeploymentNotification } from '#~/pages/modelServing/screens/projects/useModelDeploymentNotification';
36+
import useServingPlatformStatuses from '#~/pages/modelServing/useServingPlatformStatuses';
3537
import ProjectSection from './ProjectSection';
3638
import InferenceServiceFrameworkSection from './InferenceServiceFrameworkSection';
3739
import InferenceServiceServingRuntimeSection from './InferenceServiceServingRuntimeSection';
@@ -72,6 +74,11 @@ const ManageInferenceServiceModal: React.FC<ManageInferenceServiceModalProps> =
7274
createData.k8sName,
7375
false,
7476
);
77+
const currentProject = projectContext?.currentProject;
78+
const platformStatuses = useServingPlatformStatuses();
79+
const { platform } = currentProject
80+
? getProjectModelServingPlatform(currentProject, platformStatuses)
81+
: { platform: undefined };
7582

7683
const currentProjectName = projectContext?.currentProject.metadata.name || '';
7784
const currentServingRuntimeName = projectContext?.currentServingRuntime?.metadata.name || '';
@@ -237,6 +244,7 @@ const ManageInferenceServiceModal: React.FC<ManageInferenceServiceModalProps> =
237244
connectionTypeFilter={(ct) =>
238245
!isModelServingCompatible(ct, ModelServingCompatibleTypes.OCI)
239246
}
247+
platform={platform}
240248
/>
241249
</FormSection>
242250
</>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react';
2+
import {
3+
FormGroup,
4+
InputGroupText,
5+
TextInput,
6+
InputGroupItem,
7+
InputGroup,
8+
} from '@patternfly/react-core';
9+
import { PersistentVolumeClaimKind } from '#~/k8sTypes';
10+
import { trimInputOnBlur, trimInputOnPaste } from '#~/concepts/connectionTypes/utils';
11+
12+
type PVCFieldsProps = {
13+
selectedPVC: PersistentVolumeClaimKind;
14+
setModelUri: (uri: string) => void;
15+
modelPath?: string;
16+
};
17+
18+
export const PVCFields: React.FC<PVCFieldsProps> = ({ selectedPVC, setModelUri, modelPath }) => (
19+
<FormGroup label="Model path" isRequired>
20+
<InputGroup>
21+
<InputGroupText>pvc://{selectedPVC.metadata.name}/</InputGroupText>
22+
<InputGroupItem isFill>
23+
<TextInput
24+
id="pvc-model-path"
25+
aria-label="Model path"
26+
data-testid="pvc-model-path"
27+
type="text"
28+
value={modelPath ?? ''}
29+
isRequired
30+
onChange={(e, value: string) => {
31+
setModelUri(value.trim());
32+
}}
33+
onBlur={() => {
34+
trimInputOnBlur(modelPath, setModelUri);
35+
}}
36+
onPaste={() => {
37+
trimInputOnPaste(modelPath, setModelUri);
38+
}}
39+
/>
40+
</InputGroupItem>
41+
</InputGroup>
42+
</FormGroup>
43+
);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React from 'react';
2+
import { Alert, FormGroup, Label, Stack, StackItem } from '@patternfly/react-core';
3+
import TypeaheadSelect, { TypeaheadSelectOption } from '#~/components/TypeaheadSelect';
4+
import { PersistentVolumeClaimKind } from '#~/k8sTypes';
5+
import { getDisplayNameFromK8sResource } from '#~/concepts/k8s/utils';
6+
import { getModelServingPVCAnnotations } from '#~/pages/modelServing/utils';
7+
import { AccessMode } from '#~/pages/storageClasses/storageEnums';
8+
import { getPvcAccessMode } from '#~/pages/projects/utils';
9+
import { PVCFields } from './PVCFields';
10+
11+
type PvcSelectProps = {
12+
pvcs?: PersistentVolumeClaimKind[];
13+
selectedPVC?: PersistentVolumeClaimKind;
14+
onSelect: (selection: PersistentVolumeClaimKind) => void;
15+
setModelUri: (uri: string) => void;
16+
setIsConnectionValid: (isValid: boolean) => void;
17+
modelUri?: string;
18+
};
19+
20+
export const PvcSelect: React.FC<PvcSelectProps> = ({
21+
pvcs,
22+
selectedPVC,
23+
onSelect,
24+
setModelUri,
25+
setIsConnectionValid,
26+
modelUri,
27+
}) => {
28+
const [modelPath, setModelPath] = React.useState('');
29+
const lastPVCNameRef = React.useRef<string | undefined>();
30+
31+
React.useEffect(() => {
32+
if (selectedPVC && selectedPVC.metadata.name !== lastPVCNameRef.current) {
33+
const { modelPath: annotatedPath } = getModelServingPVCAnnotations(selectedPVC);
34+
const path = annotatedPath ?? '';
35+
setModelPath(path);
36+
setModelUri(generateModelUri(selectedPVC.metadata.name, path));
37+
lastPVCNameRef.current = selectedPVC.metadata.name;
38+
} else if (!selectedPVC) {
39+
setModelPath('');
40+
setModelUri('');
41+
lastPVCNameRef.current = undefined;
42+
}
43+
}, [selectedPVC, setModelUri]);
44+
45+
const handleModelPathChange = (newPath: string): void => {
46+
setModelPath(newPath);
47+
if (selectedPVC) {
48+
setModelUri(generateModelUri(selectedPVC.metadata.name, newPath));
49+
}
50+
};
51+
52+
const generateModelUri = (pvcName: string, path: string): string => `pvc://${pvcName}/${path}`;
53+
54+
React.useEffect(() => {
55+
const isValidPVCUri = (uri: string): boolean =>
56+
/^pvc:\/\/[a-zA-Z0-9-]+\/[^/\s][^\s]*$/.test(uri);
57+
setIsConnectionValid(!!selectedPVC && isValidPVCUri(modelUri ?? ''));
58+
}, [selectedPVC, modelUri, setIsConnectionValid]);
59+
60+
const options: TypeaheadSelectOption[] = React.useMemo(
61+
() =>
62+
pvcs?.map((pvc) => {
63+
const displayName = getDisplayNameFromK8sResource(pvc);
64+
const { modelPath: modelPathAnnotation, modelName } = getModelServingPVCAnnotations(pvc);
65+
const isModelServingPVC = !!modelPathAnnotation || !!modelName;
66+
return {
67+
content: displayName,
68+
value: pvc.metadata.name,
69+
dropdownLabel: (
70+
<>
71+
{isModelServingPVC && (
72+
<Label isCompact color="green">
73+
{modelName ?? 'unknown model'}
74+
</Label>
75+
)}
76+
</>
77+
),
78+
isSelected: selectedPVC?.metadata.name === pvc.metadata.name,
79+
};
80+
}) || [],
81+
[pvcs, selectedPVC],
82+
);
83+
84+
const accessMode = selectedPVC ? getPvcAccessMode(selectedPVC) : undefined;
85+
86+
return (
87+
<FormGroup label="Cluster storage" isRequired>
88+
<Stack hasGutter>
89+
<StackItem>
90+
<TypeaheadSelect
91+
placeholder="Select existing storage"
92+
selectOptions={options}
93+
dataTestId="pvc-connection-selector"
94+
onSelect={(_, selection) => {
95+
const newlySelectedPVC = pvcs?.find((pvc) => pvc.metadata.name === selection);
96+
if (newlySelectedPVC) {
97+
onSelect(newlySelectedPVC);
98+
}
99+
}}
100+
/>
101+
</StackItem>
102+
{selectedPVC && accessMode !== AccessMode.RWX && (
103+
<StackItem>
104+
<Alert variant="warning" title="Warning" isInline>
105+
This cluster storage access mode is not ReadWriteMany.
106+
</Alert>
107+
</StackItem>
108+
)}
109+
{selectedPVC && (
110+
<StackItem>
111+
<PVCFields
112+
selectedPVC={selectedPVC}
113+
modelPath={modelPath}
114+
setModelUri={handleModelPathChange}
115+
/>
116+
</StackItem>
117+
)}
118+
</Stack>
119+
</FormGroup>
120+
);
121+
};

frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { EitherOrNone } from '@openshift/dynamic-plugin-sdk';
1212
import {
1313
getCreateInferenceServiceLabels,
14+
getProjectModelServingPlatform,
1415
getSubmitInferenceServiceResourceFn,
1516
getSubmitServingRuntimeResourcesFn,
1617
useCreateInferenceServiceObject,
@@ -29,12 +30,14 @@ import {
2930
getKServeContainerEnvVarStrs,
3031
requestsUnderLimits,
3132
resourcesArePositive,
33+
isModelMesh,
3234
} from '#~/pages/modelServing/utils';
3335
import useCustomServingRuntimesEnabled from '#~/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled';
3436
import { getServingRuntimeFromName } from '#~/pages/modelServing/customServingRuntimes/utils';
3537
import DashboardModalFooter from '#~/concepts/dashboard/DashboardModalFooter';
3638
import {
3739
InferenceServiceStorageType,
40+
ServingPlatformStatuses,
3841
ServingRuntimeEditInfo,
3942
} from '#~/pages/modelServing/screens/types';
4043
import ServingRuntimeSizeSection from '#~/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeSizeSection';
@@ -65,6 +68,9 @@ import usePrefillModelDeployModal, {
6568
import { useKServeDeploymentMode } from '#~/pages/modelServing/useKServeDeploymentMode';
6669
import { SERVING_RUNTIME_SCOPE } from '#~/pages/modelServing/screens/const';
6770
import { useModelDeploymentNotification } from '#~/pages/modelServing/screens/projects/useModelDeploymentNotification';
71+
import useServingPlatformStatuses from '#~/pages/modelServing/useServingPlatformStatuses';
72+
import { ServingRuntimePlatform } from '#~/types';
73+
import usePvcs from '#~/pages/modelServing/usePvcs';
6874
import KServeAutoscalerReplicaSection from './KServeAutoscalerReplicaSection';
6975
import EnvironmentVariablesSection from './EnvironmentVariablesSection';
7076
import ServingRuntimeArgsSection from './ServingRuntimeArgsSection';
@@ -110,6 +116,23 @@ const ManageKServeModal: React.FC<ManageKServeModalProps> = ({
110116
shouldFormHidden: hideForm,
111117
existingUriOption,
112118
}) => {
119+
const getPlatform = (
120+
platformStatuses: ServingPlatformStatuses,
121+
currentProject?: ProjectKind,
122+
editingInfo?: {
123+
inferenceServiceEditInfo?: InferenceServiceKind;
124+
},
125+
) => {
126+
if (currentProject) {
127+
return getProjectModelServingPlatform(currentProject, platformStatuses).platform;
128+
}
129+
if (editingInfo?.inferenceServiceEditInfo) {
130+
return isModelMesh(editingInfo.inferenceServiceEditInfo)
131+
? ServingRuntimePlatform.MULTI
132+
: ServingRuntimePlatform.SINGLE;
133+
}
134+
return undefined;
135+
};
113136
const { isRawAvailable, isServerlessAvailable } = useKServeDeploymentMode();
114137

115138
const [createDataServingRuntime, setCreateDataServingRuntime] = useCreateServingRuntimeObject(
@@ -137,6 +160,11 @@ const ManageKServeModal: React.FC<ManageKServeModalProps> = ({
137160
createDataInferenceService.isKServeRawDeployment;
138161
const currentProjectName = projectContext?.currentProject.metadata.name;
139162
const namespace = currentProjectName || createDataInferenceService.project;
163+
const currentProject = projectContext?.currentProject;
164+
const platformStatuses = useServingPlatformStatuses();
165+
const platform = getPlatform(platformStatuses, currentProject, editInfo);
166+
167+
const pvcs = usePvcs(namespace);
140168

141169
const projectTemplates = useTemplates(namespace);
142170

@@ -473,6 +501,8 @@ const ManageKServeModal: React.FC<ManageKServeModalProps> = ({
473501
setConnection={setConnection}
474502
setIsConnectionValid={setIsConnectionValid}
475503
connections={connections}
504+
pvcs={pvcs.data}
505+
platform={platform}
476506
/>
477507
</FormSection>
478508
)}

frontend/src/pages/modelServing/screens/projects/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ const createInferenceServiceAndDataConnection = async (
363363
if (connection?.type === 'kubernetes.io/dockerconfigjson') {
364364
imagePullSecrets = [{ name: connection.metadata.name }];
365365
}
366+
if (inferenceServiceData.storage.type === InferenceServiceStorageType.PVC_STORAGE) {
367+
storageUri = inferenceServiceData.storage.uri;
368+
}
366369

367370
let inferenceService;
368371
if (editInfo) {

0 commit comments

Comments
 (0)