diff --git a/frontend/src/__mocks__/mockImageStreamK8sResource.ts b/frontend/src/__mocks__/mockImageStreamK8sResource.ts index d8734f397b..e25cc10719 100644 --- a/frontend/src/__mocks__/mockImageStreamK8sResource.ts +++ b/frontend/src/__mocks__/mockImageStreamK8sResource.ts @@ -6,6 +6,7 @@ type MockResourceConfigType = { name?: string; namespace?: string; displayName?: string; + imageTag?: string; opts?: RecursivePartial; }; @@ -13,6 +14,7 @@ export const mockImageStreamK8sResource = ({ name = 'test-imagestream', namespace = 'test-project', displayName = 'Test Image', + imageTag = 'quay.io/opendatahub/notebooks@sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', opts = {}, }: MockResourceConfigType): ImageStreamKind => _.mergeWith( @@ -56,7 +58,7 @@ export const mockImageStreamK8sResource = ({ }, from: { kind: 'DockerImage', - name: 'quay.io/opendatahub/notebooks@sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', + name: imageTag, }, }, ], @@ -72,7 +74,7 @@ export const mockImageStreamK8sResource = ({ created: '2023-06-30T15:07:36Z', dockerImageReference: 'quay.io/opendatahub/notebooks@sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', - image: 'sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', + image: imageTag, generation: 2, }, ], diff --git a/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts index 0ea14efb73..b226866015 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts @@ -18,6 +18,10 @@ import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; type HandlersProps = { isEmpty?: boolean; imageStreamName?: string; + imageStreamTag?: string; + disableKServeConfig?: boolean; + disableKServeMetrics?: boolean; + disableModelConfig?: boolean; isEnabled?: string; isUnknown?: boolean; }; @@ -25,6 +29,7 @@ type HandlersProps = { const initIntercepts = ({ isEmpty = false, imageStreamName = 'test-image', + imageStreamTag = 'latest', isEnabled = 'true', isUnknown = false, }: HandlersProps) => { @@ -99,14 +104,14 @@ const initIntercepts = ({ spec: { tags: [ { - name: 'latest', + name: imageStreamTag, }, ], }, status: { tags: [ { - tag: 'latest', + tag: imageStreamTag, }, ], }, @@ -185,7 +190,7 @@ describe('Project Details', () => { }); it('Notebook with deleted image', () => { - initIntercepts({ imageStreamName: 'test' }); + initIntercepts({ imageStreamName: 'test', imageStreamTag: 'failing-tag' }); projectDetails.visit('test-project'); const notebookRow = projectDetails.getNotebookRow('test-notebook'); notebookRow.shouldHaveNotebookImageName('Test image'); diff --git a/frontend/src/pages/projects/screens/detail/notebooks/__tests__/useNotebookImageData.spec.ts b/frontend/src/pages/projects/screens/detail/notebooks/__tests__/useNotebookImageData.spec.ts new file mode 100644 index 0000000000..5e1edc788f --- /dev/null +++ b/frontend/src/pages/projects/screens/detail/notebooks/__tests__/useNotebookImageData.spec.ts @@ -0,0 +1,35 @@ +import { mockNotebookK8sResource } from '~/__mocks__'; +import { mockImageStreamK8sResource } from '~/__mocks__/mockImageStreamK8sResource'; +import { getNotebookImageData } from '~/pages/projects/screens/detail/notebooks/useNotebookImageData'; +import { NotebookImageAvailability } from '~/pages/projects/screens/detail/notebooks/const'; + +describe('getNotebookImageData', () => { + it('should return image data when image stream exists and image version exists', () => { + const notebook = mockNotebookK8sResource({ + image: + 'quay.io/opendatahub/notebooks@sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', + }); + const images = [ + mockImageStreamK8sResource({ + imageTag: + 'quay.io/opendatahub/notebooks@sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', + }), + ]; + const result = getNotebookImageData(notebook, images); + expect(result?.imageAvailability).toBe(NotebookImageAvailability.ENABLED); + }); + + it('should return image data when image stream exists and image version does not exist', () => { + const notebook = mockNotebookK8sResource({ + image: + 'quay.io/opendatahub/notebooks@sha256:a138838e1c9acd7708462e420bf939e03296b97e9cf6c0aa0fd9a5d20361ab75', + }); + const images = [ + mockImageStreamK8sResource({ + imageTag: 'quay.io/opendatahub/notebooks@sha256:invalid', + }), + ]; + const result = getNotebookImageData(notebook, images); + expect(result?.imageAvailability).toBe(NotebookImageAvailability.DELETED); + }); +}); diff --git a/frontend/src/pages/projects/screens/detail/notebooks/useNotebookImageData.ts b/frontend/src/pages/projects/screens/detail/notebooks/useNotebookImageData.ts index 0795287e5e..cbb3773112 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/useNotebookImageData.ts +++ b/frontend/src/pages/projects/screens/detail/notebooks/useNotebookImageData.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NotebookKind } from '~/k8sTypes'; +import { ImageStreamKind, NotebookKind } from '~/k8sTypes'; import useNamespaces from '~/pages/notebookController/useNamespaces'; import useImageStreams from '~/pages/projects/screens/spawner/useImageStreams'; import { PodContainer } from '~/types'; @@ -7,81 +7,85 @@ import { getImageStreamDisplayName } from '~/pages/projects/screens/spawner/spaw import { NotebookImageAvailability } from './const'; import { NotebookImageData } from './types'; -const useNotebookImageData = (notebook?: NotebookKind): NotebookImageData => { - const { dashboardNamespace } = useNamespaces(); - const [images, loaded, loadError] = useImageStreams(dashboardNamespace, true); +export const getNotebookImageData = ( + notebook: NotebookKind, + images: ImageStreamKind[], +): NotebookImageData[0] => { + const container: PodContainer | undefined = notebook.spec.template.spec.containers.find( + (currentContainer) => currentContainer.name === notebook.metadata.name, + ); + const imageTag = container?.image.split('/').at(-1)?.split(':'); - return React.useMemo(() => { - if (!loaded || !notebook) { - return [null, false, loadError]; - } + // if image could not be parsed from the container, consider it deleted because the image tag is invalid + if (!imageTag || imageTag.length < 2 || !container) { + return { + imageAvailability: NotebookImageAvailability.DELETED, + }; + } - const container: PodContainer | undefined = notebook.spec.template.spec.containers.find( - (currentContainer) => currentContainer.name === notebook.metadata.name, - ); - const imageTag = container?.image.split('/').at(-1)?.split(':'); + const [, versionName] = imageTag; + const imageStream = images.find((image) => + image.spec.tags + ? image.spec.tags.find( + (version) => version.name === versionName || version.from?.name === container.image, + ) + : false, + ); - // if image could not be parsed from the container, consider it deleted because the image tag is invalid - if (!imageTag || imageTag.length < 2 || !container) { - return [ - { - imageAvailability: NotebookImageAvailability.DELETED, - }, - true, - undefined, - ]; - } + // if the image stream is not found, consider it deleted + if (!imageStream) { + // Get the image display name from the notebook metadata if we can't find the image stream. (this is a fallback and could still be undefined) + const imageDisplayName = notebook.metadata.annotations?.['opendatahub.io/image-display-name']; - const [imageName, versionName] = imageTag; - const imageStream = images.find((image) => image.metadata.name === imageName); + return { + imageAvailability: NotebookImageAvailability.DELETED, + imageDisplayName, + }; + } - // if the image stream is not found, consider it deleted - if (!imageStream) { - // Get the image display name from the notebook metadata if we can't find the image stream. (this is a fallback and could still be undefined) - const imageDisplayName = notebook.metadata.annotations?.['opendatahub.io/image-display-name']; + const versions = imageStream.spec.tags || []; + const imageVersion = versions.find( + (version) => version.name === versionName || version.from?.name === container.image, + ); - return [ - { - imageAvailability: NotebookImageAvailability.DELETED, - imageDisplayName, - }, - true, - undefined, - ]; - } + // because the image stream was found, get its display name + const imageDisplayName = getImageStreamDisplayName(imageStream); + + // if the image version is not found, consider the image stream deleted + if (!imageVersion) { + return { + imageAvailability: NotebookImageAvailability.DELETED, + imageDisplayName, + }; + } - const versions = imageStream.spec.tags || []; - const imageVersion = versions.find((version) => version.name === versionName); + // if the image stream exists and the image version exists, return the image data + return { + imageStream, + imageVersion, + imageAvailability: + imageStream.metadata.labels?.['opendatahub.io/notebook-image'] === 'true' + ? NotebookImageAvailability.ENABLED + : NotebookImageAvailability.DISABLED, + imageDisplayName, + }; +}; + +const useNotebookImageData = (notebook?: NotebookKind): NotebookImageData => { + const { dashboardNamespace } = useNamespaces(); + const [images, loaded, loadError] = useImageStreams(dashboardNamespace, true); - // because the image stream was found, get its display name - const imageDisplayName = getImageStreamDisplayName(imageStream); + return React.useMemo(() => { + if (!loaded || !notebook) { + return [null, false, loadError]; + } + const data = getNotebookImageData(notebook, images); - // if the image version is not found, consider the image stream deleted - if (!imageVersion) { - return [ - { - imageAvailability: NotebookImageAvailability.DELETED, - imageDisplayName, - }, - true, - undefined, - ]; + if (data === null) { + return [null, false, loadError]; } - // if the image stream exists and the image version exists, return the image data - return [ - { - imageStream, - imageVersion, - imageAvailability: - imageStream.metadata.labels?.['opendatahub.io/notebook-image'] === 'true' - ? NotebookImageAvailability.ENABLED - : NotebookImageAvailability.DISABLED, - imageDisplayName, - }, - true, - undefined, - ]; + return [data, true, undefined]; }, [images, notebook, loaded, loadError]); };