Skip to content

Commit

Permalink
fix: block nim re-deploy in existing projects
Browse files Browse the repository at this point in the history
Signed-off-by: Olga Lavtar <[email protected]>
  • Loading branch information
olavtar committed Dec 19, 2024
1 parent 14b1c4d commit ee45a4c
Show file tree
Hide file tree
Showing 19 changed files with 229 additions and 91 deletions.
10 changes: 9 additions & 1 deletion frontend/src/__mocks__/mockNimAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/__mocks__/mockNimResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down
29 changes: 15 additions & 14 deletions frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
PVCModel,
SecretModel,
ServingRuntimeModel,
TemplateModel,
} from '~/__tests__/cypress/cypress/utils/models';
import {
mockNimImages,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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({})]));
};
32 changes: 16 additions & 16 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
<ErrorBoundary>
<Provider store={sdkStore}>
<Provider store={store} context={ReduxContext}>
<Router>
<SDKInitialize>
<BrowserStorageContextProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</BrowserStorageContextProvider>
</SDKInitialize>
</Router>
</Provider>
// <React.StrictMode>
<ErrorBoundary>
<Provider store={sdkStore}>
<Provider store={store} context={ReduxContext}>
<Router>
<SDKInitialize>
<BrowserStorageContextProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</BrowserStorageContextProvider>
</SDKInitialize>
</Router>
</Provider>
</ErrorBoundary>
</React.StrictMode>,
</Provider>
</ErrorBoundary>,
// </React.StrictMode>,
);
35 changes: 25 additions & 10 deletions frontend/src/pages/modelServing/ModelServingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -46,6 +47,7 @@ type ModelServingContextType = {
preferredProject: ProjectKind | null;
serverSecrets: ContextResourceData<SecretKind>;
projects: ProjectKind[] | null;
servingPlatformStatuses: ServingPlatformStatuses;
};

type ModelServingContextProviderProps = {
Expand All @@ -67,6 +69,22 @@ export const ModelServingContext = React.createContext<ModelServingContextType>(
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<ModelServingContextProviderProps>(
Expand All @@ -92,6 +110,7 @@ const ModelServingContextProvider = conditionalArea<ModelServingContextProviderP
useInferenceServices(namespace),
);
const dataConnections = useContextResourceData<DataConnection>(useDataConnections(namespace));
const servingPlatformStatuses = useServingPlatformStatuses();

const servingRuntimeRefresh = servingRuntimes.refresh;
const inferenceServiceRefresh = inferenceServices.refresh;
Expand All @@ -102,13 +121,8 @@ const ModelServingContextProvider = conditionalArea<ModelServingContextProviderP
dataConnectionRefresh();
}, [servingRuntimeRefresh, inferenceServiceRefresh, dataConnectionRefresh]);

const {
kServe: { installed: kServeInstalled },
modelMesh: { installed: modelMeshInstalled },
} = useServingPlatformStatuses();

const notInstalledError =
!kServeInstalled && !modelMeshInstalled
!servingPlatformStatuses.kServe.installed && !servingPlatformStatuses.modelMesh.installed
? new Error('No model serving platform installed')
: undefined;

Expand Down Expand Up @@ -192,6 +206,7 @@ const ModelServingContextProvider = conditionalArea<ModelServingContextProviderP
project,
preferredProject,
projects,
servingPlatformStatuses,
}}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,6 +39,8 @@ const InferenceServiceTableRow: React.FC<InferenceServiceTableRowProps> = ({
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;
Expand Down Expand Up @@ -112,6 +115,7 @@ const InferenceServiceTableRow: React.FC<InferenceServiceTableRowProps> = ({
onClick: () => {
onEditInferenceService(inferenceService);
},
isDisabled: !isNIMAvailable && isKServeNIMEnabled,
},
{ isSeparator: true },
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -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));

Expand Down Expand Up @@ -58,7 +59,7 @@ const ServeModelButton: React.FC = () => {
getProjectModelServingPlatform(project, servingPlatformStatuses).platform,
)
}
isAriaDisabled={!project || !templatesEnabled}
isAriaDisabled={!project || (!isNIMAvailable && isKServeNIMEnabled)}
>
Deploy model
</Button>
Expand All @@ -67,7 +68,19 @@ const ServeModelButton: React.FC = () => {
if (!project) {
return (
<Tooltip data-testid="deploy-model-tooltip" content="To deploy a model, select a project.">
{deployButton}
<div>{deployButton}</div>
</Tooltip>
);
}

if (isKServeNIMEnabled && !nimLoaded) {
return <Skeleton style={{ minWidth: 100 }} fontSize="3xl" />;
}

if (!isNIMAvailable && isKServeNIMEnabled) {
return (
<Tooltip content="NIM is not available. Contact your administrator.">
<div>{deployButton}</div>
</Tooltip>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const EmptySingleModelServingCard: React.FC<EmptySingleModelServingCardProps> =
setErrorSelectingPlatform,
}) => {
const { currentProject } = React.useContext(ProjectDetailsContext);

return (
<Card
style={{
Expand Down
Loading

0 comments on commit ee45a4c

Please sign in to comment.