From ee45a4c6eeed321d9d5fd8846df40f968bc99e5f Mon Sep 17 00:00:00 2001 From: Olga Lavtar Date: Fri, 13 Dec 2024 16:03:20 -0800 Subject: [PATCH] fix: block nim re-deploy in existing projects Signed-off-by: Olga Lavtar --- frontend/src/__mocks__/mockNimAccount.ts | 10 ++++- frontend/src/__mocks__/mockNimResource.ts | 28 ++++++++++++++ .../mocked/projects/modelServingNim.cy.ts | 8 ++-- .../cypress/cypress/utils/nimUtils.ts | 29 +++++++------- frontend/src/index.tsx | 32 ++++++++-------- .../modelServing/ModelServingContext.tsx | 35 ++++++++++++----- .../global/InferenceServiceTableRow.tsx | 4 ++ .../screens/global/ServeModelButton.tsx | 23 ++++++++--- .../projects/EmptySingleModelServingCard.tsx | 1 + .../screens/projects/ModelServingPlatform.tsx | 25 ++++++++---- .../ModelServingPlatformButtonAction.tsx | 28 +++++++++----- .../screens/projects/__tests__/utils.spec.ts | 2 + .../screens/projects/useIsNIMAvailable.ts | 38 ++++++++++++------- .../src/pages/modelServing/screens/types.ts | 2 +- .../useServingPlatformStatuses.ts | 5 ++- .../pages/projects/ProjectDetailsContext.tsx | 22 +++++++++++ .../detail/overview/ProjectOverview.tsx | 8 +++- .../overview/serverModels/AddModelFooter.tsx | 14 +++++-- .../serverModels/PlatformSelectSection.tsx | 6 +-- 19 files changed, 229 insertions(+), 91 deletions(-) diff --git a/frontend/src/__mocks__/mockNimAccount.ts b/frontend/src/__mocks__/mockNimAccount.ts index 3567d5b21d..5d0b414a5a 100644 --- a/frontend/src/__mocks__/mockNimAccount.ts +++ b/frontend/src/__mocks__/mockNimAccount.ts @@ -19,7 +19,15 @@ export const mockNimAccount = ({ nimConfigName = 'mock-nvidia-nim-images-data', runtimeTemplateName = 'mock-nvidia-nim-serving-template', nimPullSecretName = 'mock-nvidia-nim-image-pull', - conditions = [], + conditions = [ + { + type: 'AccountStatus', + status: 'True', + lastTransitionTime: new Date().toISOString(), + reason: 'AccountSuccessful', + message: 'reconciled successfully', + }, + ], }: MockResourceConfigType): NIMAccountKind => ({ apiVersion: 'nim.opendatahub.io/v1', kind: 'Account', diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index 9d2ea1d1f5..75579dcd9c 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -143,3 +143,31 @@ export const mockNimModelPVC = (): PersistentVolumeClaimKind => { export const mockNimServingResource = ( resource: ConfigMapKind | SecretKind, ): NimServingResponse => ({ body: { body: resource } }); + +export const mockOdhApplication = [ + { + metadata: { + name: 'nvidia-nim', + annotations: { + 'internal.config.kubernetes.io/previousKinds': 'OdhApplication', + }, + }, + spec: { + displayName: 'NVIDIA NIM', + provider: 'NVIDIA', + description: 'NVIDIA Inference Microservices for AI model serving.', + route: 'https://nim-route.test.com', + routeNamespace: 'redhat-ods-applications', + img: 'https://example.com/nvidia-nim.png', + docsLink: 'https://docs.nvidia.com/nim', + getStartedLink: 'https://nvidia.com/get-started-nim', + getStartedMarkDown: '**NVIDIA NIM** provides fast and efficient model serving.', + category: 'Self-managed', + shownOnEnabledPage: true, + isEnabled: true, + internalRoute: 'https://internal-nim-route.test.com', + quickStart: 'nim-quickstart-guide', + beta: false, + }, + }, +]; diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index a8cf7aec24..fdb087cd05 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -264,7 +264,7 @@ describe('NIM Model Serving', () => { disableModelMesh: false, disableNIMModelServing: false, }, - true, + // true, ); projectDetailsOverviewTab.visit('test-project'); cy.findByTestId('model-serving-platform-button').should('not.exist'); @@ -277,7 +277,7 @@ describe('NIM Model Serving', () => { disableModelMesh: false, disableNIMModelServing: false, }, - true, + // true, ); projectDetailsOverviewTab.visit('test-project'); projectDetailsOverviewTab.findModelServingPlatform('nvidia-nim').should('not.exist'); @@ -291,7 +291,7 @@ describe('NIM Model Serving', () => { disableModelMesh: false, disableNIMModelServing: false, }, - true, + // true, ); projectDetails.visitSection('test-project', 'model-server'); cy.get('button[data-testid=deploy-button]').should('not.exist'); @@ -304,7 +304,7 @@ describe('NIM Model Serving', () => { disableModelMesh: false, disableNIMModelServing: false, }, - true, + // true, ); projectDetails.visitSection('test-project', 'model-server'); projectDetails.findModelServingPlatform('nvidia-nim-model').should('not.exist'); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 7bf22a7c6c..58463166a5 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -16,7 +16,6 @@ import { PVCModel, SecretModel, ServingRuntimeModel, - TemplateModel, } from '~/__tests__/cypress/cypress/utils/models'; import { mockNimImages, @@ -25,9 +24,9 @@ import { mockNimProject, mockNimServingResource, mockNimServingRuntime, - mockNimServingRuntimeTemplate, mockNvidiaNimAccessSecret, mockNvidiaNimImagePullSecret, + mockOdhApplication, } from '~/__mocks__/mockNimResource'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import type { InferenceServiceKind } from '~/k8sTypes'; @@ -62,11 +61,15 @@ export const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimCon }), ); + cy.interceptOdh('GET /api/components', mockOdhApplication); + + cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})])); + cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockNimProject(hasAllModels)])); - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); + // const templateMock = mockNimServingRuntimeTemplate(); + // cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + // cy.interceptK8s(TemplateModel, templateMock); cy.interceptK8sList( AcceleratorProfileModel, @@ -79,8 +82,6 @@ export const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimCon total: { 'nvidia.com/gpu': 1 }, allocated: { 'nvidia.com/gpu': 1 }, }); - - cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})])); }; // intercept all APIs required for deploying new NIM models in existing projects @@ -149,16 +150,16 @@ export const initInterceptorsValidatingNimEnablement = ( ): void => { cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); - if (!disableServingRuntime) { - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); - } + cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})])); + + // if (!disableServingRuntime) { + // const templateMock = mockNimServingRuntimeTemplate(); + // cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + // cy.interceptK8s(TemplateModel, templateMock); + // } cy.interceptK8sList( ProjectModel, mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]), ); - - cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})])); }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index ef03093ecc..8221d80a66 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -19,21 +19,21 @@ import { ReduxContext } from './redux/context'; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(document.getElementById('root')!); root.render( - - - - - - - - - - - - - - + // + + + + + + + + + + + + - - , + + , + // , ); diff --git a/frontend/src/pages/modelServing/ModelServingContext.tsx b/frontend/src/pages/modelServing/ModelServingContext.tsx index 398ee439e5..c282a32669 100644 --- a/frontend/src/pages/modelServing/ModelServingContext.tsx +++ b/frontend/src/pages/modelServing/ModelServingContext.tsx @@ -9,11 +9,11 @@ import { import { useNavigate } from 'react-router-dom'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { - ServingRuntimeKind, InferenceServiceKind, - TemplateKind, ProjectKind, SecretKind, + ServingRuntimeKind, + TemplateKind, } from '~/k8sTypes'; import { DEFAULT_CONTEXT_DATA, DEFAULT_LIST_WATCH_RESULT } from '~/utilities/const'; import { ContextResourceData, CustomWatchK8sResult } from '~/types'; @@ -22,10 +22,11 @@ import { useDashboardNamespace } from '~/redux/selectors'; import { DataConnection } from '~/pages/projects/types'; import useDataConnections from '~/pages/projects/screens/detail/data-connections/useDataConnections'; import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject'; -import { ProjectsContext, byName } from '~/concepts/projects/ProjectsContext'; -import { SupportedArea, conditionalArea } from '~/concepts/areas'; +import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext'; +import { conditionalArea, SupportedArea } from '~/concepts/areas'; import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; import { useTemplates } from '~/api'; +import { ServingPlatformStatuses } from '~/pages/modelServing/screens/types'; import useInferenceServices from './useInferenceServices'; import useServingRuntimes from './useServingRuntimes'; import useTemplateOrder from './customServingRuntimes/useTemplateOrder'; @@ -46,6 +47,7 @@ type ModelServingContextType = { preferredProject: ProjectKind | null; serverSecrets: ContextResourceData; projects: ProjectKind[] | null; + servingPlatformStatuses: ServingPlatformStatuses; }; type ModelServingContextProviderProps = { @@ -67,6 +69,22 @@ export const ModelServingContext = React.createContext( project: null, preferredProject: null, projects: null, + servingPlatformStatuses: { + kServe: { + enabled: false, + installed: false, + }, + kServeNIM: { + enabled: false, + installed: false, + isLoaded: false, + }, + modelMesh: { + enabled: false, + installed: false, + }, + platformEnabledCount: 0, + }, }); const ModelServingContextProvider = conditionalArea( @@ -92,6 +110,7 @@ const ModelServingContextProvider = conditionalArea(useDataConnections(namespace)); + const servingPlatformStatuses = useServingPlatformStatuses(); const servingRuntimeRefresh = servingRuntimes.refresh; const inferenceServiceRefresh = inferenceServices.refresh; @@ -102,13 +121,8 @@ const ModelServingContextProvider = conditionalArea {children} diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx index ff34ef8080..dee0176440 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx @@ -11,6 +11,7 @@ import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable'; import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext'; import { isProjectNIMSupported } from '~/pages/modelServing/screens/projects/nimUtils'; +import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; import InferenceServiceEndpoint from './InferenceServiceEndpoint'; import InferenceServiceProject from './InferenceServiceProject'; import InferenceServiceStatus from './InferenceServiceStatus'; @@ -38,6 +39,8 @@ const InferenceServiceTableRow: React.FC = ({ const { projects } = React.useContext(ProjectsContext); const project = projects.find(byName(inferenceService.metadata.namespace)) ?? null; const isKServeNIMEnabled = project ? isProjectNIMSupported(project) : false; + const servingPlatformStatuses = useServingPlatformStatuses(); + const isNIMAvailable = servingPlatformStatuses.kServeNIM.enabled; const [modelMetricsEnabled] = useModelMetricsEnabled(); const kserveMetricsEnabled = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; @@ -112,6 +115,7 @@ const InferenceServiceTableRow: React.FC = ({ onClick: () => { onEditInferenceService(inferenceService); }, + isDisabled: !isNIMAvailable && isKServeNIMEnabled, }, { isSeparator: true }, { diff --git a/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx b/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx index c7a5d9b5d1..13eebbde5b 100644 --- a/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx +++ b/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, Skeleton, Tooltip } from '@patternfly/react-core'; import { useParams } from 'react-router-dom'; import ManageInferenceServiceModal from '~/pages/modelServing/screens/projects/InferenceServiceModal/ManageInferenceServiceModal'; import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; @@ -12,7 +12,6 @@ import { ServingRuntimePlatform } from '~/types'; import { getProjectModelServingPlatform } from '~/pages/modelServing/screens/projects/utils'; import ManageKServeModal from '~/pages/modelServing/screens/projects/kServeModal/ManageKServeModal'; import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext'; -import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; import { isProjectNIMSupported } from '~/pages/modelServing/screens/projects/nimUtils'; import ManageNIMServingModal from '~/pages/modelServing/screens/projects/NIMServiceModal/ManageNIMServingModal'; @@ -27,10 +26,12 @@ const ServeModelButton: React.FC = () => { servingRuntimeTemplateOrder: { data: templateOrder }, servingRuntimeTemplateDisablement: { data: templateDisablement }, dataConnections: { data: dataConnections }, + servingPlatformStatuses, } = React.useContext(ModelServingContext); const { projects } = React.useContext(ProjectsContext); const { namespace } = useParams<{ namespace: string }>(); - const servingPlatformStatuses = useServingPlatformStatuses(); + const isNIMAvailable = servingPlatformStatuses.kServeNIM.enabled; + const nimLoaded = servingPlatformStatuses.kServeNIM.isLoaded; const project = projects.find(byName(namespace)); @@ -58,7 +59,7 @@ const ServeModelButton: React.FC = () => { getProjectModelServingPlatform(project, servingPlatformStatuses).platform, ) } - isAriaDisabled={!project || !templatesEnabled} + isAriaDisabled={!project || (!isNIMAvailable && isKServeNIMEnabled)} > Deploy model @@ -67,7 +68,19 @@ const ServeModelButton: React.FC = () => { if (!project) { return ( - {deployButton} +
{deployButton}
+
+ ); + } + + if (isKServeNIMEnabled && !nimLoaded) { + return ; + } + + if (!isNIMAvailable && isKServeNIMEnabled) { + return ( + +
{deployButton}
); } diff --git a/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx b/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx index b950d71714..76cc83dc86 100644 --- a/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx +++ b/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx @@ -20,6 +20,7 @@ const EmptySingleModelServingCard: React.FC = setErrorSelectingPlatform, }) => { const { currentProject } = React.useContext(ProjectDetailsContext); + return ( { // deployingFromRegistry = User came from the Model Registry page because this project didn't have a serving platform selected const deployingFromRegistry = !!(modelRegistryName && registeredModelId && modelVersionId); - const servingPlatformStatuses = useServingPlatformStatuses(); - const kServeEnabled = servingPlatformStatuses.kServe.enabled; - const isNIMAvailable = servingPlatformStatuses.kServeNIM.enabled; - const modelMeshEnabled = servingPlatformStatuses.modelMesh.enabled; - const { servingRuntimes: { data: servingRuntimes, @@ -80,8 +74,13 @@ const ModelServingPlatform: React.FC = () => { serverSecrets: { refresh: refreshTokens }, inferenceServices: { refresh: refreshInferenceServices }, currentProject, + servingPlatformStatuses, } = React.useContext(ProjectDetailsContext); + const kServeEnabled = servingPlatformStatuses.kServe.enabled; + const modelMeshEnabled = servingPlatformStatuses.modelMesh.enabled; + const isNIMAvailable = servingPlatformStatuses.kServeNIM.enabled; + const isKServeNIMEnabled = isProjectNIMSupported(currentProject); const templatesSorted = getSortedTemplates(templates, templateOrder); @@ -140,6 +139,12 @@ const ModelServingPlatform: React.FC = () => { testId={`${isProjectModelMesh ? 'add-server' : 'deploy'}-button`} emptyTemplates={emptyTemplates} variant="primary" + // isNimDisabled={!isNIMAvailable && isKServeNIMEnabled} + // tooltipContent={ + // !isNIMAvailable && isKServeNIMEnabled + // ? 'NIM is not available. Contact your administrator.' + // : undefined + // } onClick={() => { setPlatformSelected( isProjectModelMesh ? ServingRuntimePlatform.MULTI : ServingRuntimePlatform.SINGLE, @@ -221,6 +226,12 @@ const ModelServingPlatform: React.FC = () => { isProjectModelMesh={isProjectModelMesh} testId={`${isProjectModelMesh ? 'add-server' : 'deploy'}-button`} emptyTemplates={emptyTemplates} + // isNimDisabled={!isNIMAvailable && isKServeNIMEnabled} + // tooltipContent={ + // !isNIMAvailable && isKServeNIMEnabled + // ? 'NIM is not available. Contact your administrator.' + // : undefined + // } onClick={() => { setPlatformSelected( isProjectModelMesh diff --git a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx index 4f7057ce24..8d35cb160e 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { Button, Tooltip, Content, ButtonProps } from '@patternfly/react-core'; +import { Button, ButtonProps, Content, Tooltip } from '@patternfly/react-core'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; +import { isProjectNIMSupported } from '~/pages/modelServing/screens/projects/nimUtils'; type ModelServingPlatformButtonActionProps = ButtonProps & { isProjectModelMesh: boolean; @@ -17,13 +18,18 @@ const ModelServingPlatformButtonAction: React.FC { const { servingRuntimeTemplates: [, templatesLoaded], + servingPlatformStatuses, + currentProject, } = React.useContext(ProjectDetailsContext); + const isNIMAvailable = servingPlatformStatuses.kServeNIM.enabled; + const isKServeNIMEnabled = isProjectNIMSupported(currentProject); + const isNimDisabled = !isNIMAvailable && isKServeNIMEnabled; - const actionButton = () => ( + const actionButton = (