Skip to content

Commit 7276738

Browse files
committed
Merge branch 'main' into RHOAIENG-36419-Improve-error-handling-in-BFF-API-for-LlamaStack-errors-to-UI
2 parents 078bba5 + 0573b09 commit 7276738

230 files changed

Lines changed: 9458 additions & 3796 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/__tests__/featureStoreUtils.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const NAMESPACE = {
1515
DEFAULT: 'default',
1616
TEST_NS: 'test-ns',
1717
NAMESPACE_WITH_DASH: 'namespace-with-dash',
18+
TEST_FEAST_BANKING: 'test-feast-banking',
1819
} as const;
1920

2021
const PROJECT = {
@@ -23,18 +24,24 @@ const PROJECT = {
2324
TEST: 'test',
2425
ENABLED_2: 'enabled-2',
2526
NO_LABELS: 'no-labels',
27+
FRAUD_DETECT: 'fraud_detect',
28+
CREDIT_SCORING_LOCAL: 'credit_scoring_local',
2629
} as const;
2730

2831
const CONFIG_NAME = {
2932
BANKING: 'banking-config',
3033
RETAIL: 'retail-config',
34+
FRAUD_DETECT: 'feast-fraud-detect-client',
35+
CREDIT_SCORING_LOCAL: 'feast-sample-git-client',
3136
} as const;
3237

3338
const SERVICE_NAME = {
3439
BANKING_REST: 'feast-banking-registry-rest',
3540
RETAIL_REST: 'feast-retail-registry-rest',
3641
TEST_REST: 'feast-test-registry-rest',
3742
COMPLEX_REST: 'feast-my-complex-name-registry-rest',
43+
FRAUD_DETECT_REST: 'feast-fraud-detect-registry-rest',
44+
CREDIT_SCORING_REST: 'feast-sample-git-registry-rest',
3845
} as const;
3946

4047
const REGISTRY_URL = {
@@ -44,6 +51,8 @@ const REGISTRY_URL = {
4451
RETAIL_HTTPS: 'https://feast-retail-registry.default.svc.cluster.local',
4552
TEST_NO_PROTOCOL: 'feast-test-registry.test-ns.svc.cluster.local',
4653
COMPLEX_NAME: 'feast-my-complex-name-registry.namespace-with-dash.svc.cluster.local',
54+
FRAUD_DETECT_HTTPS: 'https://feast-fraud-detect-registry.test-feast-banking.svc.cluster.local',
55+
CREDIT_SCORING_HTTPS: 'https://feast-sample-git-registry.credit-namespace.svc.cluster.local',
4756
INVALID: 'invalid-url-format',
4857
} as const;
4958

@@ -81,7 +90,7 @@ const createClientConfigInfo = (overrides: Partial<ClientConfigInfo> = {}): Clie
8190
});
8291

8392
const createFeatureStoreCRD = (overrides: Partial<FeatureStoreCRD> = {}): FeatureStoreCRD => ({
84-
apiVersion: 'feast.dev/v1alpha1',
93+
apiVersion: 'feast.dev/v1',
8594
kind: 'FeatureStore',
8695
metadata: {
8796
name: PROJECT.BANKING,

backend/src/routes/api/featurestores/featureStores.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async function fetchFeatureStoreCRDs(
5656
try {
5757
const response = (await fastify.kube.customObjectsApi.listNamespacedCustomObject(
5858
'feast.dev',
59-
'v1alpha1',
59+
'v1',
6060
namespace,
6161
'featurestores',
6262
)) as CustomObjectResponse;

backend/src/routes/api/featurestores/fsworkbenchIntegration.ts

Lines changed: 36 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,23 @@ import { KubeFastifyInstance, OauthFastifyRequest } from '../../../types';
33
import { getAccessToken, getDirectCallOptions } from '../../../utils/directCallUtils';
44
import { secureRoute } from '../../../utils/route-security';
55
import { getNamespaces } from '../../../utils/notebookUtils';
6-
import { orderBy } from 'lodash';
76
import {
87
parseNamespacesData,
98
getClientConfigs,
109
fetchFromRegistry,
1110
fetchConfigMap,
12-
batchFetchConfigMapsByNamespace,
1311
type ClientConfigInfo,
1412
handleError,
1513
} from './featureStoreUtils';
16-
import { V1ConfigMap } from '@kubernetes/client-node';
17-
18-
interface WorkbenchFeatureStoreConfig {
19-
namespace: string;
20-
configName: string;
21-
configMap: V1ConfigMap | null;
22-
hasAccessToFeatureStore: boolean;
23-
}
2414

2515
interface WorkbenchResponse {
26-
clientConfigs: WorkbenchFeatureStoreConfig[];
2716
namespaces: Array<{
2817
namespace: string;
29-
clientConfigs: string[];
18+
clientConfigs: Array<{
19+
configName: string;
20+
projectName: string;
21+
hasAccessToFeatureStore: boolean;
22+
}>;
3023
}>;
3124
}
3225

@@ -38,7 +31,10 @@ async function checkFeatureStoreAccess(
3831
): Promise<boolean> {
3932
try {
4033
const projects = await fetchFromRegistry(fastify, registryUrl, userToken);
41-
const hasAccess = projects && projects.length > 0;
34+
if (!projects || projects.length === 0) {
35+
return false;
36+
}
37+
const hasAccess = projects.some((p) => p.name === projectName || p.project === projectName);
4238
return hasAccess;
4339
} catch (error) {
4440
fastify.log.info(
@@ -74,32 +70,6 @@ async function checkMultipleFeatureStoreAccess(
7470
return accessResults;
7571
}
7672

77-
async function processNamespaceConfigs(
78-
fastify: KubeFastifyInstance,
79-
namespace: string,
80-
clientConfigs: string[],
81-
token?: string,
82-
): Promise<WorkbenchFeatureStoreConfig[]> {
83-
const clientConfigsData = await getClientConfigs(fastify, { [namespace]: clientConfigs });
84-
85-
if (clientConfigsData.length === 0) {
86-
return [];
87-
}
88-
89-
const accessResults = await checkMultipleFeatureStoreAccess(fastify, clientConfigsData, token);
90-
91-
const configMapMap = await batchFetchConfigMapsByNamespace(fastify, clientConfigsData);
92-
93-
const configsWithAccess = clientConfigsData.map((config) => ({
94-
namespace: config.namespace,
95-
configName: config.configName,
96-
configMap: configMapMap.get(`${config.namespace}/${config.configName}`) || null,
97-
hasAccessToFeatureStore: accessResults.get(config.projectName) || false,
98-
}));
99-
100-
return orderBy(configsWithAccess, ['hasAccessToFeatureStore'], ['desc']);
101-
}
102-
10373
export default async (fastify: KubeFastifyInstance): Promise<void> => {
10474
fastify.get(
10575
'/workbench-integration',
@@ -114,7 +84,6 @@ export default async (fastify: KubeFastifyInstance): Promise<void> => {
11484

11585
if (!feastConfig || !feastConfig.data?.namespaces) {
11686
return reply.send({
117-
clientConfigs: [],
11887
namespaces: [],
11988
});
12089
}
@@ -129,20 +98,38 @@ export default async (fastify: KubeFastifyInstance): Promise<void> => {
12998

13099
const token = await getAccessToken(await getDirectCallOptions(fastify, req, ''));
131100

132-
const namespacePromises = namespaces.map(({ namespace: ns, clientConfigs }) =>
133-
processNamespaceConfigs(fastify, ns, clientConfigs, token),
101+
const allClientConfigsData = await getClientConfigs(
102+
fastify,
103+
Object.fromEntries(
104+
namespaces.map(({ namespace, clientConfigs }) => [namespace, clientConfigs]),
105+
),
134106
);
135107

136-
const namespaceResults = await Promise.all(namespacePromises);
137-
const allClientConfigs = orderBy(
138-
namespaceResults.flat(),
139-
['hasAccessToFeatureStore'],
140-
['desc'],
108+
const accessResults = await checkMultipleFeatureStoreAccess(
109+
fastify,
110+
allClientConfigsData,
111+
token,
141112
);
142113

114+
const namespaceResults = namespaces.map(({ namespace }) => {
115+
const namespaceConfigs = allClientConfigsData.filter(
116+
(config) => config.namespace === namespace,
117+
);
118+
const accessibleConfigs = namespaceConfigs
119+
.filter((config) => accessResults.get(config.projectName))
120+
.map((config) => ({
121+
configName: config.configName,
122+
projectName: config.projectName,
123+
hasAccessToFeatureStore: accessResults.get(config.projectName) ?? false,
124+
}));
125+
return {
126+
namespace,
127+
clientConfigs: accessibleConfigs,
128+
};
129+
});
130+
143131
const response: WorkbenchResponse = {
144-
clientConfigs: allClientConfigs,
145-
namespaces,
132+
namespaces: namespaceResults.filter((ns) => ns.clientConfigs.length > 0),
146133
};
147134

148135
return reply.send(response);

backend/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ export type DashboardConfig = K8sResourceCommon & {
4949
disableKueue: boolean;
5050
disableLMEval: boolean;
5151
disableFeatureStore: boolean;
52+
trainingJobs: boolean;
5253
genAiStudio: boolean;
5354
modelAsService: boolean;
55+
mlflow: boolean;
5456
};
5557
// Intentionally disjointed from the CRD, we should move away from this code-wise now; CRD later
5658
// groupsConfig?: {

backend/src/utils/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export const blankDashboardCR: DashboardConfig = {
8585
disableFineTuning: true,
8686
disableKueue: true,
8787
disableLMEval: true,
88+
mlflow: false,
89+
trainingJobs: false,
8890
},
8991
notebookController: {
9092
enabled: true,

frontend/src/__mocks__/mockDashboardConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ export type MockDashboardConfigType = {
4646
hardwareProfileOrder?: string[];
4747
pvcSize?: string;
4848
mlflow?: boolean;
49+
projectRBAC?: boolean;
50+
embedMLflow?: boolean;
4951
};
5052

5153
export const mockDashboardConfig = ({
5254
mlflow = false,
55+
projectRBAC = false,
56+
embedMLflow = false,
5357
disableInfo = false,
5458
disableSupport = false,
5559
disableClusterManager = false,
@@ -209,6 +213,8 @@ export const mockDashboardConfig = ({
209213
spec: {
210214
dashboardConfig: {
211215
mlflow,
216+
projectRBAC,
217+
embedMLflow,
212218
enablement: true,
213219
disableInfo,
214220
disableSupport,

frontend/src/__mocks__/mockLLMInferenceServiceK8sResource.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type LLMInferenceServiceKind } from '@odh-dashboard/llmd-serving/types';
22
import { genUID } from '@odh-dashboard/internal/__mocks__/mockUtils';
33
import { ServingRuntimeModelType } from '@odh-dashboard/internal/types';
4+
import { ModelAnnotation } from '#~/pages/projects/screens/spawner/storage/types.ts';
45

56
type MockLLMInferenceServiceConfigType = {
67
name?: string;
@@ -17,6 +18,7 @@ type MockLLMInferenceServiceConfigType = {
1718
additionalLabels?: Record<string, string>;
1819
isNonDashboardItem?: boolean;
1920
modelType?: ServingRuntimeModelType;
21+
isStopped?: boolean;
2022
};
2123

2224
export const mockLLMInferenceServiceK8sResource = ({
@@ -31,6 +33,7 @@ export const mockLLMInferenceServiceK8sResource = ({
3133
lastTransitionTime = '2023-03-17T16:12:41Z',
3234
isReady = true,
3335
url,
36+
isStopped = false,
3437
}: MockLLMInferenceServiceConfigType): LLMInferenceServiceKind => ({
3538
apiVersion: 'serving.kserve.io/v1alpha1',
3639
kind: 'LLMInferenceService',
@@ -40,6 +43,7 @@ export const mockLLMInferenceServiceK8sResource = ({
4043
'opendatahub.io/hardware-profile-name': 'small-profile',
4144
'opendatahub.io/hardware-profile-namespace': 'opendatahub',
4245
'opendatahub.io/model-type': ServingRuntimeModelType.GENERATIVE,
46+
...(isStopped ? { [ModelAnnotation.STOPPED_ANNOTATION]: 'true' } : {}),
4347
},
4448
creationTimestamp,
4549
...(deleted ? { deletionTimestamp: new Date().toUTCString() } : {}),

frontend/src/__mocks__/mockProjectK8sResource.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type MockResourceConfigType = {
1313
enableNIM?: boolean;
1414
isDSProject?: boolean;
1515
phase?: 'Active' | 'Terminating';
16+
enableKueue?: boolean;
1617
};
1718

1819
export const mockProjectK8sResource = ({
@@ -26,6 +27,7 @@ export const mockProjectK8sResource = ({
2627
description = '',
2728
isDSProject = true,
2829
phase = 'Active',
30+
enableKueue = false,
2931
}: MockResourceConfigType): ProjectKind => ({
3032
kind: 'Project',
3133
apiVersion: 'project.openshift.io/v1',
@@ -39,6 +41,7 @@ export const mockProjectK8sResource = ({
3941
[KnownLabels.MODEL_SERVING_PROJECT]: 'false',
4042
}),
4143
...(isDSProject && { [KnownLabels.DASHBOARD_RESOURCE]: 'true' }),
44+
...(enableKueue && { 'kueue.openshift.io/managed': 'true' }),
4245
},
4346
...(hasAnnotations && {
4447
annotations: {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { proxyGET } from '#~/api/proxyUtils';
2+
import { K8sAPIOptions } from '#~/k8sTypes';
3+
4+
export type WorkbenchFeatureStoreResponse = {
5+
namespaces: Array<{
6+
namespace: string;
7+
clientConfigs: Array<{
8+
configName: string;
9+
projectName: string;
10+
hasAccessToFeatureStore: boolean;
11+
}>;
12+
}>;
13+
};
14+
15+
export const getWorkbenchFeatureStores = (
16+
opts?: K8sAPIOptions,
17+
): Promise<WorkbenchFeatureStoreResponse> =>
18+
proxyGET<WorkbenchFeatureStoreResponse>('', '/api/featurestores/workbench-integration', {}, opts);

frontend/src/api/k8s/__tests__/roleBindings.spec.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ import {
33
k8sCreateResource,
44
k8sDeleteResource,
55
k8sGetResource,
6-
k8sListResource,
76
k8sPatchResource,
7+
k8sListResourceItems,
88
} from '@openshift/dynamic-plugin-sdk-utils';
99
import { mockRoleBindingK8sResource } from '#~/__mocks__/mockRoleBindingK8sResource';
10-
import { mockK8sResourceList } from '#~/__mocks__/mockK8sResourceList';
1110
import { mock200Status, mock404Error } from '#~/__mocks__/mockK8sStatus';
1211
import { KnownLabels, RoleBindingKind, RoleBindingSubject } from '#~/k8sTypes';
1312
import {
@@ -27,18 +26,18 @@ import {
2726
} from '#~/concepts/roleBinding/types';
2827

2928
jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({
30-
k8sListResource: jest.fn(),
3129
k8sGetResource: jest.fn(),
3230
k8sCreateResource: jest.fn(),
3331
k8sDeleteResource: jest.fn(),
3432
k8sPatchResource: jest.fn(),
33+
k8sListResourceItems: jest.fn(),
3534
}));
3635

37-
const k8sListResourceMock = jest.mocked(k8sListResource<RoleBindingKind>);
3836
const k8sGetResourceMock = jest.mocked(k8sGetResource);
3937
const k8sCreateResourceMock = jest.mocked(k8sCreateResource);
4038
const k8sDeleteResourceMock = jest.mocked(k8sDeleteResource<RoleBindingKind, K8sStatus>);
4139
const k8sPatchResourceMock = jest.mocked(k8sPatchResource<RoleBindingKind>);
40+
const k8sListResourceItemsMock = jest.mocked(k8sListResourceItems<RoleBindingKind>);
4241

4342
const roleBindingMock = mockRoleBindingK8sResource({});
4443
const namespace = 'namespace';
@@ -222,32 +221,32 @@ describe('generateRoleBindingPermissions', () => {
222221

223222
describe('listRoleBindings', () => {
224223
it('should list role bindings without namespace and label selector', async () => {
225-
k8sListResourceMock.mockResolvedValue(mockK8sResourceList([roleBindingMock]));
224+
k8sListResourceItemsMock.mockResolvedValue([roleBindingMock]);
226225
const result = await listRoleBindings();
227-
expect(k8sListResourceMock).toHaveBeenCalledWith({
226+
expect(k8sListResourceItemsMock).toHaveBeenCalledWith({
228227
model: RoleBindingModel,
229228
queryOptions: {},
230229
});
231-
expect(k8sListResourceMock).toHaveBeenCalledTimes(1);
230+
expect(k8sListResourceItemsMock).toHaveBeenCalledTimes(1);
232231
expect(result).toStrictEqual([roleBindingMock]);
233232
});
234233

235234
it('should list role bindings with namespace and label selector', async () => {
236-
k8sListResourceMock.mockResolvedValue(mockK8sResourceList([roleBindingMock]));
235+
k8sListResourceItemsMock.mockResolvedValue([roleBindingMock]);
237236
const result = await listRoleBindings(namespace, 'labelSelector');
238-
expect(k8sListResourceMock).toHaveBeenCalledWith({
237+
expect(k8sListResourceItemsMock).toHaveBeenCalledWith({
239238
model: RoleBindingModel,
240239
queryOptions: { ns: namespace, queryParams: { labelSelector: 'labelSelector' } },
241240
});
242-
expect(k8sListResourceMock).toHaveBeenCalledTimes(1);
241+
expect(k8sListResourceItemsMock).toHaveBeenCalledTimes(1);
243242
expect(result).toStrictEqual([roleBindingMock]);
244243
});
245244

246245
it('should handle errors and rethrow', async () => {
247-
k8sListResourceMock.mockRejectedValue(new Error('error1'));
246+
k8sListResourceItemsMock.mockRejectedValue(new Error('error1'));
248247
await expect(listRoleBindings()).rejects.toThrow('error1');
249-
expect(k8sListResourceMock).toHaveBeenCalledTimes(1);
250-
expect(k8sListResourceMock).toHaveBeenCalledWith({
248+
expect(k8sListResourceItemsMock).toHaveBeenCalledTimes(1);
249+
expect(k8sListResourceItemsMock).toHaveBeenCalledWith({
251250
model: RoleBindingModel,
252251
queryOptions: {},
253252
});

0 commit comments

Comments
 (0)