Skip to content

Commit cb26468

Browse files
committed
Updates from feedback
1 parent 533dc9f commit cb26468

3 files changed

Lines changed: 174 additions & 25 deletions

File tree

packages/cypress/cypress/tests/e2e/dataScienceProjects/models/testDeployLLMDServing.cy.ts

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ import {
2323
createCleanHardwareProfile,
2424
cleanupHardwareProfiles,
2525
} from '../../../../utils/oc_commands/hardwareProfiles';
26-
import {
27-
createCleanLLMInferenceServiceConfig,
28-
cleanupLLMInferenceServiceConfig,
29-
checkLLMInferenceServiceConfigState,
30-
} from '../../../../utils/oc_commands/llmInferenceServiceConfig';
3126
import { checkLLMInferenceServiceState } from '../../../../utils/oc_commands/modelServing';
3227
import { stubClipboard, getClipboardContent } from '../../../../utils/clipboardUtils';
3328

@@ -39,11 +34,10 @@ let hardwareProfileResourceName: string;
3934
let hardwareProfileYamlPath: string;
4035
let modelURI: string;
4136
let servingRuntime: string;
37+
let existingImage: string;
38+
let replaceImage: string;
4239
let yamlEditorModelName: string;
4340
let yamlEditorModelPath: string;
44-
const llmInferenceServiceConfigName = 'kserve-config-llm-template-cpu';
45-
const llmInferenceServiceConfigYamlPath =
46-
'resources/modelServing/llmd-inference-service-config.yaml';
4741

4842
describe('A user can deploy an LLMD model', () => {
4943
retryableBefore(() => {
@@ -57,23 +51,18 @@ describe('A user can deploy an LLMD model', () => {
5751
servingRuntime = testData.servingRuntime;
5852
hardwareProfileResourceName = `${testData.hardwareProfileName}`;
5953
hardwareProfileYamlPath = `resources/yaml/llmd-hardware-profile.yaml`;
54+
existingImage = testData.existingImage;
55+
replaceImage = testData.replaceImage;
6056
yamlEditorModelName = testData.yamlEditorModelName;
6157
yamlEditorModelPath = 'cypress/fixtures/resources/yaml/yaml_editor_model_serving.yaml';
58+
6259
cy.log(`Loaded project name: ${projectName}`);
6360
createCleanProject(projectName);
6461
})
6562
.then(() => {
6663
// Load Hardware Profile
6764
cy.log(`Load Hardware Profile Name: ${hardwareProfileResourceName}`);
6865
createCleanHardwareProfile(hardwareProfileYamlPath);
69-
})
70-
.then(() => {
71-
// Load LLMInferenceServiceConfig
72-
cy.log(`Load LLMInferenceServiceConfig: ${llmInferenceServiceConfigName}`);
73-
createCleanLLMInferenceServiceConfig(
74-
llmInferenceServiceConfigName,
75-
llmInferenceServiceConfigYamlPath,
76-
);
7766
});
7867
});
7968

@@ -82,8 +71,6 @@ describe('A user can deploy an LLMD model', () => {
8271
cy.log(`Cleaning up Hardware Profile: ${testData.hardwareProfileName}`);
8372
// Call cleanupHardwareProfiles with the actual name from the YAML file
8473
cleanupHardwareProfiles(hardwareProfileResourceName);
85-
cy.log(`Cleaning up LLMInferenceServiceConfig: ${llmInferenceServiceConfigName}`);
86-
cleanupLLMInferenceServiceConfig(llmInferenceServiceConfigName);
8774
// Delete provisioned Project - wait for completion due to RHOAIENG-19969 to support test retries, 5 minute timeout
8875
// TODO: Review this timeout once RHOAIENG-19969 is resolved
8976
deleteOpenShiftProject(projectName, { wait: true, ignoreNotFound: true, timeout: 300000 });
@@ -149,14 +136,16 @@ describe('A user can deploy an LLMD model', () => {
149136
modelServingWizard.findYAMLViewerToggle(YAMLViewerToggleOption.YAML).should('exist').click();
150137
modelServingWizard.findYAMLCodeEditor().waitForReady();
151138

152-
cy.step('Verify YAML content includes the LLMInferenceServiceConfig CPU image');
139+
cy.step('Patch YAML to use CPU image (workaround for non-GPU cluster)');
140+
modelServingWizard.findYAMLCodeEditor().replaceInEditor(existingImage, replaceImage);
153141
modelServingWizard.findYAMLCodeEditor().copyToClipboard().click();
154142
getClipboardContent('copiedYAML').then((copied) => {
155143
expect(copied).to.have.length.at.least(1);
156144
const yamlContent = copied[0];
157145
expect(yamlContent).to.include('apiVersion: serving.kserve.io/v1alpha1');
158146
expect(yamlContent).to.include('kind: LLMInferenceService');
159147
expect(yamlContent).to.include(`name: ${modelName}`);
148+
expect(yamlContent).to.include(replaceImage);
160149
});
161150
modelServingWizard.findYAMLCodeEditor().download().should('exist').click();
162151
// Back to Form view
@@ -177,13 +166,12 @@ describe('A user can deploy an LLMD model', () => {
177166
modelServingSection.findModelServerDeployedName(modelName);
178167

179168
cy.step('Verify that the Model is ready');
169+
// Image was patched in YAML editor before submit, so no post-deployment patching needed
180170
cy.get<string>('@resourceName').then((resourceName) => {
181-
checkLLMInferenceServiceState(resourceName, projectName);
171+
checkLLMInferenceServiceState(resourceName, projectName, { checkReady: true });
182172
});
183-
checkLLMInferenceServiceConfigState(llmInferenceServiceConfigName);
184173

185174
cy.step('Verify the model Row');
186-
cy.reload();
187175
const llmdRow = modelServingGlobal.getDeploymentRow(modelName);
188176
llmdRow.findStatusLabel(ModelStateLabel.STARTED).should('exist');
189177
llmdRow.findServingRuntime().should('have.text', servingRuntime);
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {
2+
ModelLocationSelectOption,
3+
ModelStateLabel,
4+
ModelTypeLabel,
5+
} from '@odh-dashboard/model-serving/types/form-data';
6+
import { deleteOpenShiftProject } from '../../../../utils/oc_commands/project';
7+
import { HTPASSWD_CLUSTER_ADMIN_USER } from '../../../../utils/e2eUsers';
8+
import { projectDetails, projectListPage } from '../../../../pages/projects';
9+
import { retryableBefore } from '../../../../utils/retryableHooks';
10+
import { createCleanProject } from '../../../../utils/projectChecker';
11+
import {
12+
modelServingGlobal,
13+
modelServingSection,
14+
modelServingWizard,
15+
deleteModelServingModal,
16+
inferenceServiceActions,
17+
} from '../../../../pages/modelServing';
18+
import { generateTestUUID } from '../../../../utils/uuidGenerator';
19+
import type { DataScienceProjectData } from '../../../../types';
20+
import { loadDSPFixture } from '../../../../utils/dataLoader';
21+
import {
22+
createCleanHardwareProfile,
23+
cleanupHardwareProfiles,
24+
} from '../../../../utils/oc_commands/hardwareProfiles';
25+
import {
26+
createCleanLLMInferenceServiceConfig,
27+
cleanupLLMInferenceServiceConfig,
28+
checkLLMInferenceServiceConfigState,
29+
} from '../../../../utils/oc_commands/llmInferenceServiceConfig';
30+
import { checkLLMInferenceServiceState } from '../../../../utils/oc_commands/modelServing';
31+
32+
let testData: DataScienceProjectData;
33+
let projectName: string;
34+
let modelName: string;
35+
const uuid = generateTestUUID();
36+
let hardwareProfileResourceName: string;
37+
let modelURI: string;
38+
const llmInferenceServiceConfigName = 'kserve-config-llm-template-cpu';
39+
const llmInferenceServiceConfigDisplayName = 'vLLM CPU LLMInferenceServiceConfig';
40+
const llmInferenceServiceConfigYamlPath =
41+
'resources/modelServing/llmd-inference-service-config.yaml';
42+
43+
describe('A user can deploy a model via vLLM on MaaS (LLMInferenceServiceConfig)', () => {
44+
retryableBefore(() => {
45+
cy.log('Loading test data');
46+
return loadDSPFixture('e2e/dataScienceProjects/testDeployLLMDServing.yaml')
47+
.then((fixtureData: DataScienceProjectData) => {
48+
testData = fixtureData;
49+
projectName = `${testData.projectResourceName}-maas-${uuid}`;
50+
modelName = `${testData.singleModelName}-maas`;
51+
modelURI = testData.modelLocationURI;
52+
hardwareProfileResourceName = `${testData.hardwareProfileName}`;
53+
54+
cy.log(`Loaded project name: ${projectName}`);
55+
createCleanProject(projectName);
56+
})
57+
.then(() => {
58+
cy.log(`Load Hardware Profile Name: ${hardwareProfileResourceName}`);
59+
createCleanHardwareProfile('resources/yaml/llmd-hardware-profile.yaml');
60+
})
61+
.then(() => {
62+
cy.log(`Load LLMInferenceServiceConfig: ${llmInferenceServiceConfigName}`);
63+
createCleanLLMInferenceServiceConfig(
64+
llmInferenceServiceConfigName,
65+
llmInferenceServiceConfigYamlPath,
66+
);
67+
});
68+
});
69+
70+
after(() => {
71+
cy.log(`Cleaning up Hardware Profile: ${hardwareProfileResourceName}`);
72+
cleanupHardwareProfiles(hardwareProfileResourceName);
73+
cy.log(`Cleaning up LLMInferenceServiceConfig: ${llmInferenceServiceConfigName}`);
74+
cleanupLLMInferenceServiceConfig(llmInferenceServiceConfigName);
75+
// Delete provisioned Project - wait for completion due to RHOAIENG-19969 to support test retries, 5 minute timeout
76+
// TODO: Review this timeout once RHOAIENG-19969 is resolved
77+
deleteOpenShiftProject(projectName, { wait: true, ignoreNotFound: true, timeout: 300000 });
78+
});
79+
80+
it(
81+
'Verify User can deploy a model by selecting an LLMInferenceServiceConfig',
82+
{
83+
tags: ['@Smoke', '@SmokeSet3', '@Dashboard', '@ModelServing', '@NonConcurrent'],
84+
},
85+
() => {
86+
cy.step('Log into the application as admin');
87+
cy.visitWithLogin('/?devFeatureFlags=vLLMDeploymentOnMaaS=true', HTPASSWD_CLUSTER_ADMIN_USER);
88+
89+
cy.step(`Navigate to the Project list tab and search for ${projectName}`);
90+
projectListPage.navigate();
91+
projectListPage.filterProjectByName(projectName);
92+
projectListPage.findProjectLink(projectName).click();
93+
94+
cy.step('Open the deploy model wizard');
95+
projectDetails.findSectionTab('model-server').click();
96+
modelServingGlobal.selectSingleServingModelButtonIfExists();
97+
modelServingGlobal.findDeployModelButton().click();
98+
99+
cy.step('Step 1: Model details - select Generative type');
100+
modelServingWizard.findModelLocationSelectOption(ModelLocationSelectOption.URI).click();
101+
modelServingWizard.findUrilocationInput().clear().type(modelURI);
102+
modelServingWizard.findSaveConnectionCheckbox().should('be.checked');
103+
modelServingWizard
104+
.findSaveConnectionInput()
105+
.clear()
106+
.type(`${modelName}${testData.connectionNameSuffix}`);
107+
modelServingWizard.findModelTypeSelectOption(ModelTypeLabel.GENERATIVE).click();
108+
109+
cy.step('Verify legacy checkbox is unchecked (non-legacy MaaS path)');
110+
modelServingWizard.findLegacyModeCheckbox().should('exist').should('not.be.checked');
111+
modelServingWizard.findNextButton().should('be.enabled').click();
112+
113+
cy.step('Step 2: Model deployment - select vLLM CPU LLMInferenceServiceConfig');
114+
modelServingWizard.findModelDeploymentNameInput().clear().type(modelName);
115+
modelServingWizard.findResourceNameButton().click();
116+
modelServingWizard
117+
.findResourceNameInput()
118+
.should('be.visible')
119+
.invoke('val')
120+
.as('resourceName');
121+
modelServingWizard.selectPotentiallyDisabledProfile(hardwareProfileResourceName);
122+
modelServingWizard.findServingRuntimeTemplateSearchSelector().click();
123+
modelServingWizard
124+
.findGlobalScopedTemplateOption(llmInferenceServiceConfigDisplayName)
125+
.should('exist')
126+
.click();
127+
modelServingWizard.findNextButton().should('be.enabled').click();
128+
129+
cy.step('Step 3: Advanced settings');
130+
modelServingWizard.findTokenAuthenticationCheckbox().should('be.checked');
131+
modelServingWizard.findNextButton().click();
132+
133+
cy.step('Step 4: Review and submit');
134+
modelServingWizard.findSubmitButton().click();
135+
136+
cy.step('Verify the model is available in UI');
137+
modelServingSection.findModelServerDeployedName(modelName);
138+
139+
cy.step('Verify LLMInferenceService exists in the project namespace');
140+
cy.get<string>('@resourceName').then((resourceName) => {
141+
checkLLMInferenceServiceState(resourceName, projectName, { checkReady: true });
142+
});
143+
144+
cy.step('Verify LLMInferenceServiceConfig was copied to the project namespace');
145+
checkLLMInferenceServiceConfigState(llmInferenceServiceConfigName, projectName, {
146+
containerImage: 'quay.io/pierdipi/vllm-cpu:latest',
147+
});
148+
149+
cy.step('Verify the model Row');
150+
const deploymentRow = modelServingGlobal.getDeploymentRow(modelName);
151+
deploymentRow.findStatusLabel(ModelStateLabel.STARTED).should('exist');
152+
deploymentRow.findKebab().click();
153+
inferenceServiceActions.findDeleteInferenceServiceAction().click();
154+
deleteModelServingModal.findInput().clear().type(modelName);
155+
deleteModelServingModal.findSubmitButton().should('be.enabled').click();
156+
},
157+
);
158+
});

packages/cypress/cypress/utils/oc_commands/llmInferenceServiceConfig.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,22 @@ export const createCleanLLMInferenceServiceConfig = (
5454
};
5555

5656
/**
57-
* Verifies that an LLMInferenceServiceConfig exists in the applications namespace
57+
* Verifies that an LLMInferenceServiceConfig exists in the given namespace
5858
* and contains the expected metadata and spec fields.
5959
*
6060
* @param configName - The `metadata.name` of the config to check.
61+
* @param namespace - The namespace to look for the config in (e.g. the project namespace where it is copied on deploy).
6162
* @param expectedFields - Optional fields to verify in the resource JSON.
6263
*/
6364
export const checkLLMInferenceServiceConfigState = (
6465
configName: string,
66+
namespace: string,
6567
expectedFields?: { containerImage?: string },
6668
): Cypress.Chainable<CommandLineResult> => {
6769
const sanitizedName = configName.replace(/[^a-zA-Z0-9_-]/g, '');
68-
const ocCommand = `oc get LLMInferenceServiceConfig ${sanitizedName} -n ${applicationNamespace} -o json`;
69-
cy.log(`Checking LLMInferenceServiceConfig exists: ${configName}`);
70+
const sanitizedNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, '');
71+
const ocCommand = `oc get LLMInferenceServiceConfig ${sanitizedName} -n ${sanitizedNamespace} -o json`;
72+
cy.log(`Checking LLMInferenceServiceConfig exists: ${configName} in namespace ${namespace}`);
7073

7174
return cy.exec(ocCommand, { failOnNonZeroExit: true }).then((result) => {
7275
let config;

0 commit comments

Comments
 (0)