diff --git a/frontend/src/__mocks__/mockDSCStatus.ts b/frontend/src/__mocks__/mockDSCStatus.ts new file mode 100644 index 0000000000..2041453aef --- /dev/null +++ b/frontend/src/__mocks__/mockDSCStatus.ts @@ -0,0 +1,121 @@ +import { DataScienceClusterKindStatus, K8sCondition } from '~/k8sTypes'; + +export type MockDataScienceClusterKindStatus = { + conditions?: K8sCondition[]; + phase?: string; + codeFlareEnabled?: boolean; + dataSciencePipelineOperatorEnabled?: boolean; + kserveEnabled?: boolean; + modelMeshEnabled?: boolean; + odhDashboardEnabled?: boolean; + rayEnabled?: boolean; + workbenchesEnabled?: boolean; +}; + +export const mockDataScienceStatus = ({ + conditions = [], + phase = 'Ready', + codeFlareEnabled = true, + dataSciencePipelineOperatorEnabled = true, + kserveEnabled = true, + modelMeshEnabled = true, + odhDashboardEnabled = true, + rayEnabled = true, + workbenchesEnabled = true, +}: MockDataScienceClusterKindStatus): DataScienceClusterKindStatus => ({ + conditions: [ + ...[ + { + lastHeartbeatTime: '2023-10-20T11:44:48Z', + lastTransitionTime: '2023-10-15T19:04:21Z', + message: 'DataScienceCluster resource reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'ReconcileComplete', + }, + { + lastHeartbeatTime: '2023-10-20T11:44:48Z', + lastTransitionTime: '2023-10-15T19:04:21Z', + message: 'DataScienceCluster resource reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'Available', + }, + { + lastHeartbeatTime: '2023-10-20T11:44:48Z', + lastTransitionTime: '2023-10-15T19:04:21Z', + message: 'DataScienceCluster resource reconciled successfully', + reason: 'ReconcileCompleted', + status: 'False', + type: 'Progressing', + }, + { + lastHeartbeatTime: '2023-10-20T11:44:48Z', + lastTransitionTime: '2023-10-15T19:04:10Z', + message: 'DataScienceCluster resource reconciled successfully', + reason: 'ReconcileCompleted', + status: 'False', + type: 'Degraded', + }, + { + lastHeartbeatTime: '2023-10-20T11:44:48Z', + lastTransitionTime: '2023-10-15T19:04:21Z', + message: 'DataScienceCluster resource reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'Upgradeable', + }, + { + lastHeartbeatTime: '2023-10-20T11:44:59Z', + lastTransitionTime: '2023-10-20T11:44:59Z', + message: 'Component reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'odh-dashboardReady', + }, + { + lastHeartbeatTime: '2023-10-20T11:44:59Z', + lastTransitionTime: '2023-10-20T11:44:59Z', + message: 'Component reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'data-science-pipelines-operatorReady', + }, + { + lastHeartbeatTime: '2023-10-20T11:45:01Z', + lastTransitionTime: '2023-10-20T11:45:01Z', + message: 'Component reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'workbenchesReady', + }, + { + lastHeartbeatTime: '2023-10-20T11:45:04Z', + lastTransitionTime: '2023-10-20T11:45:04Z', + message: 'Component reconciled successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'kserveReady', + }, + { + lastHeartbeatTime: '2023-10-20T11:45:06Z', + lastTransitionTime: '2023-10-20T11:45:06Z', + message: 'Component is disabled', + reason: 'ReconcileInit', + status: 'Unknown', + type: 'rayReady', + }, + ], + ...conditions, + ], + installedComponents: { + codeflare: codeFlareEnabled, + 'data-science-pipelines-operator': dataSciencePipelineOperatorEnabled, + kserve: kserveEnabled, + 'model-mesh': modelMeshEnabled, + 'odh-dashboard': odhDashboardEnabled, + ray: rayEnabled, + workbenches: workbenchesEnabled, + }, + phase, +}); diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index f717de949c..d29e3d849d 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -90,6 +90,7 @@ export type K8sCondition = { reason?: string; message?: string; lastTransitionTime?: string; + lastHeartbeatTime?: string; }; export type ServingRuntimeAnnotations = Partial<{ diff --git a/frontend/src/pages/modelServing/__tests__/utils.spec.ts b/frontend/src/pages/modelServing/__tests__/utils.spec.ts index c6ac177ce3..0d58c92ae9 100644 --- a/frontend/src/pages/modelServing/__tests__/utils.spec.ts +++ b/frontend/src/pages/modelServing/__tests__/utils.spec.ts @@ -1,4 +1,9 @@ -import { resourcesArePositive } from '~/pages/modelServing/utils'; +import { mockDataScienceStatus } from '~/__mocks__/mockDSCStatus'; +import { + checkKserveFailureStatus, + checkPlatformAvailability, + resourcesArePositive, +} from '~/pages/modelServing/utils'; import { ContainerResources } from '~/types'; describe('resourcesArePositive', () => { @@ -50,3 +55,75 @@ describe('resourcesArePositive', () => { expect(resourcesArePositive(resources)).toBe(true); }); }); + +describe('servingPlatformsInstallaed', () => { + it('should return true for kserve but false for model-mesh', () => { + const mockedDataScienceStatusKserve = mockDataScienceStatus({ + kserveEnabled: true, + modelMeshEnabled: false, + }); + + expect(checkPlatformAvailability(mockedDataScienceStatusKserve)).toEqual({ + kServeAvailable: true, + modelMeshAvailable: false, + }); + }); + + it('should return false for kserve but true for model-mesh', () => { + const mockedDataScienceStatusKserve = mockDataScienceStatus({ + kserveEnabled: false, + modelMeshEnabled: true, + }); + + expect(checkPlatformAvailability(mockedDataScienceStatusKserve)).toEqual({ + kServeAvailable: false, + modelMeshAvailable: true, + }); + }); + + it('should return false for both kserve and model-mesh', () => { + const mockedDataScienceStatusKserve = mockDataScienceStatus({ + kserveEnabled: false, + modelMeshEnabled: false, + }); + + expect(checkPlatformAvailability(mockedDataScienceStatusKserve)).toEqual({ + kServeAvailable: false, + modelMeshAvailable: false, + }); + }); + + it('should not find any status issue for kserve', () => { + const mockedDataScienceStatusKserve = mockDataScienceStatus({}); + + expect(checkKserveFailureStatus(mockedDataScienceStatusKserve)).toEqual(''); + }); + + it('should find an issue with kserve', () => { + const errorMessage = + 'Component reconciliation failed: operator servicemeshoperator not found. Please install the operator before enabling kserve component'; + const mockedDataScienceStatusKserve = mockDataScienceStatus({ + conditions: [ + { + lastHeartbeatTime: '2023-10-20T11:31:24Z', + lastTransitionTime: '2023-10-15T19:04:21Z', + message: + 'DataScienceCluster resource reconciled with component errors: 1 error occurred:\n\t* operator servicemeshoperator not found. Please install the operator before enabling kserve component', + reason: 'ReconcileCompletedWithComponentErrors', + status: 'True', + type: 'ReconcileComplete', + }, + { + lastHeartbeatTime: '2023-10-20T11:31:19Z', + lastTransitionTime: '2023-10-20T11:31:19Z', + message: errorMessage, + reason: 'ReconcileFailed', + status: 'False', + type: 'kserveReady', + }, + ], + }); + + expect(checkKserveFailureStatus(mockedDataScienceStatusKserve)).toEqual(errorMessage); + }); +}); diff --git a/frontend/src/pages/modelServing/utils.ts b/frontend/src/pages/modelServing/utils.ts index 4da138966a..46badeecd2 100644 --- a/frontend/src/pages/modelServing/utils.ts +++ b/frontend/src/pages/modelServing/utils.ts @@ -22,6 +22,7 @@ import { K8sAPIOptions, RoleBindingKind, ServingRuntimeKind, + DataScienceClusterKindStatus, } from '~/k8sTypes'; import { ContainerResources } from '~/types'; import { getDisplayNameFromK8sResource, translateDisplayNameForK8s } from '~/pages/projects/utils'; @@ -211,3 +212,15 @@ export const isModelServerEditInfoChanged = ( createData.tokens.map((token) => token.name).sort(), )) : true; + +export const checkPlatformAvailability = ( + status: DataScienceClusterKindStatus, +): { kServeAvailable: boolean; modelMeshAvailable: boolean } => ({ + kServeAvailable: status.installedComponents.kserve, + modelMeshAvailable: status.installedComponents['model-mesh'], +}); + +export const checkKserveFailureStatus = (status: DataScienceClusterKindStatus): string => + status.conditions.find( + (condition) => condition.type === 'kserveReady' && condition.status === 'False', + )?.message || ''; diff --git a/frontend/src/services/getClusterStatusService.ts b/frontend/src/services/getClusterStatusService.ts new file mode 100644 index 0000000000..48380a1a98 --- /dev/null +++ b/frontend/src/services/getClusterStatusService.ts @@ -0,0 +1,12 @@ +import axios from 'axios'; +import { DataScienceClusterKindStatus } from '~/k8sTypes'; + +export const fetchClusterStatus = (): Promise => { + const url = '/api/dsc/status'; + return axios + .get(url) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +};