Skip to content

Commit fce6bde

Browse files
Fix workbench update corruption when removing cluster storage (#5264)
* Clear old volumes and volumeMounts in updateNotebook to prevent corrupted merges * added unit test * Enhance Volume type to include optional configMap property and update related tests for volume handling in updateNotebook * Update Volume type to include defaultMode property and refactor volume extraction in updateNotebook tests * remove product bug label, and bug tag, add minor test fix Signed-off-by: dalthecow <dalcowboiz@gmail.com> --------- Signed-off-by: dalthecow <dalcowboiz@gmail.com> Co-authored-by: dalthecow <dalcowboiz@gmail.com>
1 parent 70bfc94 commit fce6bde

4 files changed

Lines changed: 95 additions & 20 deletions

File tree

frontend/src/api/k8s/__tests__/notebooks.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
k8sPatchResource,
55
k8sListResource,
66
k8sDeleteResource,
7+
k8sUpdateResource,
78
K8sStatus,
89
} from '@openshift/dynamic-plugin-sdk-utils';
910
import { NotebookKind } from '#~/k8sTypes';
@@ -30,6 +31,7 @@ import {
3031
getStopPatch,
3132
startPatch,
3233
mergePatchUpdateNotebook,
34+
updateNotebook,
3335
restartNotebook,
3436
patchNotebookImage,
3537
} from '#~/api/k8s/notebooks';
@@ -50,6 +52,7 @@ jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({
5052
k8sGetResource: jest.fn(),
5153
k8sListResource: jest.fn(),
5254
k8sPatchResource: jest.fn(),
55+
k8sUpdateResource: jest.fn(),
5356
}));
5457

5558
jest.mock('#~/api/k8sUtils', () => ({
@@ -67,6 +70,7 @@ const k8sCreateResourceMock = jest.mocked(k8sCreateResource<NotebookKind>);
6770
const k8sGetResourceMock = jest.mocked(k8sGetResource<NotebookKind>);
6871
const k8sPatchResourceMock = jest.mocked(k8sPatchResource<NotebookKind>);
6972
const k8sListResourceMock = jest.mocked(k8sListResource<NotebookKind>);
73+
const k8sUpdateResourceMock = jest.mocked(k8sUpdateResource<NotebookKind>);
7074
const k8sMergePatchResourceMock = jest.mocked(k8sMergePatchResource<NotebookKind>);
7175
const k8sDeleteResourceMock = jest.mocked(k8sDeleteResource<NotebookKind, K8sStatus>);
7276

@@ -1350,3 +1354,78 @@ describe('restartNotebook', () => {
13501354
expect(renderResult).toStrictEqual(notebookMock);
13511355
});
13521356
});
1357+
1358+
describe('updateNotebook', () => {
1359+
it('should clear old volumes and volumeMounts to prevent lodash merge corruption', async () => {
1360+
// Create existing notebook with multiple volumes including system volumes
1361+
const existingNotebook = mockNotebookK8sResource({ uid });
1362+
1363+
// Add volumes that would cause corruption if merged by index with lodash
1364+
existingNotebook.spec.template.spec.volumes = [
1365+
{ name: 'storage-1', persistentVolumeClaim: { claimName: 'storage-1' } },
1366+
{ name: 'storage-2', persistentVolumeClaim: { claimName: 'storage-2' } },
1367+
{ name: 'shm', emptyDir: { medium: 'Memory' } },
1368+
{
1369+
name: 'trusted-ca',
1370+
configMap: {
1371+
name: 'workbench-trusted-ca-bundle',
1372+
optional: true,
1373+
},
1374+
},
1375+
{ name: 'oauth-config', secret: { secretName: 'test-oauth-config', defaultMode: 420 } },
1376+
];
1377+
1378+
existingNotebook.spec.template.spec.containers[0].volumeMounts = [
1379+
{ name: 'storage-1', mountPath: '/opt/app-root/src' },
1380+
{ name: 'storage-2', mountPath: '/data' },
1381+
{ name: 'shm', mountPath: '/dev/shm' },
1382+
{ name: 'trusted-ca', mountPath: '/etc/pki/tls/certs/ca-bundle.crt' },
1383+
];
1384+
1385+
// Create new notebook data with only storage-1 (simulating removal of storage-2)
1386+
const newNotebookData = mockStartNotebookData({
1387+
notebookId: existingNotebook.metadata.name,
1388+
});
1389+
1390+
k8sUpdateResourceMock.mockResolvedValue(existingNotebook);
1391+
1392+
await updateNotebook(existingNotebook, newNotebookData, username);
1393+
1394+
// Get the resource that was passed to k8sUpdateResource
1395+
const { resource: mergedNotebook } = k8sUpdateResourceMock.mock.calls[0][0];
1396+
1397+
// Verify volumes are clean (no duplicates, no multiple source types per volume)
1398+
const { volumes } = mergedNotebook.spec.template.spec;
1399+
expect(volumes).toBeDefined();
1400+
1401+
// Check that shm volume was added and is clean (only has emptyDir, not merged with PVC)
1402+
const shmVolume = volumes?.find((v) => v.name === 'shm');
1403+
expect(shmVolume).toBeDefined();
1404+
expect(shmVolume).toEqual({
1405+
name: 'shm',
1406+
emptyDir: { medium: 'Memory' },
1407+
});
1408+
// Verify no corruption - should not have both emptyDir and persistentVolumeClaim
1409+
expect(shmVolume?.persistentVolumeClaim).toBeUndefined();
1410+
expect(shmVolume?.secret).toBeUndefined();
1411+
expect(shmVolume?.configMap).toBeUndefined();
1412+
1413+
// Verify no duplicate volume names
1414+
const volumeNames = volumes?.map((v) => v.name) || [];
1415+
const uniqueVolumeNames = new Set(volumeNames);
1416+
expect(volumeNames.length).toBe(uniqueVolumeNames.size);
1417+
1418+
// Verify volumeMounts are clean
1419+
const volumeMounts = mergedNotebook.spec.template.spec.containers[0].volumeMounts || [];
1420+
1421+
// Verify no duplicate mount paths
1422+
const mountPaths = volumeMounts.map((m) => m.mountPath);
1423+
const uniqueMountPaths = new Set(mountPaths);
1424+
expect(mountPaths.length).toBe(uniqueMountPaths.size);
1425+
1426+
// Verify shm volumeMount exists
1427+
const shmMount = volumeMounts.find((m) => m.name === 'shm');
1428+
expect(shmMount).toBeDefined();
1429+
expect(shmMount?.mountPath).toBe('/dev/shm');
1430+
});
1431+
});

frontend/src/api/k8s/notebooks.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ export const updateNotebook = (
291291
oldNotebook.spec.template.spec.nodeSelector = {};
292292
container.resources = {};
293293

294+
// Clear old volumes and volumeMounts to prevent lodash merge from
295+
// merging them by array index, which creates corrupted volumes with multiple types
296+
oldNotebook.spec.template.spec.volumes = [];
297+
container.volumeMounts = [];
298+
294299
return k8sUpdateResource<NotebookKind>(
295300
applyK8sAPIOptions(
296301
{

frontend/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,12 @@ export type Volume = {
631631
secret?: {
632632
secretName: string;
633633
optional?: boolean;
634+
defaultMode?: number;
635+
};
636+
configMap?: {
637+
name: string;
638+
optional?: boolean;
639+
defaultMode?: number;
634640
};
635641
};
636642

packages/cypress/cypress/tests/e2e/dataScienceProjects/workbenches/testWorkbenchCreation.cy.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
getImageStreamDisplayName,
2121
} from '../../../../utils/oc_commands/imageStreams';
2222

23-
describe('[Product Bug: RHOAIENG-31579] Create, Delete and Edit - Workbench Tests', () => {
23+
describe('Create, Delete and Edit - Workbench Tests', () => {
2424
let editTestNamespace: string;
2525
let editedTestNamespace: string;
2626
let editedTestDescription: string;
@@ -71,15 +71,7 @@ describe('[Product Bug: RHOAIENG-31579] Create, Delete and Edit - Workbench Test
7171
it(
7272
'Create Workbench from the launcher page and verify that it is created successfully.',
7373
{
74-
tags: [
75-
'@Sanity',
76-
'@SanitySet1',
77-
'@ODS-1931',
78-
'@ODS-2218',
79-
'@Dashboard',
80-
'@Workbenches',
81-
'@Bug',
82-
],
74+
tags: ['@Sanity', '@SanitySet1', '@ODS-1931', '@ODS-2218', '@Dashboard', '@Workbenches'],
8375
},
8476
() => {
8577
const workbenchName = editTestNamespace.replace('dsp-', '');
@@ -127,7 +119,8 @@ describe('[Product Bug: RHOAIENG-31579] Create, Delete and Edit - Workbench Test
127119
// Edit the workbench and update
128120
cy.step('Editing the workbench - both the Name and Description');
129121
notebookRow.findKebab().click();
130-
notebookRow.findKebabAction('Edit workbench').click();
122+
// If we click to edit the wb immediately after stopping we risk encountering a sync issue when editing the wb, delay remedies this
123+
notebookRow.findKebabAction('Edit workbench').trigger('click', { delay: 2000 });
131124
createSpawnerPage.getNameInput().clear().type(editedTestNamespace);
132125
createSpawnerPage.getDescriptionInput().type(editedTestDescription);
133126
createSpawnerPage.findSubmitButton().click();
@@ -149,15 +142,7 @@ describe('[Product Bug: RHOAIENG-31579] Create, Delete and Edit - Workbench Test
149142
it(
150143
'Verify user can delete PV storage, data connection and workbench in a shared DS project',
151144
{
152-
tags: [
153-
'@Sanity',
154-
'@SanitySet1',
155-
'@ODS-1931',
156-
'@ODS-2218',
157-
'@Dashboard',
158-
'@Workbenches',
159-
'@Bug',
160-
],
145+
tags: ['@Sanity', '@SanitySet1', '@ODS-1931', '@ODS-2218', '@Dashboard', '@Workbenches'],
161146
},
162147
() => {
163148
// Authentication and navigation

0 commit comments

Comments
 (0)