From 59593fab8598d8de0f08b5a5c1dc52e55a0841c7 Mon Sep 17 00:00:00 2001 From: Andrew Ballantyne Date: Wed, 15 Nov 2023 17:24:41 -0500 Subject: [PATCH] Update jupyter & ds projects to hide outdated images --- backend/src/routes/api/images/imageUtils.ts | 3 + backend/src/utils/constants.ts | 1 + .../__mocks__/mockImageStreamK8sResource.ts | 6 +- frontend/src/k8sTypes.ts | 1 + .../spawner/__tests__/spawnerUtils.spec.ts | 70 ++++++++++++++++++- .../pages/projects/screens/spawner/const.ts | 2 + .../projects/screens/spawner/spawnerUtils.ts | 7 +- 7 files changed, 87 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/api/images/imageUtils.ts b/backend/src/routes/api/images/imageUtils.ts index 29c0337b0a..48ce6a5ef5 100644 --- a/backend/src/routes/api/images/imageUtils.ts +++ b/backend/src/routes/api/images/imageUtils.ts @@ -186,6 +186,9 @@ const getTagInfo = (imageStream: ImageStream): ImageTagInfo[] => { if (!checkTagExistence(tag, imageStream)) { return; //Skip tag } + if (tagAnnotations[IMAGE_ANNOTATIONS.OUTDATED]) { + return; // tag is outdated - we want to keep it around for existing notebooks, not for new ones + } //TODO: add build status const tagInfo: ImageTagInfo = { diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 0604d40052..079ac5d0bf 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -26,6 +26,7 @@ export const IMAGE_ANNOTATIONS = { DEPENDENCIES: 'opendatahub.io/notebook-python-dependencies', IMAGE_ORDER: 'opendatahub.io/notebook-image-order', RECOMMENDED: 'opendatahub.io/workbench-image-recommended', + OUTDATED: 'opendatahub.io/image-tag-outdated', }; export const blankDashboardCR: DashboardConfig = { apiVersion: 'opendatahub.io/v1alpha', diff --git a/frontend/src/__mocks__/mockImageStreamK8sResource.ts b/frontend/src/__mocks__/mockImageStreamK8sResource.ts index ba26f61dcf..df2888926f 100644 --- a/frontend/src/__mocks__/mockImageStreamK8sResource.ts +++ b/frontend/src/__mocks__/mockImageStreamK8sResource.ts @@ -15,7 +15,7 @@ export const mockImageStreamK8sResource = ({ displayName = 'Test Image', opts = {}, }: MockResourceConfigType): ImageStreamKind => - _.merge( + _.mergeWith( { apiVersion: 'image.openshift.io/v1', kind: 'ImageStream', @@ -81,4 +81,8 @@ export const mockImageStreamK8sResource = ({ }, } as ImageStreamKind, opts, + // Make sure tags can be overridden + (defaultValue, optsValue) => + // Allow for emptying the array + Array.isArray(optsValue) && optsValue.length === 0 ? [] : undefined, ); diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index ef343aacbd..d073347d92 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -65,6 +65,7 @@ type ImageStreamSpecTagAnnotations = Partial<{ 'opendatahub.io/notebook-software': string; 'opendatahub.io/workbench-image-recommended': string; 'opendatahub.io/default-image': string; + 'opendatahub.io/image-tag-outdated': string; }>; export type NotebookAnnotations = Partial<{ diff --git a/frontend/src/pages/projects/screens/spawner/__tests__/spawnerUtils.spec.ts b/frontend/src/pages/projects/screens/spawner/__tests__/spawnerUtils.spec.ts index e9e72aaa12..49aa2009a1 100644 --- a/frontend/src/pages/projects/screens/spawner/__tests__/spawnerUtils.spec.ts +++ b/frontend/src/pages/projects/screens/spawner/__tests__/spawnerUtils.spec.ts @@ -1,6 +1,11 @@ import { AWS_KEYS } from '~/pages/projects/dataConnections/const'; -import { isAWSValid } from '~/pages/projects/screens/spawner/spawnerUtils'; +import { + getExistingVersionsForImageStream, + isAWSValid, +} from '~/pages/projects/screens/spawner/spawnerUtils'; import { EnvVariableDataEntry } from '~/pages/projects/types'; +import { mockImageStreamK8sResource } from '~/__mocks__/mockImageStreamK8sResource'; +import { IMAGE_ANNOTATIONS } from '~/pages/projects/screens/spawner/const'; describe('isAWSValid', () => { const getMockAWSData = ({ @@ -67,3 +72,66 @@ describe('isAWSValid', () => { ); }); }); + +describe('getExistingVersionsForImageStream', () => { + it('should handle no image tags', () => { + const imageStream = mockImageStreamK8sResource({ + opts: { spec: { tags: [] }, status: { tags: [] } }, + }); + expect(getExistingVersionsForImageStream(imageStream)).toHaveLength(0); + }); + + it('should return the only default value', () => { + expect(getExistingVersionsForImageStream(mockImageStreamK8sResource({}))).toHaveLength(1); + }); + + it('should exclude the outdated items', () => { + // Override the first value + const imageStream = mockImageStreamK8sResource({ + opts: { spec: { tags: [{ annotations: { [IMAGE_ANNOTATIONS.OUTDATED]: 'true' } }] } }, + }); + expect(getExistingVersionsForImageStream(imageStream)).toHaveLength(0); + + // Add an outdated 2nd value + const imageStream2 = mockImageStreamK8sResource({ + opts: { + spec: { + tags: [{}, { name: 'test', annotations: { [IMAGE_ANNOTATIONS.OUTDATED]: 'true' } }], + }, + }, + }); + expect(getExistingVersionsForImageStream(imageStream2)).toHaveLength(1); + }); + + it('should exclude removed tags', () => { + const imageStream = mockImageStreamK8sResource({ + opts: { + spec: { + tags: [{ name: 'not-the-available-tag' }], + }, + }, + }); + expect(getExistingVersionsForImageStream(imageStream)).toHaveLength(0); + }); + + it('should exclude removed tags & outdated ones', () => { + const imageStream = mockImageStreamK8sResource({ + opts: { + spec: { + tags: [ + {}, + { name: 'not-the-available-tag' }, + { name: 'should-be-included' }, + { name: 'outdated', annotations: { [IMAGE_ANNOTATIONS.OUTDATED]: 'true' } }, + ], + }, + status: { + tags: [{ tag: 'should-be-included' }, { tag: 'outdated' }], + }, + }, + }); + const result = getExistingVersionsForImageStream(imageStream); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ name: 'should-be-included' }); + }); +}); diff --git a/frontend/src/pages/projects/screens/spawner/const.ts b/frontend/src/pages/projects/screens/spawner/const.ts index 5133389851..c9a4f65888 100644 --- a/frontend/src/pages/projects/screens/spawner/const.ts +++ b/frontend/src/pages/projects/screens/spawner/const.ts @@ -15,6 +15,7 @@ export const ScrollableSelectorID = 'workbench-spawner-page'; export const FAILED_PHASES = [BUILD_PHASE.ERROR, BUILD_PHASE.FAILED]; export const PENDING_PHASES = [BUILD_PHASE.NEW, BUILD_PHASE.PENDING, BUILD_PHASE.CANCELLED]; +// TODO: Convert to enum export const IMAGE_ANNOTATIONS = { DESC: 'opendatahub.io/notebook-image-desc' as const, DISP_NAME: 'opendatahub.io/notebook-image-name' as const, @@ -24,6 +25,7 @@ export const IMAGE_ANNOTATIONS = { DEPENDENCIES: 'opendatahub.io/notebook-python-dependencies' as const, IMAGE_ORDER: 'opendatahub.io/notebook-image-order' as const, RECOMMENDED: 'opendatahub.io/workbench-image-recommended' as const, + OUTDATED: 'opendatahub.io/image-tag-outdated' as const, }; export const DEFAULT_NOTEBOOK_SIZES = [ diff --git a/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts b/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts index 019d779531..8ec6839225 100644 --- a/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts +++ b/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts @@ -199,6 +199,9 @@ export const getImageVersionSoftwareString = (imageVersion: ImageStreamSpecTagTy return softwareString.join(', '); }; +const isOutdated = (version: ImageStreamSpecTagType): boolean => + !!version.annotations?.[IMAGE_ANNOTATIONS.OUTDATED]; + /** * Get all the `imageStream.spec.tags` and filter the ones exists in `imageStream.status.tags` */ @@ -206,7 +209,9 @@ export const getExistingVersionsForImageStream = ( imageStream: ImageStreamKind, ): ImageStreamSpecTagType[] => { const allVersions = imageStream.spec.tags || []; - return allVersions.filter((version) => checkVersionExistence(imageStream, version)); + return allVersions + .filter((version) => !isOutdated(version)) + .filter((version) => checkVersionExistence(imageStream, version)); }; /**