Skip to content

Commit

Permalink
feat: fetching resources names from the Account CR (#3535)
Browse files Browse the repository at this point in the history
* feat: fetching resources names from the Account CR

Signed-off-by: Olga Lavtar <[email protected]>

* feat: fixed unit tests

Signed-off-by: Olga Lavtar <[email protected]>

* feat: fixed cypress tests

Signed-off-by: Olga Lavtar <[email protected]>

* feat: fixed unit test after code changes.

Signed-off-by: Olga Lavtar <[email protected]>

* feat: applied changes based on the comments.

Signed-off-by: Olga Lavtar <[email protected]>

* feat: rename accounts.ts to nimAccounts.ts

Signed-off-by: Olga Lavtar <[email protected]>

* fix: pass resource key instead of the resource name.

Signed-off-by: Olga Lavtar <[email protected]>

* fix: pass resource key instead of the resource name.

Signed-off-by: Olga Lavtar <[email protected]>

* fix: remove NIMAccountConstants

Signed-off-by: Olga Lavtar <[email protected]>

* fix: unit test fix

Signed-off-by: Olga Lavtar <[email protected]>

* fix: using lodash to get the resourceName

Signed-off-by: Olga Lavtar <[email protected]>

* fix: rename listAccounts to listNIMAccounts

Signed-off-by: Olga Lavtar <[email protected]>

* fix: remove dashboardNamespace

Signed-off-by: Olga Lavtar <[email protected]>

* fix: remove namespace when calling getNIMAccount, rename createNIMSecret to manageNIMSecret

Signed-off-by: Olga Lavtar <[email protected]>

---------

Signed-off-by: Olga Lavtar <[email protected]>
  • Loading branch information
olavtar authored Dec 16, 2024
1 parent c5b0a95 commit 47f520e
Show file tree
Hide file tree
Showing 20 changed files with 341 additions and 114 deletions.
4 changes: 2 additions & 2 deletions backend/src/routes/api/integrations/nim/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FastifyReply, FastifyRequest } from 'fastify';
import { secureAdminRoute } from '../../../../utils/route-security';
import { KubeFastifyInstance } from '../../../../types';
import { isString } from 'lodash';
import { createNIMAccount, createNIMSecret, getNIMAccount, isAppEnabled } from './nimUtils';
import { createNIMAccount, getNIMAccount, isAppEnabled, manageNIMSecret } from './nimUtils';

module.exports = async (fastify: KubeFastifyInstance) => {
const PAGE_NOT_FOUND_MESSAGE = '404 page not found';
Expand Down Expand Up @@ -62,7 +62,7 @@ module.exports = async (fastify: KubeFastifyInstance) => {
const enableValues = request.body;

try {
await createNIMSecret(fastify, enableValues);
await manageNIMSecret(fastify, enableValues);
// Ensure the account exists
try {
const account = await getNIMAccount(fastify);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/api/integrations/nim/nimUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const createNIMAccount = async (fastify: KubeFastifyInstance): Promise<NI
return Promise.resolve(response.body as NIMAccountKind);
};

export const createNIMSecret = async (
export const manageNIMSecret = async (
fastify: KubeFastifyInstance,
enableValues: { [key: string]: string },
): Promise<{ secret: SecretKind }> => {
Expand Down
57 changes: 35 additions & 22 deletions backend/src/routes/api/nim-serving/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
import { KubeFastifyInstance, OauthFastifyRequest } from '../../../types';
import { createCustomError } from '../../../utils/requestUtils';
import { logRequestDetails } from '../../../utils/fileUtils';

const secretNames = ['nvidia-nim-access', 'nvidia-nim-image-pull'];
const configMapName = 'nvidia-nim-images-data';
import { getNIMAccount } from '../integrations/nim/nimUtils';
import { get } from 'lodash';

export default async (fastify: KubeFastifyInstance): Promise<void> => {
const resourceMap: Record<string, { type: 'Secret' | 'ConfigMap'; path: string[] }> = {
apiKeySecret: { type: 'Secret', path: ['spec', 'apiKeySecret', 'name'] },
nimPullSecret: { type: 'Secret', path: ['status', 'nimPullSecret', 'name'] },
nimConfig: { type: 'ConfigMap', path: ['status', 'nimConfig', 'name'] },
};

fastify.get(
'/:nimResource',
async (
request: OauthFastifyRequest<{
Params: { nimResource: string };
}>,
) => {
async (request: OauthFastifyRequest<{ Params: { nimResource: string } }>) => {
logRequestDetails(fastify, request);
const { nimResource } = request.params;
const { coreV1Api, namespace } = fastify.kube;

if (secretNames.includes(nimResource)) {
try {
return await coreV1Api.readNamespacedSecret(nimResource, namespace);
} catch (e) {
fastify.log.error(`Failed to fetch secret ${nimResource}: ${e.message}`);
throw createCustomError('Not found', 'Secret not found', 404);
}
// Fetch the Account CR to determine the actual resource name dynamically
const account = await getNIMAccount(fastify);
if (!account) {
throw createCustomError('Not found', 'NIM account not found', 404);
}

const resourceInfo = resourceMap[nimResource];
if (!resourceInfo) {
throw createCustomError('Not found', `Invalid resource type: ${nimResource}`, 404);
}

const resourceName = get(account, resourceInfo.path);
if (!resourceName) {
fastify.log.error(`Resource name for '${nimResource}' not found in account CR.`);
throw createCustomError('Not found', `${nimResource} name not found in account`, 404);
}

if (nimResource === configMapName) {
try {
return await coreV1Api.readNamespacedConfigMap(configMapName, namespace);
} catch (e) {
fastify.log.error(`Failed to fetch configMap ${nimResource}: ${e.message}`);
throw createCustomError('Not found', 'ConfigMap not found', 404);
try {
// Fetch the resource from Kubernetes using the dynamically retrieved name
if (resourceInfo.type === 'Secret') {
return await coreV1Api.readNamespacedSecret(resourceName, namespace);
} else {
return await coreV1Api.readNamespacedConfigMap(resourceName, namespace);
}
} catch (e: any) {
fastify.log.error(
`Failed to fetch ${resourceInfo.type.toLowerCase()} ${resourceName}: ${e.message}`,
);
throw createCustomError('Not found', `${resourceInfo.type} not found`, 404);
}
throw createCustomError('Not found', 'Resource not found', 404);
},
);
};
43 changes: 43 additions & 0 deletions frontend/src/__mocks__/mockNimAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { K8sCondition, NIMAccountKind } from '~/k8sTypes';

type MockResourceConfigType = {
name?: string;
namespace?: string;
uid?: string;
apiKeySecretName?: string;
nimConfigName?: string;
runtimeTemplateName?: string;
nimPullSecretName?: string;
conditions?: K8sCondition[];
};

export const mockNimAccount = ({
name = 'odh-nim-account',
namespace = 'opendatahub',
uid = 'test-uid',
apiKeySecretName = 'mock-nvidia-nim-access',
nimConfigName = 'mock-nvidia-nim-images-data',
runtimeTemplateName = 'mock-nvidia-nim-serving-template',
nimPullSecretName = 'mock-nvidia-nim-image-pull',
conditions = [],
}: MockResourceConfigType): NIMAccountKind => ({
apiVersion: 'nim.opendatahub.io/v1',
kind: 'Account',
metadata: {
name,
namespace,
uid,
creationTimestamp: new Date().toISOString(),
},
spec: {
apiKeySecret: {
name: apiKeySecretName,
},
},
status: {
nimConfig: nimConfigName ? { name: nimConfigName } : undefined,
runtimeTemplate: runtimeTemplateName ? { name: runtimeTemplateName } : undefined,
nimPullSecret: nimPullSecretName ? { name: nimPullSecretName } : undefined,
conditions,
},
});
8 changes: 4 additions & 4 deletions frontend/src/__mocks__/mockNimResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { mockPVCK8sResource } from './mockPVCK8sResource';

export const mockNimImages = (): ConfigMapKind =>
mockConfigMap({
name: 'nvidia-nim-images-data',
name: 'mock-nvidia-nim-images-data',
namespace: 'opendatahub',
data: {
alphafold2: JSON.stringify({
Expand Down Expand Up @@ -86,7 +86,7 @@ export const mockNimServingRuntime = (): ServingRuntimeKind => {

export const mockNimServingRuntimeTemplate = (): TemplateKind => {
const templateMock = mockServingRuntimeTemplateK8sResource({
name: 'nvidia-nim-serving-template',
name: 'mock-nvidia-nim-serving-template',
displayName: 'NVIDIA NIM',
platforms: [ServingRuntimePlatform.SINGLE],
apiProtocol: ServingRuntimeAPIProtocol.REST,
Expand All @@ -101,7 +101,7 @@ export const mockNimServingRuntimeTemplate = (): TemplateKind => {

export const mockNvidiaNimAccessSecret = (): SecretKind => {
const secret = mockSecretK8sResource({
name: 'nvidia-nim-access',
name: 'mock-nvidia-nim-access',
});
delete secret.data;
secret.data = {};
Expand All @@ -113,7 +113,7 @@ export const mockNvidiaNimAccessSecret = (): SecretKind => {

export const mockNvidiaNimImagePullSecret = (): SecretKind => {
const secret = mockSecretK8sResource({
name: 'nvidia-nim-image-pull',
name: 'mock-nvidia-nim-image-pull',
});
delete secret.data;
secret.data = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import type {
RegisteredModelList,
} from '~/concepts/modelRegistry/types';
import type {
ConsoleLinkKind,
DashboardConfigKind,
DataScienceClusterInitializationKindStatus,
DataScienceClusterKindStatus,
ModelRegistryKind,
NotebookKind,
OdhQuickStart,
RoleBindingKind,
ServingRuntimeKind,
TemplateKind,
NotebookKind,
ModelRegistryKind,
ConsoleLinkKind,
} from '~/k8sTypes';

import type { StartNotebookData } from '~/pages/projects/types';
Expand Down Expand Up @@ -675,7 +675,7 @@ declare global {
type: 'GET /api/nim-serving/:resource',
options: {
path: {
resource: 'nvidia-nim-images-data' | 'nvidia-nim-access' | 'nvidia-nim-image-pull';
resource: string;
};
},
response: OdhResponse<NimServingResponse>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig';
import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList';
import {
NIMAccountModel,
ProjectModel,
SecretModel,
ServiceModel,
Expand All @@ -30,6 +31,7 @@ import {
import { ServingRuntimePlatform } from '~/types';
import { kserveModal } from '~/__tests__/cypress/cypress/pages/modelServing';
import { mockModelArtifact } from '~/__mocks__/mockModelArtifact';
import { mockNimAccount } from '~/__mocks__/mockNimAccount';

const MODEL_REGISTRY_API_VERSION = 'v1alpha3';

Expand Down Expand Up @@ -177,6 +179,8 @@ const initIntercepts = ({
{ namespace: 'opendatahub' },
),
);

cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})]));
};

describe('Deploy model version', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,24 @@ import { ServingRuntimePlatform } from '~/types';
import {
DataSciencePipelineApplicationModel,
ImageStreamModel,
InferenceServiceModel,
NIMAccountModel,
NotebookModel,
PVCModel,
PodModel,
ProjectModel,
PVCModel,
RouteModel,
SecretModel,
ServiceAccountModel,
TemplateModel,
InferenceServiceModel,
ServingRuntimeModel,
TemplateModel,
} from '~/__tests__/cypress/cypress/utils/models';
import { mockServingRuntimeK8sResource } from '~/__mocks__/mockServingRuntimeK8sResource';
import { mockInferenceServiceK8sResource } from '~/__mocks__/mockInferenceServiceK8sResource';
import { asProjectAdminUser } from '~/__tests__/cypress/cypress/utils/mockUsers';
import { NamespaceApplicationCase } from '~/pages/projects/types';
import { mockNimServingRuntimeTemplate } from '~/__mocks__/mockNimResource';
import { mockNimAccount } from '~/__mocks__/mockNimAccount';

type HandlersProps = {
isEmpty?: boolean;
Expand Down Expand Up @@ -271,6 +273,8 @@ const initIntercepts = ({
},
buildMockPipelines(isEmpty ? [] : [mockPipelineKF({})]),
);

cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})]));
};

describe('Project Details', () => {
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
AcceleratorProfileModel,
ConfigMapModel,
InferenceServiceModel,
NIMAccountModel,
ProjectModel,
PVCModel,
SecretModel,
Expand All @@ -30,6 +31,7 @@ import {
} from '~/__mocks__/mockNimResource';
import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile';
import type { InferenceServiceKind } from '~/k8sTypes';
import { mockNimAccount } from '~/__mocks__/mockNimAccount';

/* ###################################################
###### Interception Initialization Utilities ######
Expand Down Expand Up @@ -77,6 +79,8 @@ 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 All @@ -89,23 +93,24 @@ export const initInterceptsToDeployModel = (nimInferenceService: InferenceServic

cy.interceptOdh(
`GET /api/nim-serving/:resource`,
{ path: { resource: 'nvidia-nim-images-data' } },
{ path: { resource: 'nimConfig' } },
mockNimServingResource(mockNimImages()),
);

cy.interceptOdh(
`GET /api/nim-serving/:resource`,
{ path: { resource: 'nvidia-nim-access' } },
{ path: { resource: 'apiKeySecret' } },
mockNimServingResource(mockNvidiaNimAccessSecret()),
);

cy.interceptOdh(
`GET /api/nim-serving/:resource`,
{ path: { resource: 'nvidia-nim-image-pull' } },
{ path: { resource: 'nimPullSecret' } },
mockNimServingResource(mockNvidiaNimImagePullSecret()),
);

cy.interceptK8s('POST', PVCModel, mockNimModelPVC());
cy.interceptK8s('GET', NIMAccountModel, mockNimAccount({}));
};

// intercept all APIs required for deleting an existing NIM models
Expand Down Expand Up @@ -154,4 +159,6 @@ export const initInterceptorsValidatingNimEnablement = (
ProjectModel,
mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]),
);

cy.interceptK8sList(NIMAccountModel, mockK8sResourceList([mockNimAccount({})]));
};
1 change: 1 addition & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Normal SDK/pass-through network API calls
export * from './k8s/nimAccounts';
export * from './k8s/builds';
export * from './k8s/configMaps';
export * from './k8s/events';
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/api/k8s/nimAccounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { k8sListResource } from '@openshift/dynamic-plugin-sdk-utils';
import { NIMAccountModel } from '~/api/models';
import { NIMAccountKind } from '~/k8sTypes';

export const listNIMAccounts = async (namespace: string): Promise<NIMAccountKind[]> =>
k8sListResource<NIMAccountKind>({
model: NIMAccountModel,
queryOptions: {
ns: namespace,
},
}).then((listResource) => listResource.items);
7 changes: 7 additions & 0 deletions frontend/src/api/models/odh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ export const InferenceServiceModel: K8sModelCommon = {
kind: 'InferenceService',
plural: 'inferenceservices',
};

export const NIMAccountModel: K8sModelCommon = {
apiVersion: 'v1',
apiGroup: 'nim.opendatahub.io',
kind: 'Account',
plural: 'accounts',
};
24 changes: 24 additions & 0 deletions frontend/src/k8sTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1338,3 +1338,27 @@ export type ModelRegistryKind = K8sResourceCommon & {
conditions?: K8sCondition[];
};
};

export type NIMAccountKind = K8sResourceCommon & {
metadata: {
name: string;
namespace: string;
};
spec: {
apiKeySecret: {
name: string;
};
};
status?: {
nimConfig?: {
name: string;
};
runtimeTemplate?: {
name: string;
};
nimPullSecret?: {
name: string;
};
conditions?: K8sCondition[];
};
};
Loading

0 comments on commit 47f520e

Please sign in to comment.