Skip to content

Commit 1f78759

Browse files
committed
Add deployment strategy to wizard'
1 parent 019c34e commit 1f78759

File tree

13 files changed

+221
-0
lines changed

13 files changed

+221
-0
lines changed

frontend/src/__tests__/cypress/cypress/pages/modelServing.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,18 @@ class ModelServingWizard extends Wizard {
11901190
findDiscardButton() {
11911191
return cy.findByRole('button', { name: 'Discard' });
11921192
}
1193+
1194+
findDeploymentStrategySection() {
1195+
return cy.findByTestId('deployment-strategy-section');
1196+
}
1197+
1198+
findDeploymentStrategyRollingOption() {
1199+
return this.findDeploymentStrategySection().findByTestId('deployment-strategy-rolling');
1200+
}
1201+
1202+
findDeploymentStrategyRecreateOption() {
1203+
return this.findDeploymentStrategySection().findByTestId('deployment-strategy-recreate');
1204+
}
11931205
}
11941206

11951207
export const modelServingGlobal = new ModelServingGlobal();

frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingDeploy.cy.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ describe('Model Serving Deploy Wizard', () => {
416416
modelServingWizard.findServiceAccountByIndex(0).clear();
417417
modelServingWizard.findNextButton().should('be.disabled');
418418
modelServingWizard.findServiceAccountByIndex(0).clear().type('new name');
419+
420+
modelServingWizard.findDeploymentStrategySection().should('exist');
421+
modelServingWizard.findDeploymentStrategyRollingOption().should('be.checked');
419422
modelServingWizard.findNextButton().should('be.enabled').click();
420423

421424
// Step 4: Summary
@@ -445,6 +448,9 @@ describe('Model Serving Deploy Wizard', () => {
445448
predictor: {
446449
minReplicas: 99,
447450
maxReplicas: 99,
451+
deploymentStrategy: {
452+
type: 'RollingUpdate',
453+
},
448454
model: {
449455
modelFormat: {
450456
name: 'vLLM',
@@ -475,6 +481,9 @@ describe('Model Serving Deploy Wizard', () => {
475481
// Check spec structure without the model details
476482
expect(interception.request.body.spec.predictor.minReplicas).to.equal(99);
477483
expect(interception.request.body.spec.predictor.maxReplicas).to.equal(99);
484+
expect(interception.request.body.spec.predictor.deploymentStrategy.type).to.equal(
485+
'RollingUpdate',
486+
);
478487

479488
// Check model format exists
480489
expect(interception.request.body.spec.predictor.model.modelFormat.name).to.equal('vLLM');
@@ -691,6 +700,8 @@ describe('Model Serving Deploy Wizard', () => {
691700
modelServingWizard.findEnvVariableName('0').clear().type('valid_name');
692701
modelServingWizard.findEnvVariableValue('0').type('test-value');
693702

703+
modelServingWizard.findDeploymentStrategySection().should('exist');
704+
modelServingWizard.findDeploymentStrategyRecreateOption().click();
694705
modelServingWizard.findNextButton().should('be.enabled').click();
695706

696707
// Step 4: Summary
@@ -715,6 +726,9 @@ describe('Model Serving Deploy Wizard', () => {
715726
},
716727
spec: {
717728
predictor: {
729+
deploymentStrategy: {
730+
type: 'Recreate',
731+
},
718732
model: {
719733
modelFormat: {
720734
name: 'openvino_ir',
@@ -750,6 +764,7 @@ describe('Model Serving Deploy Wizard', () => {
750764
'openvino_ir',
751765
);
752766
expect(interception.request.body.spec.predictor.model.modelFormat.version).to.equal('opset1');
767+
expect(interception.request.body.spec.predictor.deploymentStrategy.type).to.equal('Recreate');
753768
});
754769

755770
// Actual request

frontend/src/k8sTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ export type InferenceServiceKind = K8sResourceCommon & {
536536
annotations?: Record<string, string>;
537537
tolerations?: Toleration[];
538538
nodeSelector?: NodeSelector;
539+
deploymentStrategy?: {
540+
type: 'RollingUpdate' | 'Recreate';
541+
};
539542
model?: {
540543
modelFormat?: {
541544
name: string;

packages/kserve/src/deploy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const deployKServeDeployment = async (
3535
runtimeArgs: wizardData.runtimeArgs.data,
3636
environmentVariables: wizardData.environmentVariables.data,
3737
modelAvailability: wizardData.modelAvailability.data,
38+
deploymentStrategy: wizardData.deploymentStrategy.data,
3839
};
3940

4041
const servingRuntime =

packages/kserve/src/deployModel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { ServingRuntimeModelType } from '@odh-dashboard/internal/types';
77

88
import {
9+
DeploymentStrategyFieldData,
910
type ModelLocationData,
1011
ModelLocationType,
1112
} from '@odh-dashboard/model-serving/types/form-data';
@@ -26,6 +27,7 @@ import {
2627
applyDashboardResourceLabel,
2728
applyDisplayNameDesc,
2829
applyModelType,
30+
applyDeploymentStrategy,
2931
} from './deployUtils';
3032
import { applyHardwareProfileToDeployment, applyReplicas } from './hardware';
3133
import {
@@ -50,6 +52,7 @@ export type CreatingInferenceServiceObject = {
5052
environmentVariables?: EnvironmentVariablesFieldData;
5153
modelAvailability?: ModelAvailabilityFieldsData;
5254
createConnectionData?: CreateConnectionData;
55+
deploymentStrategy?: DeploymentStrategyFieldData;
5356
};
5457

5558
const assembleInferenceService = (
@@ -74,6 +77,7 @@ const assembleInferenceService = (
7477
tokenAuth,
7578
runtimeArgs,
7679
environmentVariables,
80+
deploymentStrategy,
7781
} = data;
7882
let inferenceService: InferenceServiceKind = existingInferenceService
7983
? { ...existingInferenceService }
@@ -142,6 +146,10 @@ const assembleInferenceService = (
142146
environmentVariables ?? { variables: [], enabled: false },
143147
);
144148

149+
if (deploymentStrategy) {
150+
inferenceService = applyDeploymentStrategy(inferenceService, deploymentStrategy);
151+
}
152+
145153
return inferenceService;
146154
};
147155

packages/kserve/src/deployUtils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,14 @@ export const applyModelType = (
369369
};
370370
return result;
371371
};
372+
373+
export const applyDeploymentStrategy = (
374+
inferenceService: InferenceServiceKind,
375+
deploymentStrategy: 'rolling' | 'recreate',
376+
): InferenceServiceKind => {
377+
const result = structuredClone(inferenceService);
378+
result.spec.predictor.deploymentStrategy = {
379+
type: deploymentStrategy === 'rolling' ? 'RollingUpdate' : 'Recreate',
380+
};
381+
return result;
382+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React from 'react';
2+
import { Radio, Stack, StackItem } from '@patternfly/react-core';
3+
import { z } from 'zod';
4+
import { useAppContext } from '@odh-dashboard/internal/app/AppContext';
5+
6+
// Schema
7+
export const deploymentStrategyFieldSchema = z.enum(['rolling', 'recreate']);
8+
9+
export type DeploymentStrategyFieldData = z.infer<typeof deploymentStrategyFieldSchema>;
10+
11+
export const isValidDeploymentStrategy = (value: unknown): value is DeploymentStrategyFieldData => {
12+
return deploymentStrategyFieldSchema.safeParse(value).success;
13+
};
14+
15+
// Hook
16+
export type DeploymentStrategyFieldHook = {
17+
data: DeploymentStrategyFieldData;
18+
setData: (data: DeploymentStrategyFieldData) => void;
19+
};
20+
21+
export const useDeploymentStrategyField = (
22+
existingData?: DeploymentStrategyFieldData,
23+
): DeploymentStrategyFieldHook => {
24+
const { dashboardConfig } = useAppContext();
25+
const hasInitializedRef = React.useRef(false);
26+
27+
const clusterDefault = React.useMemo(() => {
28+
const strategy = dashboardConfig.spec.modelServing?.deploymentStrategy;
29+
if (strategy === 'rolling' || strategy === 'recreate') {
30+
return strategy;
31+
}
32+
return 'rolling';
33+
}, [dashboardConfig.spec.modelServing?.deploymentStrategy]);
34+
35+
const [deploymentStrategy, setDeploymentStrategy] = React.useState<DeploymentStrategyFieldData>(
36+
() => existingData || clusterDefault,
37+
);
38+
39+
React.useEffect(() => {
40+
if (!existingData && !hasInitializedRef.current) {
41+
hasInitializedRef.current = true;
42+
setDeploymentStrategy(clusterDefault);
43+
}
44+
}, [clusterDefault, existingData]);
45+
46+
return {
47+
data: deploymentStrategy,
48+
setData: setDeploymentStrategy,
49+
};
50+
};
51+
52+
// Component
53+
type DeploymentStrategyFieldProps = {
54+
value: DeploymentStrategyFieldData;
55+
onChange: (value: DeploymentStrategyFieldData) => void;
56+
isDisabled?: boolean;
57+
};
58+
59+
export const DeploymentStrategyField: React.FC<DeploymentStrategyFieldProps> = ({
60+
value,
61+
onChange,
62+
isDisabled = false,
63+
}) => {
64+
return (
65+
<Stack hasGutter>
66+
<StackItem>
67+
<Radio
68+
id="deployment-strategy-rolling"
69+
name="deployment-strategy"
70+
label={<span className="pf-v6-u-font-weight-bold">Rolling update</span>}
71+
description={
72+
<>
73+
Existing inference service pods are terminated <u>after</u> new ones are started. This
74+
ensures zero downtime and continuous availability.
75+
</>
76+
}
77+
isChecked={value === 'rolling'}
78+
onChange={() => onChange('rolling')}
79+
isDisabled={isDisabled}
80+
data-testid="deployment-strategy-rolling"
81+
/>
82+
</StackItem>
83+
<StackItem>
84+
<Radio
85+
id="deployment-strategy-recreate"
86+
name="deployment-strategy"
87+
label={<span className="pf-v6-u-font-weight-bold">Recreate</span>}
88+
description={
89+
<>
90+
All existing inference service pods are terminated <u>before</u> any new ones are
91+
started. This saves resources but guarantees a period of downtime.
92+
</>
93+
}
94+
isChecked={value === 'recreate'}
95+
onChange={() => onChange('recreate')}
96+
isDisabled={isDisabled}
97+
data-testid="deployment-strategy-recreate"
98+
/>
99+
</StackItem>
100+
</Stack>
101+
);
102+
};

packages/model-serving/src/components/deploymentWizard/steps/AdvancedOptionsStep.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ExternalRouteField } from '../fields/ExternalRouteField';
1313
import { TokenAuthenticationField } from '../fields/TokenAuthenticationField';
1414
import { RuntimeArgsField } from '../fields/RuntimeArgsField';
1515
import { EnvironmentVariablesField } from '../fields/EnvironmentVariablesField';
16+
import { DeploymentStrategyField } from '../fields/DeploymentStrategyField';
1617
import { UseModelDeploymentWizardState } from '../useDeploymentWizard';
1718
import { AvailableAiAssetsFieldsComponent } from '../fields/ModelAvailabilityFields';
1819
import { showAuthWarning } from '../hooks/useAuthWarning';
@@ -51,6 +52,11 @@ export const AdvancedSettingsStepContent: React.FC<AdvancedSettingsStepContentPr
5152
wizardState.state.modelServer.data,
5253
]);
5354

55+
const isLlmdSelected = React.useMemo(() => {
56+
const modelServerData = wizardState.state.modelServer.data;
57+
return modelServerData?.name === 'llmd-serving';
58+
}, [wizardState.state.modelServer.data]);
59+
5460
const getKServeContainer = (
5561
servingRuntime?: ServingRuntimeKind,
5662
): ServingContainer | undefined => {
@@ -192,6 +198,21 @@ export const AdvancedSettingsStepContent: React.FC<AdvancedSettingsStepContentPr
192198
</Stack>
193199
</FormGroup>
194200
</StackItem>
201+
{!isLlmdSelected && (
202+
<StackItem>
203+
<FormGroup
204+
label="Deployment strategy"
205+
data-testid="deployment-strategy-section"
206+
fieldId="deployment-strategy"
207+
>
208+
<DeploymentStrategyField
209+
value={wizardState.state.deploymentStrategy.data}
210+
onChange={wizardState.state.deploymentStrategy.setData}
211+
isDisabled={!allowCreate}
212+
/>
213+
</FormGroup>
214+
</StackItem>
215+
)}
195216
</Stack>
196217
</FormSection>
197218
</Form>

packages/model-serving/src/components/deploymentWizard/steps/ReviewStep.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,18 @@ const getStatusSections = (projectName: string): StatusSection[] => [
307307
},
308308
optional: true,
309309
},
310+
{
311+
key: 'deploymentStrategy',
312+
label: 'Deployment strategy',
313+
comp: (state) => {
314+
const strategy = state.deploymentStrategy.data;
315+
return strategy === 'recreate' ? 'Recreate' : 'Rolling update';
316+
},
317+
isVisible: (wizardState) => {
318+
const modelServerData = wizardState.state.modelServer.data;
319+
return modelServerData?.name !== 'llmd-serving';
320+
},
321+
},
310322
],
311323
},
312324
];

packages/model-serving/src/components/deploymentWizard/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { useModelLocationData } from './fields/ModelLocationInputFields';
1919
import type { useNumReplicasField } from './fields/NumReplicasField';
2020
import type { useRuntimeArgsField } from './fields/RuntimeArgsField';
2121
import type { useTokenAuthenticationField } from './fields/TokenAuthenticationField';
22+
import type { useDeploymentStrategyField } from './fields/DeploymentStrategyField';
2223
import {
2324
useCreateConnectionData,
2425
type CreateConnectionData,
@@ -67,6 +68,7 @@ export type InitialWizardFormData = {
6768
initSelectedConnection?: LabeledConnection | undefined;
6869
modelAvailability?: ModelAvailabilityFieldsData;
6970
createConnectionData?: CreateConnectionData;
71+
deploymentStrategy?: DeploymentStrategyFieldData;
7072
// Add more field handlers as needed
7173
};
7274

@@ -86,6 +88,7 @@ export type WizardFormData = {
8688
modelAvailability: ReturnType<typeof useModelAvailabilityFields>;
8789
modelServer: ReturnType<typeof useModelServerSelectField>;
8890
createConnectionData: ReturnType<typeof useCreateConnectionData>;
91+
deploymentStrategy: ReturnType<typeof useDeploymentStrategyField>;
8992
};
9093
};
9194
// wizard form data
@@ -104,6 +107,7 @@ export type HardwareProfileConfigFieldData =
104107
WizardFormData['state']['hardwareProfileConfig']['formData'];
105108
export type ModelFormatFieldData = WizardFormData['state']['modelFormatState']['modelFormat'];
106109
export type ModelAvailabilityFieldsData = WizardFormData['state']['modelAvailability']['data'];
110+
export type DeploymentStrategyFieldData = WizardFormData['state']['deploymentStrategy']['data'];
107111

108112
// extensible fields
109113

0 commit comments

Comments
 (0)