Skip to content

Commit 0955000

Browse files
authored
feat: Enhance the dashboard to support deploying NIM with a PVC (opendatahub-io#4447)
* v1 * fix: lint * hide on edit * refactor: DEFAULT_MODEL_PATH * model pvc dropdown * deletion logic * fix: lint * fix for current tests * fix:lint * lint * defaultMockInferenceServiceData * added testid's * tests + info * coderabbitai * deleted comment * no model is selected + relativeTime
1 parent b880467 commit 0955000

File tree

9 files changed

+1318
-27
lines changed

9 files changed

+1318
-27
lines changed

frontend/src/__mocks__/mockNimResource.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,179 @@ export const mockNimModelPVC = (): PersistentVolumeClaimKind => {
174174
export const mockNimServingResource = (
175175
resource: ConfigMapKind | SecretKind,
176176
): NimServingResponse => ({ body: { body: resource } });
177+
178+
// Mock PVC that contains a specific model
179+
type MockNimPVCOptions = {
180+
name?: string;
181+
namespace?: string;
182+
modelName?: string;
183+
servingRuntimeName?: string;
184+
size?: string;
185+
storageClassName?: string;
186+
createdDaysAgo?: number;
187+
};
188+
189+
export const mockNimModelPVCWithModel = ({
190+
name = 'nim-pvc-arctic-embed-l',
191+
namespace = 'test-project',
192+
modelName = 'arctic-embed-l',
193+
servingRuntimeName = 'test-serving-runtime',
194+
size = '50Gi',
195+
storageClassName = 'fast-ssd',
196+
createdDaysAgo = 2,
197+
}: MockNimPVCOptions = {}): PersistentVolumeClaimKind => {
198+
const createdDate = new Date();
199+
createdDate.setDate(createdDate.getDate() - createdDaysAgo);
200+
201+
const pvc = mockPVCK8sResource({
202+
name,
203+
namespace,
204+
storageClassName,
205+
});
206+
207+
// Add annotations that indicate this PVC contains a specific model
208+
if (!pvc.metadata.annotations) {
209+
pvc.metadata.annotations = {};
210+
}
211+
pvc.metadata.annotations['nim.nvidia.com/model-name'] = modelName;
212+
pvc.metadata.annotations['nim.nvidia.com/serving-runtime'] = servingRuntimeName;
213+
pvc.metadata.annotations['openshift.io/description'] = `NIM PVC containing ${modelName} model`;
214+
215+
// Set creation timestamp and storage size manually
216+
pvc.metadata.creationTimestamp = createdDate.toISOString();
217+
218+
// Set the size in spec and status
219+
pvc.spec.resources.requests = { storage: size };
220+
if (pvc.status) {
221+
pvc.status.capacity = { storage: size };
222+
}
223+
224+
return pvc;
225+
};
226+
227+
// Mock ServingRuntime that uses a specific PVC
228+
type MockNimServingRuntimeWithPVCOptions = {
229+
name?: string;
230+
namespace?: string;
231+
displayName?: string;
232+
pvcName?: string;
233+
modelName?: string;
234+
createdDaysAgo?: number;
235+
};
236+
237+
export const mockNimServingRuntimeWithPVC = ({
238+
name = 'test-serving-runtime',
239+
namespace = 'test-project',
240+
displayName = 'Test Serving Runtime',
241+
pvcName = 'nim-pvc-arctic-embed-l',
242+
modelName = 'arctic-embed-l',
243+
createdDaysAgo = 2,
244+
}: MockNimServingRuntimeWithPVCOptions = {}): ServingRuntimeKind => {
245+
const createdDate = new Date();
246+
createdDate.setDate(createdDate.getDate() - createdDaysAgo);
247+
248+
const servingRuntime = mockServingRuntimeK8sResource({
249+
name,
250+
displayName,
251+
namespace,
252+
});
253+
254+
// Add NIM-specific annotations
255+
if (!servingRuntime.metadata.annotations) {
256+
servingRuntime.metadata.annotations = {};
257+
}
258+
servingRuntime.metadata.annotations['opendatahub.io/template-display-name'] = 'NVIDIA NIM';
259+
servingRuntime.metadata.annotations['opendatahub.io/template-name'] = 'nvidia-nim-runtime';
260+
261+
// Set creation timestamp
262+
servingRuntime.metadata.creationTimestamp = createdDate.toISOString();
263+
264+
// Add PVC volume configuration - this is key for PVC discovery
265+
servingRuntime.spec.volumes = [
266+
{
267+
name: 'model-storage',
268+
persistentVolumeClaim: {
269+
claimName: pvcName,
270+
},
271+
},
272+
];
273+
274+
// Add volume mount to container
275+
if (!servingRuntime.spec.containers[0].volumeMounts) {
276+
servingRuntime.spec.containers[0].volumeMounts = [];
277+
}
278+
servingRuntime.spec.containers[0].volumeMounts.push({
279+
name: 'model-storage',
280+
mountPath: '/mnt/models/cache',
281+
});
282+
283+
// Set supported model format to match the model - use the passed modelName
284+
servingRuntime.spec.supportedModelFormats = [
285+
{
286+
name: modelName, // Use the modelName parameter directly
287+
version: '1',
288+
autoSelect: true,
289+
},
290+
];
291+
292+
// Set NIM container image to include the model name
293+
servingRuntime.spec.containers[0].image = `nvcr.io/nim/snowflake/${modelName}:1.0.1`;
294+
295+
return servingRuntime;
296+
};
297+
298+
// Mock multiple PVCs for testing selection scenarios
299+
export const mockMultipleNimPVCs = (): PersistentVolumeClaimKind[] => [
300+
// Recent PVC with arctic-embed-l
301+
mockNimModelPVCWithModel({
302+
name: 'nim-pvc-arctic-recent',
303+
modelName: 'arctic-embed-l',
304+
servingRuntimeName: 'arctic-runtime-1',
305+
createdDaysAgo: 1,
306+
size: '30Gi',
307+
}),
308+
// Older PVC with arctic-embed-l
309+
mockNimModelPVCWithModel({
310+
name: 'nim-pvc-arctic-old',
311+
modelName: 'arctic-embed-l',
312+
servingRuntimeName: 'arctic-runtime-2',
313+
createdDaysAgo: 5,
314+
size: '40Gi',
315+
}),
316+
// PVC with different model (should not show up when arctic is selected)
317+
mockNimModelPVCWithModel({
318+
name: 'nim-pvc-alphafold',
319+
modelName: 'alphafold2',
320+
servingRuntimeName: 'alphafold-runtime',
321+
createdDaysAgo: 3,
322+
size: '60Gi',
323+
}),
324+
];
325+
326+
// Mock multiple ServingRuntimes that use different PVCs
327+
export const mockMultipleNimServingRuntimes = (): ServingRuntimeKind[] => [
328+
// ServingRuntime using first PVC
329+
mockNimServingRuntimeWithPVC({
330+
name: 'arctic-runtime-1',
331+
displayName: 'Arctic Runtime 1',
332+
pvcName: 'nim-pvc-arctic-recent',
333+
modelName: 'arctic-embed-l',
334+
createdDaysAgo: 1,
335+
}),
336+
// ServingRuntime using second PVC
337+
mockNimServingRuntimeWithPVC({
338+
name: 'arctic-runtime-2',
339+
displayName: 'Arctic Runtime 2',
340+
pvcName: 'nim-pvc-arctic-old',
341+
modelName: 'arctic-embed-l',
342+
createdDaysAgo: 5,
343+
}),
344+
// ServingRuntime using different model PVC
345+
mockNimServingRuntimeWithPVC({
346+
name: 'alphafold-runtime',
347+
displayName: 'AlphaFold Runtime',
348+
pvcName: 'nim-pvc-alphafold',
349+
modelName: 'alphafold2',
350+
createdDaysAgo: 3,
351+
}),
352+
];

frontend/src/__tests__/cypress/cypress/pages/components/NIMDeployModal.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,210 @@ class NIMDeployModal extends Modal {
8080
findServiceAccountNameInput() {
8181
return this.find().findByTestId('service-account-form-name');
8282
}
83+
84+
// PVC Storage Option Selection
85+
findCreateNewPVCRadio() {
86+
return this.find().findByTestId('create-new-pvc');
87+
}
88+
89+
findUseExistingPVCRadio() {
90+
return this.find().findByTestId('use-existing-pvc');
91+
}
92+
93+
// PVC Size Section (for new PVC creation)
94+
findPVCSizeSection() {
95+
return this.find().findByTestId('pvc-size');
96+
}
97+
98+
// PVC Selection Section (for existing PVC)
99+
findPVCSelectionSection() {
100+
return this.find().findByTestId('pvc-selection-section');
101+
}
102+
103+
findManualPVCInput() {
104+
return this.find().findByTestId('manual-pvc-input');
105+
}
106+
107+
findUseManualPVCButton() {
108+
return this.find().findByTestId('use-manual-pvc-button');
109+
}
110+
111+
// Model Path Section
112+
findModelPathSection() {
113+
return this.find().findByTestId('model-path-section');
114+
}
115+
116+
findModelPathInput() {
117+
return this.find().findByTestId('model-path-input');
118+
}
119+
120+
// Alert Messages
121+
findNoCompatiblePVCsAlert() {
122+
return this.find().findByTestId('no-compatible-pvcs-alert');
123+
}
124+
125+
findCompatiblePVCsFoundMessage() {
126+
return this.find().findByTestId('compatible-pvcs-found-message');
127+
}
128+
129+
findPVCLoadingSpinner() {
130+
return this.find().findByTestId('pvc-loading-spinner');
131+
}
132+
133+
findPVCLoadingErrorAlert() {
134+
return this.find().findByTestId('pvc-loading-error-alert');
135+
}
136+
137+
// Helper methods for testing PVC workflows
138+
selectCreateNewPVC() {
139+
this.findCreateNewPVCRadio().click();
140+
return this;
141+
}
142+
143+
selectUseExistingPVC() {
144+
this.findUseExistingPVCRadio().click();
145+
return this;
146+
}
147+
148+
setModelPath(path: string) {
149+
this.findModelPathInput().clear().type(path);
150+
return this;
151+
}
152+
153+
// Assertion helpers
154+
shouldShowCreateNewPVCOption() {
155+
this.findCreateNewPVCRadio().should('be.visible');
156+
return this;
157+
}
158+
159+
shouldShowUseExistingPVCOption() {
160+
this.findUseExistingPVCRadio().should('be.visible');
161+
return this;
162+
}
163+
164+
// FIXED: Removed unused 'modelName' parameter and hardcoded the expected text
165+
shouldShowNoCompatiblePVCsAlert() {
166+
this.findNoCompatiblePVCsAlert().should(
167+
'contain.text',
168+
'No existing storage volumes found that contain arctic-embed-l',
169+
);
170+
return this;
171+
}
172+
173+
shouldHaveModelPath(path: string) {
174+
this.findModelPathInput().should('have.value', path);
175+
return this;
176+
}
177+
178+
shouldDisableModelPathInput() {
179+
this.findModelPathInput().should('be.disabled');
180+
return this;
181+
}
182+
183+
shouldDisableManualPVCInput() {
184+
this.findManualPVCInput().should('be.disabled');
185+
return this;
186+
}
187+
188+
// SimpleSelect dropdown methods
189+
findExistingPVCSelect() {
190+
return this.find().contains('Select from');
191+
}
192+
193+
findExistingPVCSelectByText() {
194+
return this.find().contains('Select from');
195+
}
196+
197+
// FIXED: Removed unnecessary waits and console statements - streamlined approach
198+
clickExistingPVCSelect() {
199+
this.find()
200+
.contains('Select from')
201+
.then(($selectText) => {
202+
// Try clicking the text directly first
203+
cy.wrap($selectText).click({ force: true });
204+
205+
// Check if dropdown opened, if not try parent element
206+
cy.get(
207+
'[role="listbox"], [role="menu"], .pf-v6-c-menu__content, .pf-v6-c-select__menu',
208+
).then(($dropdown) => {
209+
if ($dropdown.length === 0) {
210+
// Try clicking the parent element if direct click didn't work
211+
cy.wrap($selectText)
212+
.closest('.pf-v6-c-menu-toggle, button, [role="button"]')
213+
.click({ force: true });
214+
}
215+
});
216+
});
217+
return this;
218+
}
219+
220+
// FIXED: Removed console statements and unnecessary waits - simplified to essential logic
221+
selectExistingPVCRobust(pvcName: string) {
222+
this.find()
223+
.contains('Select from')
224+
.then(($element) => {
225+
// Primary approach: click the element
226+
cy.wrap($element).click({ force: true });
227+
228+
cy.get('body').then(() => {
229+
cy.get(
230+
'[role="listbox"], [role="menu"], .pf-v6-c-menu__content, .pf-v6-c-select__menu',
231+
).then(($dropdown) => {
232+
if ($dropdown.length > 0) {
233+
// Dropdown opened successfully
234+
cy.wrap($dropdown).should('be.visible').contains(pvcName).click();
235+
} else {
236+
// Fallback approaches
237+
cy.wrap($element).parent().click({ force: true });
238+
239+
cy.get('.pf-v6-c-menu-toggle, [data-testid*="select"], button')
240+
.contains('Select from')
241+
.click({ force: true });
242+
243+
// Final attempt with broader selectors
244+
cy.get(
245+
'[role="listbox"], [role="menu"], .pf-v6-c-menu__content, .pf-v6-c-select__menu, [data-popper-placement]',
246+
{ timeout: 5000 },
247+
)
248+
.should('be.visible')
249+
.contains(pvcName)
250+
.click();
251+
}
252+
});
253+
});
254+
});
255+
256+
return this;
257+
}
258+
259+
// Main PVC selection method - uses the robust approach
260+
selectExistingPVC(pvcName: string) {
261+
return this.selectExistingPVCRobust(pvcName);
262+
}
263+
264+
shouldShowPVCLoadingSpinner() {
265+
this.find().findByTestId('pvc-loading-spinner').should('be.visible');
266+
return this;
267+
}
268+
269+
shouldShowCompatiblePVCs(count: number) {
270+
this.find()
271+
.findByTestId('compatible-pvcs-found-message')
272+
.should('contain.text', `Found ${count} storage volume(s)`);
273+
this.findExistingPVCSelectByText().should('be.visible');
274+
this.find().findByTestId('use-manual-pvc-button').should('be.visible');
275+
return this;
276+
}
277+
278+
enterManualPVCName(pvcName: string) {
279+
this.find().findByTestId('use-manual-pvc-button').click();
280+
this.findManualPVCInput().clear().type(pvcName);
281+
return this;
282+
}
283+
284+
findBackToCompatibleListButton() {
285+
return this.find().findByTestId('back-to-compatible-list-button');
286+
}
83287
}
84288

85289
export const nimDeployModal = new NIMDeployModal();

0 commit comments

Comments
 (0)