diff --git a/backend/src/routes/api/dsci/index.ts b/backend/src/routes/api/dsci/index.ts new file mode 100644 index 0000000000..74bca0de5f --- /dev/null +++ b/backend/src/routes/api/dsci/index.ts @@ -0,0 +1,12 @@ +import { getClusterInitialization } from '../../../utils/dsci'; +import { KubeFastifyInstance } from '../../../types'; +import { secureRoute } from '../../../utils/route-security'; + +module.exports = async (fastify: KubeFastifyInstance) => { + fastify.get( + '/status', + secureRoute(fastify)(async () => { + return getClusterInitialization(fastify); + }), + ); +}; diff --git a/backend/src/types.ts b/backend/src/types.ts index 5403eb25f5..7280c53a79 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -31,6 +31,7 @@ export type DashboardConfig = K8sResourceCommon & { disableBiasMetrics: boolean; disablePerformanceMetrics: boolean; disableKServe: boolean; + disableKServeAuth: boolean; disableModelMesh: boolean; disableAcceleratorProfiles: boolean; disablePipelineExperiments: boolean; @@ -945,7 +946,7 @@ type ComponentNames = | 'workbenches'; export type DataScienceClusterKindStatus = { - conditions: []; + conditions: K8sCondition[]; installedComponents: { [key in ComponentNames]?: boolean }; phase?: string; }; @@ -960,6 +961,21 @@ export type DataScienceClusterList = { items: DataScienceClusterKind[]; }; +export type DataScienceClusterInitializationKindStatus = { + conditions: K8sCondition[]; + phase?: string; +}; + +export type DataScienceClusterInitializationKind = K8sResourceCommon & { + spec: unknown; // we should never need to look into this + status: DataScienceClusterInitializationKindStatus; +}; + +export type DataScienceClusterInitializationList = { + kind: 'DataScienceClusterInitializationList'; + items: DataScienceClusterInitializationKind[]; +}; + export type SubscriptionStatusData = { installedCSV?: string; installPlanRefNamespace?: string; @@ -997,4 +1013,4 @@ export type DSPipelineKind = K8sResourceCommon & { status?: { conditions?: K8sCondition[]; }; -}; \ No newline at end of file +}; diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index b2b5e1f6ab..1012763f10 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -56,6 +56,7 @@ export const blankDashboardCR: DashboardConfig = { disablePerformanceMetrics: false, disablePipelines: false, disableKServe: false, + disableKServeAuth: false, disableModelMesh: false, disableAcceleratorProfiles: false, disablePipelineExperiments: true, diff --git a/backend/src/utils/dsci.ts b/backend/src/utils/dsci.ts new file mode 100644 index 0000000000..4ad8427a92 --- /dev/null +++ b/backend/src/utils/dsci.ts @@ -0,0 +1,25 @@ +import { + DataScienceClusterInitializationKind, + DataScienceClusterInitializationKindStatus, + DataScienceClusterInitializationList, + KubeFastifyInstance, +} from '../types'; +import { createCustomError } from './requestUtils'; + +export const getClusterInitialization = async ( + fastify: KubeFastifyInstance, +): Promise => { + const result: DataScienceClusterInitializationKind | null = await fastify.kube.customObjectsApi + .listClusterCustomObject('dscinitialization.opendatahub.io', 'v1', 'dscinitializations') + .then((res) => (res.body as DataScienceClusterInitializationList).items[0]) + .catch((e) => { + fastify.log.error(`Failure to fetch dsci: ${e.response.body}`); + return null; + }); + + if (!result) { + throw createCustomError('DSCI Unavailable', 'Unable to get status', 404); + } + + return result.status; +}; diff --git a/docs/dashboard-config.md b/docs/dashboard-config.md index c1b50447d4..3031c0e84f 100644 --- a/docs/dashboard-config.md +++ b/docs/dashboard-config.md @@ -25,6 +25,7 @@ The following are a list of features that are supported, along with there defaul | disableProjectSharing | false | Disables Project Sharing from Data Science Projects. | | disableCustomServingRuntimes | false | Disables Custom Serving Runtimes from the Admin Panel. | | disableKServe | false | Disables the ability to select KServe as a Serving Platform. | +| disableKServeAuth | false | Disables the ability to use auth in KServe. | | disableModelMesh | false | Disables the ability to select ModelMesh as a Serving Platform. | | disableAcceleratorProfiles | false | Disables Accelerator profiles from the Admin Panel. | | modelMetricsNamespace | false | Enables the namespace in which the Model Serving Metrics' Prometheus Operator is installed. | diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index b4f2a53fdd..e428f91c45 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -14,6 +14,7 @@ type MockDashboardConfigType = { disableModelServing?: boolean; disableCustomServingRuntimes?: boolean; disableKServe?: boolean; + disableKServeAuth?: boolean; disableModelMesh?: boolean; disableAcceleratorProfiles?: boolean; disablePerformanceMetrics?: boolean; @@ -37,6 +38,7 @@ export const mockDashboardConfig = ({ disableCustomServingRuntimes = false, disablePipelines = false, disableKServe = false, + disableKServeAuth = false, disableModelMesh = false, disableAcceleratorProfiles = false, disablePerformanceMetrics = false, @@ -73,6 +75,7 @@ export const mockDashboardConfig = ({ disableBiasMetrics, disablePerformanceMetrics, disableKServe, + disableKServeAuth, disableModelMesh, disableAcceleratorProfiles, disablePipelineExperiments, diff --git a/frontend/src/__mocks__/mockDsciStatus.ts b/frontend/src/__mocks__/mockDsciStatus.ts new file mode 100644 index 0000000000..370b590490 --- /dev/null +++ b/frontend/src/__mocks__/mockDsciStatus.ts @@ -0,0 +1,37 @@ +import { StackCapability } from '~/concepts/areas/types'; +import { DataScienceClusterInitializationKindStatus, K8sCondition } from '~/k8sTypes'; + +export type MockDsciStatus = { + conditions?: K8sCondition[]; + requiredCapabilities?: StackCapability[]; + phase?: string; +}; + +export const mockDsciStatus = ({ + conditions = [], + requiredCapabilities = [], + phase = 'Ready', +}: MockDsciStatus): DataScienceClusterInitializationKindStatus => ({ + conditions: [ + ...[ + { + lastHeartbeatTime: '2023-10-20T11:45:04Z', + lastTransitionTime: '2023-10-20T11:45:04Z', + message: 'Reconcile completed successfully', + reason: 'ReconcileCompleted', + status: 'True', + type: 'ReconcileComplete', + }, + ...requiredCapabilities.map((capability) => ({ + lastHeartbeatTime: '2023-10-20T11:45:04Z', + lastTransitionTime: '2023-10-20T11:45:04Z', + message: `Capability ${capability} installed`, + reason: 'ReconcileCompleted', + status: 'True', + type: capability, + })), + ], + ...conditions, + ], + phase, +}); diff --git a/frontend/src/__mocks__/mockInferenceServiceModalData.ts b/frontend/src/__mocks__/mockInferenceServiceModalData.ts index 0c7373aef2..bc1e6f17be 100644 --- a/frontend/src/__mocks__/mockInferenceServiceModalData.ts +++ b/frontend/src/__mocks__/mockInferenceServiceModalData.ts @@ -21,6 +21,9 @@ export const mockInferenceServiceModalData = ({ }, minReplicas = 1, maxReplicas = 1, + externalRoute = false, + tokenAuth = false, + tokens = [], }: MockResourceConfigType): CreatingInferenceServiceObject => ({ name, project, @@ -29,4 +32,7 @@ export const mockInferenceServiceModalData = ({ format, minReplicas, maxReplicas, + externalRoute, + tokenAuth, + tokens, }); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ServingRuntimeList.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ServingRuntimeList.cy.ts index f4e8d80605..fd67582501 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ServingRuntimeList.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ServingRuntimeList.cy.ts @@ -36,9 +36,12 @@ import { be } from '~/__tests__/cypress/cypress/utils/should'; import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; import { ServingRuntimePlatform } from '~/types'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; +import { StackCapability } from '~/concepts/areas/types'; +import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; type HandlersProps = { disableKServeConfig?: boolean; + disableKServeAuthConfig?: boolean; disableModelMeshConfig?: boolean; disableAccelerator?: boolean; projectEnableModelMesh?: boolean; @@ -50,10 +53,12 @@ type HandlersProps = { rejectInferenceService?: boolean; rejectServingRuntime?: boolean; rejectDataConnection?: boolean; + requiredCapabilities?: StackCapability[]; }; const initIntercepts = ({ disableKServeConfig, + disableKServeAuthConfig, disableModelMeshConfig, disableAccelerator, projectEnableModelMesh, @@ -86,6 +91,7 @@ const initIntercepts = ({ rejectInferenceService = false, rejectServingRuntime = false, rejectDataConnection = false, + requiredCapabilities = [], }: HandlersProps) => { cy.intercept( '/api/dsc/status', @@ -93,12 +99,19 @@ const initIntercepts = ({ installedComponents: { kserve: true, 'model-mesh': true }, }), ); + cy.intercept( + '/api/dsci/status', + mockDsciStatus({ + requiredCapabilities, + }), + ); cy.intercept('/api/status', mockStatus()); cy.intercept( '/api/config', mockDashboardConfig({ disableKServe: disableKServeConfig, disableModelMesh: disableModelMeshConfig, + disableKServeAuth: disableKServeAuthConfig, }), ); cy.intercept( @@ -545,6 +558,7 @@ describe('Serving Runtime List', () => { disableModelMeshConfig: false, disableKServeConfig: false, servingRuntimes: [], + requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ], }); projectDetails.visitSection('test-project', 'model-server'); @@ -561,6 +575,10 @@ describe('Serving Runtime List', () => { kserveModal.findServingRuntimeTemplateDropdown().findDropdownItem('Caikit').click(); kserveModal.findModelFrameworkSelect().findSelectOption('onnx - 1').click(); kserveModal.findSubmitButton().should('be.disabled'); + // check external route, token should be checked and no alert + kserveModal.findAuthenticationCheckbox().check(); + kserveModal.findExternalRouteError().should('not.exist'); + kserveModal.findServiceAccountNameInput().should('have.value', 'default-name'); kserveModal.findExistingConnectionSelect().findSelectOption('Test Secret').click(); kserveModal.findLocationPathInput().type('test-model/'); kserveModal.findSubmitButton().should('be.enabled'); @@ -615,6 +633,31 @@ describe('Serving Runtime List', () => { }); }); + it('Kserve auth should be hidden when auth is disabled', () => { + initIntercepts({ + disableModelMeshConfig: false, + disableKServeConfig: false, + servingRuntimes: [], + }); + + projectDetails.visitSection('test-project', 'model-server'); + + modelServingSection.getServingPlatformCard('single-serving').findDeployModelButton().click(); + + kserveModal.shouldBeOpen(); + + // test that you can not submit on empty + kserveModal.findSubmitButton().should('be.disabled'); + + // test filling in minimum required fields + kserveModal.findModelNameInput().type('Test Name'); + kserveModal.findServingRuntimeTemplateDropdown().findDropdownItem('Caikit').click(); + kserveModal.findModelFrameworkSelect().findSelectOption('onnx - 1').click(); + kserveModal.findSubmitButton().should('be.disabled'); + // check external route, token should be checked and no alert + kserveModal.findAuthenticationCheckbox().should('not.exist'); + }); + it('Do not deploy KServe model when user cannot edit namespace', () => { initIntercepts({ disableModelMeshConfig: false, @@ -1495,6 +1538,37 @@ describe('Serving Runtime List', () => { }); }); + describe('Check token section in serving runtime details', () => { + it('Check token section is enabled if capability is enabled', () => { + initIntercepts({ + projectEnableModelMesh: false, + disableKServeConfig: false, + disableModelMeshConfig: true, + disableAccelerator: true, + requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ], + }); + projectDetails.visitSection('test-project', 'model-server'); + const kserveRow = modelServingSection.getKServeRow('Llama Caikit'); + kserveRow.findExpansion().should(be.collapsed); + kserveRow.findToggleButton().click(); + kserveRow.findDescriptionListItem('Token authorization').should('exist'); + }); + + it('Check token section is disabled if capability is disabled', () => { + initIntercepts({ + projectEnableModelMesh: false, + disableKServeConfig: false, + disableModelMeshConfig: true, + disableAccelerator: true, + }); + projectDetails.visitSection('test-project', 'model-server'); + const kserveRow = modelServingSection.getKServeRow('Llama Caikit'); + kserveRow.findExpansion().should(be.collapsed); + kserveRow.findToggleButton().click(); + kserveRow.findDescriptionListItem('Token authorization').should('not.exist'); + }); + }); + describe('Dry run check', () => { it('Check when inference service dryRun fails', () => { initIntercepts({ diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts index 9094ad8ed4..16381b14a6 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts @@ -85,6 +85,14 @@ class InferenceServiceModal extends Modal { return this.find().findByTestId('existing-data-connection-radio'); } + findExternalRouteError() { + return this.find().findByTestId('external-route-no-token-alert'); + } + + findServiceAccountNameInput() { + return this.find().findByTestId('service-account-form-name'); + } + findNewDataConnectionOption() { return this.find().findByTestId('new-data-connection-radio'); } diff --git a/frontend/src/api/k8s/__tests__/inferenceServices.spec.ts b/frontend/src/api/k8s/__tests__/inferenceServices.spec.ts index 83529202c4..ac45f952d9 100644 --- a/frontend/src/api/k8s/__tests__/inferenceServices.spec.ts +++ b/frontend/src/api/k8s/__tests__/inferenceServices.spec.ts @@ -52,6 +52,30 @@ describe('assembleInferenceService', () => { expect(inferenceService.metadata.annotations?.['serving.kserve.io/deploymentMode']).toBe( undefined, ); + expect(inferenceService.metadata.annotations?.['security.opendatahub.io/enable-auth']).toBe( + undefined, + ); + expect( + inferenceService.metadata.annotations?.['serving.knative.openshift.io/enablePassthrough'], + ).toBe('true'); + expect(inferenceService.metadata.annotations?.['sidecar.istio.io/inject']).toBe('true'); + expect(inferenceService.metadata.annotations?.['sidecar.istio.io/rewriteAppHTTPProbers']).toBe( + 'true', + ); + }); + + it('should have the right annotations when creating for Kserve with auth', async () => { + const inferenceService = assembleInferenceService( + mockInferenceServiceModalData({ tokenAuth: true }), + ); + + expect(inferenceService.metadata.annotations).toBeDefined(); + expect(inferenceService.metadata.annotations?.['serving.kserve.io/deploymentMode']).toBe( + undefined, + ); + expect(inferenceService.metadata.annotations?.['security.opendatahub.io/enable-auth']).toBe( + 'true', + ); expect( inferenceService.metadata.annotations?.['serving.knative.openshift.io/enablePassthrough'], ).toBe('true'); diff --git a/frontend/src/api/k8s/inferenceServices.ts b/frontend/src/api/k8s/inferenceServices.ts index d180897cca..022b030953 100644 --- a/frontend/src/api/k8s/inferenceServices.ts +++ b/frontend/src/api/k8s/inferenceServices.ts @@ -24,7 +24,8 @@ export const assembleInferenceService = ( inferenceService?: InferenceServiceKind, acceleratorState?: AcceleratorProfileState, ): InferenceServiceKind => { - const { storage, format, servingRuntimeName, project, maxReplicas, minReplicas } = data; + const { storage, format, servingRuntimeName, project, maxReplicas, minReplicas, tokenAuth } = + data; const name = editName || translateDisplayNameForK8s(data.name); const { path, dataConnection } = storage; const dataConnectionKey = secretKey || dataConnection; @@ -44,6 +45,7 @@ export const assembleInferenceService = ( 'serving.knative.openshift.io/enablePassthrough': 'true', 'sidecar.istio.io/inject': 'true', 'sidecar.istio.io/rewriteAppHTTPProbers': 'true', + ...(tokenAuth && { 'security.opendatahub.io/enable-auth': 'true' }), }), }, }, @@ -82,6 +84,7 @@ export const assembleInferenceService = ( 'serving.knative.openshift.io/enablePassthrough': 'true', 'sidecar.istio.io/inject': 'true', 'sidecar.istio.io/rewriteAppHTTPProbers': 'true', + ...(tokenAuth && { 'security.opendatahub.io/enable-auth': 'true' }), }), }, }, @@ -104,6 +107,10 @@ export const assembleInferenceService = ( }, }; + if (!tokenAuth && updateInferenceService.metadata.annotations) { + delete updateInferenceService.metadata.annotations['serving.knative.openshift.io/token-auth']; + } + if (!isModelMesh && tolerations.length !== 0) { updateInferenceService.spec.predictor.tolerations = tolerations; } diff --git a/frontend/src/concepts/areas/AreaContext.tsx b/frontend/src/concepts/areas/AreaContext.tsx index 07a50a001e..fe0385b202 100644 --- a/frontend/src/concepts/areas/AreaContext.tsx +++ b/frontend/src/concepts/areas/AreaContext.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import { Alert, Bullseye, Spinner } from '@patternfly/react-core'; import useFetchDscStatus from '~/concepts/areas/useFetchDscStatus'; -import { DataScienceClusterKindStatus } from '~/k8sTypes'; +import useFetchDsciStatus from '~/concepts/areas/useFetchDsciStatus'; +import { + DataScienceClusterInitializationKindStatus, + DataScienceClusterKindStatus, +} from '~/k8sTypes'; type AreaContextState = { /** @@ -10,10 +14,12 @@ type AreaContextState = { * TODO: Remove when we no longer want to support v1 */ dscStatus: DataScienceClusterKindStatus | null; + dsciStatus: DataScienceClusterInitializationKindStatus | null; }; export const AreaContext = React.createContext({ dscStatus: null, + dsciStatus: null, }); type AreaContextProps = { @@ -21,9 +27,13 @@ type AreaContextProps = { }; const AreaContextProvider: React.FC = ({ children }) => { - const [dscStatus, loaded, error] = useFetchDscStatus(); + const [dscStatus, loadedDsc, errorDsc] = useFetchDscStatus(); + const [dsciStatus, loadedDsci, errorDsci] = useFetchDsciStatus(); - const contextValue = React.useMemo(() => ({ dscStatus }), [dscStatus]); + const error = errorDsc || errorDsci; + const loaded = loadedDsc && loadedDsci; + + const contextValue = React.useMemo(() => ({ dscStatus, dsciStatus }), [dscStatus, dsciStatus]); if (error) { return ( diff --git a/frontend/src/concepts/areas/__tests__/useFetchDsciStatus.spec.ts b/frontend/src/concepts/areas/__tests__/useFetchDsciStatus.spec.ts new file mode 100644 index 0000000000..c4bd271592 --- /dev/null +++ b/frontend/src/concepts/areas/__tests__/useFetchDsciStatus.spec.ts @@ -0,0 +1,102 @@ +import { act } from '@testing-library/react'; +import axios from 'axios'; +import { standardUseFetchState, testHook } from '~/__tests__/unit/testUtils/hooks'; +import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; +import useFetchDsciStatus from '~/concepts/areas/useFetchDsciStatus'; + +jest.mock('axios', () => ({ + get: jest.fn(), +})); + +const mockAxios = jest.mocked(axios.get); + +describe('useFetchDscStatus', () => { + it('should return successful dsci status', async () => { + const mockedResponse = { data: mockDsciStatus({}) }; + mockAxios.mockResolvedValue(mockedResponse); + + const renderResult = testHook(useFetchDsciStatus)(); + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith(`/api/dsci/status`); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(renderResult).hookToStrictEqual(standardUseFetchState(mockedResponse.data, true)); + expect(renderResult).hookToHaveUpdateCount(2); + expect(renderResult).hookToBeStable([false, false, true, true]); + + //refresh + mockAxios.mockResolvedValue({ data: mockedResponse.data }); + await act(() => renderResult.result.current[3]()); + expect(mockAxios).toHaveBeenCalledTimes(2); + expect(axios.get).toHaveBeenCalledWith(`/api/dsci/status`); + expect(renderResult).hookToHaveUpdateCount(3); + expect(renderResult).hookToBeStable([false, true, true, true]); + }); + + it('should handle 404 as an error', async () => { + const error = { + response: { + status: 404, + }, + }; + + mockAxios.mockRejectedValue(error); + const renderResult = testHook(useFetchDsciStatus)(); + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith(`/api/dsci/status`); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null, true)); + expect(renderResult).hookToHaveUpdateCount(2); + expect(renderResult).hookToBeStable([false, false, true, true]); + + // refresh + await act(() => renderResult.result.current[3]()); + expect(mockAxios).toHaveBeenCalledTimes(2); + expect(axios.get).toHaveBeenCalledWith(`/api/dsci/status`); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null, true)); + expect(renderResult).hookToHaveUpdateCount(3); + expect(renderResult).hookToBeStable([true, true, false, true]); + }); + + it('should handle other errors and rethrow', async () => { + const error = (message: string) => ({ + response: { + data: { + message, + }, + }, + }); + + mockAxios.mockRejectedValue(error('error1')); + const renderResult = testHook(useFetchDsciStatus)(); + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith(`/api/dsci/status`); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null, false, new Error('error1'))); + expect(renderResult).hookToHaveUpdateCount(2); + expect(renderResult).hookToBeStable([true, true, false, true]); + + // refresh + mockAxios.mockRejectedValue(error('error2')); + await act(() => renderResult.result.current[3]()); + expect(mockAxios).toHaveBeenCalledTimes(2); + expect(axios.get).toHaveBeenCalledWith(`/api/dsci/status`); + expect(renderResult).hookToStrictEqual(standardUseFetchState(null, false, new Error('error2'))); + expect(renderResult).hookToHaveUpdateCount(3); + expect(renderResult).hookToBeStable([true, true, false, true]); + }); +}); diff --git a/frontend/src/concepts/areas/__tests__/utils.spec.ts b/frontend/src/concepts/areas/__tests__/utils.spec.ts index b8d7633329..4e1b639198 100644 --- a/frontend/src/concepts/areas/__tests__/utils.spec.ts +++ b/frontend/src/concepts/areas/__tests__/utils.spec.ts @@ -3,6 +3,7 @@ import { mockDscStatus } from '~/__mocks__/mockDscStatus'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { StackComponent } from '~/concepts/areas/types'; import { SupportedAreasStateMap } from '~/concepts/areas/const'; +import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; describe('isAreaAvailable', () => { describe('v1 Operator (deprecated)', () => { @@ -11,6 +12,7 @@ describe('isAreaAvailable', () => { SupportedArea.DS_PIPELINES, mockDashboardConfig({ disablePipelines: false }).spec, null, + null, ); expect(isAvailable.status).toBe(true); @@ -24,6 +26,7 @@ describe('isAreaAvailable', () => { SupportedArea.DS_PIPELINES, mockDashboardConfig({ disablePipelines: true }).spec, null, + null, ); expect(isAvailable.status).not.toBe(true); @@ -37,6 +40,7 @@ describe('isAreaAvailable', () => { SupportedArea.WORKBENCHES, mockDashboardConfig({}).spec, null, + null, ); expect(isAvailable.status).toBe(true); @@ -53,6 +57,7 @@ describe('isAreaAvailable', () => { SupportedArea.DS_PIPELINES, mockDashboardConfig({ disablePipelines: false }).spec, mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: true } }), + mockDsciStatus({}), ); expect(isAvailable.status).toBe(true); @@ -66,6 +71,7 @@ describe('isAreaAvailable', () => { SupportedArea.DS_PIPELINES, mockDashboardConfig({ disablePipelines: false }).spec, mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: false } }), + mockDsciStatus({}), ); expect(isAvailable.status).not.toBe(true); @@ -79,6 +85,7 @@ describe('isAreaAvailable', () => { SupportedArea.DS_PIPELINES, mockDashboardConfig({ disablePipelines: true }).spec, mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: true } }), + mockDsciStatus({}), ); expect(isAvailable.status).not.toBe(true); @@ -92,6 +99,7 @@ describe('isAreaAvailable', () => { SupportedArea.DS_PIPELINES, mockDashboardConfig({ disablePipelines: true }).spec, mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: false } }), + mockDsciStatus({}), ); expect(isAvailable.status).not.toBe(true); @@ -105,6 +113,7 @@ describe('isAreaAvailable', () => { SupportedArea.WORKBENCHES, mockDashboardConfig({}).spec, mockDscStatus({ installedComponents: { [StackComponent.WORKBENCHES]: true } }), + mockDsciStatus({}), ); expect(isAvailable.status).toBe(true); @@ -118,6 +127,7 @@ describe('isAreaAvailable', () => { SupportedArea.WORKBENCHES, mockDashboardConfig({}).spec, mockDscStatus({ installedComponents: { [StackComponent.WORKBENCHES]: false } }), + mockDsciStatus({}), ); expect(isAvailable.status).not.toBe(true); @@ -143,6 +153,7 @@ describe('isAreaAvailable', () => { SupportedArea.CUSTOM_RUNTIMES, mockDashboardConfig({ disableModelServing: false }).spec, mockDscStatus({}), + mockDsciStatus({}), ); expect(isAvailableReliantCustomRuntimes.status).toBe(true); @@ -166,6 +177,7 @@ describe('isAreaAvailable', () => { SupportedArea.CUSTOM_RUNTIMES, mockDashboardConfig({ disableModelServing: true }).spec, mockDscStatus({}), + mockDsciStatus({}), ); expect(isAvailable.status).not.toBe(true); diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts index 4dbcfaf6a4..1c4fe82376 100644 --- a/frontend/src/concepts/areas/const.ts +++ b/frontend/src/concepts/areas/const.ts @@ -1,4 +1,4 @@ -import { StackComponent, SupportedArea, SupportedAreasState } from './types'; +import { StackCapability, StackComponent, SupportedArea, SupportedAreasState } from './types'; export const SupportedAreasStateMap: SupportedAreasState = { [SupportedArea.BYON]: { @@ -29,6 +29,11 @@ export const SupportedAreasStateMap: SupportedAreasState = { featureFlags: ['disableKServe'], requiredComponents: [StackComponent.K_SERVE], }, + [SupportedArea.K_SERVE_AUTH]: { + featureFlags: ['disableKServeAuth'], + reliantAreas: [SupportedArea.K_SERVE], + requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ], + }, [SupportedArea.MODEL_MESH]: { featureFlags: ['disableModelMesh'], requiredComponents: [StackComponent.MODEL_MESH], diff --git a/frontend/src/concepts/areas/types.ts b/frontend/src/concepts/areas/types.ts index 300f6ba326..9cd04fef5a 100644 --- a/frontend/src/concepts/areas/types.ts +++ b/frontend/src/concepts/areas/types.ts @@ -11,6 +11,7 @@ export type IsAreaAvailableStatus = { featureFlags: { [key in FeatureFlag]?: 'on' | 'off' } | null; // simplified. `disableX` flags are weird to read reliantAreas: { [key in SupportedArea]?: boolean } | null; // only needs 1 to be true requiredComponents: { [key in StackComponent]?: boolean } | null; + requiredCapabilities: { [key in StackCapability]?: boolean } | null; }; /** All areas that we need to support in some fashion or another */ @@ -36,6 +37,7 @@ export enum SupportedArea { MODEL_SERVING = 'model-serving-shell', CUSTOM_RUNTIMES = 'custom-serving-runtimes', K_SERVE = 'kserve', + K_SERVE_AUTH = 'kserve-auth', MODEL_MESH = 'model-mesh', BIAS_METRICS = 'bias-metrics', PERFORMANCE_METRICS = 'performance-metrics', @@ -64,6 +66,12 @@ export enum StackComponent { MODEL_REGISTRY = 'model-registry-operator', } +/** Capabilities of the Operator. Part of the DSCI Status. */ +export enum StackCapability { + SERVICE_MESH = 'CapabilityServiceMesh', + SERVICE_MESH_AUTHZ = 'CapabilityServiceMeshAuthorization', +} + // TODO: Support extra operators, like the pipelines operator -- maybe as a "external dependency need?" type SupportedComponentFlagValue = { /** @@ -75,6 +83,12 @@ type SupportedComponentFlagValue = { * TODO: support AND -- maybe double array? */ reliantAreas?: SupportedArea[]; + /** + * Required capabilities supported by the Operator. The list is "AND"-ed together. + * If the Operator does not support the capability, the area is not available. + * The capabilities are retrieved from the DSCI status. + */ + requiredCapabilities?: StackCapability[]; } & EitherOrBoth< { /** diff --git a/frontend/src/concepts/areas/useFetchDsciStatus.ts b/frontend/src/concepts/areas/useFetchDsciStatus.ts new file mode 100644 index 0000000000..1da899f2bb --- /dev/null +++ b/frontend/src/concepts/areas/useFetchDsciStatus.ts @@ -0,0 +1,24 @@ +import axios from 'axios'; +import useFetchState, { FetchState } from '~/utilities/useFetchState'; +import { DataScienceClusterInitializationKindStatus } from '~/k8sTypes'; + +/** + * Should only return `null` when on v1 Operator. + */ +const fetchDsciStatus = (): Promise => { + const url = '/api/dsci/status'; + return axios + .get(url) + .then((response) => response.data) + .catch((e) => { + if (e.response.status === 404) { + return null; + } + throw new Error(e.response.data.message); + }); +}; + +const useFetchDsciStatus = (): FetchState => + useFetchState(fetchDsciStatus, null); + +export default useFetchDsciStatus; diff --git a/frontend/src/concepts/areas/useIsAreaAvailable.ts b/frontend/src/concepts/areas/useIsAreaAvailable.ts index 4697b1909a..71af15737a 100644 --- a/frontend/src/concepts/areas/useIsAreaAvailable.ts +++ b/frontend/src/concepts/areas/useIsAreaAvailable.ts @@ -7,14 +7,15 @@ import { isAreaAvailable } from './utils'; const useIsAreaAvailable = (area: SupportedArea): IsAreaAvailableStatus => { const { dashboardConfig } = useAppContext(); - const { dscStatus } = React.useContext(AreaContext); + const { dscStatus, dsciStatus } = React.useContext(AreaContext); const dashboardConfigSpecSafe = useDeepCompareMemoize(dashboardConfig.spec); const dscStatusSafe = useDeepCompareMemoize(dscStatus); + const dsciStatusSafe = useDeepCompareMemoize(dsciStatus); return React.useMemo( - () => isAreaAvailable(area, dashboardConfigSpecSafe, dscStatusSafe), - [area, dashboardConfigSpecSafe, dscStatusSafe], + () => isAreaAvailable(area, dashboardConfigSpecSafe, dscStatusSafe, dsciStatusSafe), + [area, dashboardConfigSpecSafe, dscStatusSafe, dsciStatusSafe], ); }; diff --git a/frontend/src/concepts/areas/utils.ts b/frontend/src/concepts/areas/utils.ts index a6d7dc3aba..525fd43a7c 100644 --- a/frontend/src/concepts/areas/utils.ts +++ b/frontend/src/concepts/areas/utils.ts @@ -1,4 +1,8 @@ -import { DashboardConfigKind, DataScienceClusterKindStatus } from '~/k8sTypes'; +import { + DashboardConfigKind, + DataScienceClusterInitializationKindStatus, + DataScienceClusterKindStatus, +} from '~/k8sTypes'; import { IsAreaAvailableStatus, FeatureFlag, SupportedArea } from './types'; import { SupportedAreasStateMap } from './const'; @@ -27,14 +31,17 @@ export const isAreaAvailable = ( area: SupportedArea, dashboardConfigSpec: DashboardConfigKind['spec'], dscStatus: DataScienceClusterKindStatus | null, + dsciStatus: DataScienceClusterInitializationKindStatus | null, ): IsAreaAvailableStatus => { - const { featureFlags, requiredComponents, reliantAreas } = SupportedAreasStateMap[area]; + const { featureFlags, requiredComponents, reliantAreas, requiredCapabilities } = + SupportedAreasStateMap[area]; const reliantAreasState = reliantAreas ? reliantAreas.reduce( (areaStates, currentArea) => ({ ...areaStates, - [currentArea]: isAreaAvailable(currentArea, dashboardConfigSpec, dscStatus).status, + [currentArea]: isAreaAvailable(currentArea, dashboardConfigSpec, dscStatus, dsciStatus) + .status, }), {}, ) @@ -65,10 +72,32 @@ export const isAreaAvailable = ( ? Object.values(requiredComponentsState).every((v) => v) : true; + const requiredCapabilitiesState = + requiredCapabilities && dsciStatus + ? requiredCapabilities.reduce( + (acc, capability) => ({ + ...acc, + [capability]: dsciStatus.conditions.some( + (c) => c.type === capability && c.status === 'True', + ), + }), + {}, + ) + : null; + + const hasMetRequiredCapabilities = requiredCapabilitiesState + ? Object.values(requiredCapabilitiesState).every((v) => v) + : true; + return { - status: hasMetReliantAreas && hasMetFeatureFlags && hasMetRequiredComponents, + status: + hasMetReliantAreas && + hasMetFeatureFlags && + hasMetRequiredComponents && + hasMetRequiredCapabilities, reliantAreas: reliantAreasState, featureFlags: featureFlagState, requiredComponents: requiredComponentsState, + requiredCapabilities: requiredCapabilitiesState, }; }; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index c548ffcb37..788bb79e6c 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -384,11 +384,16 @@ export type SupportedModelFormats = { autoSelect?: boolean; }; +export type InferenceServiceAnnotations = Partial<{ + 'security.opendatahub.io/enable-auth': string; +}>; + export type InferenceServiceKind = K8sResourceCommon & { metadata: { name: string; namespace: string; - annotations?: DisplayNameAnnotations & + annotations?: InferenceServiceAnnotations & + DisplayNameAnnotations & EitherOrNone< { 'serving.kserve.io/deploymentMode': 'ModelMesh'; @@ -1131,6 +1136,7 @@ export type DashboardCommonConfig = { disableBiasMetrics: boolean; disablePerformanceMetrics: boolean; disableKServe: boolean; + disableKServeAuth: boolean; disableModelMesh: boolean; disableAcceleratorProfiles: boolean; // TODO Temp feature flag - remove with https://issues.redhat.com/browse/RHOAIENG-3826 @@ -1201,6 +1207,11 @@ export type DataScienceClusterKindStatus = { phase?: string; }; +export type DataScienceClusterInitializationKindStatus = { + conditions: K8sCondition[]; + phase?: string; +}; + export type ModelRegistryKind = K8sResourceCommon & { metadata: { name: string; diff --git a/frontend/src/pages/modelServing/ModelServingContext.tsx b/frontend/src/pages/modelServing/ModelServingContext.tsx index 17fe0d304f..2a3dfd3a24 100644 --- a/frontend/src/pages/modelServing/ModelServingContext.tsx +++ b/frontend/src/pages/modelServing/ModelServingContext.tsx @@ -10,7 +10,13 @@ import { } from '@patternfly/react-core'; import { useNavigate } from 'react-router-dom'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; -import { ServingRuntimeKind, InferenceServiceKind, TemplateKind, ProjectKind } from '~/k8sTypes'; +import { + ServingRuntimeKind, + InferenceServiceKind, + TemplateKind, + ProjectKind, + SecretKind, +} from '~/k8sTypes'; import { DEFAULT_CONTEXT_DATA } from '~/utilities/const'; import { ContextResourceData } from '~/types'; import { useContextResourceData } from '~/utilities/useContextResourceData'; @@ -26,9 +32,12 @@ import useServingRuntimes from './useServingRuntimes'; import useTemplates from './customServingRuntimes/useTemplates'; import useTemplateOrder from './customServingRuntimes/useTemplateOrder'; import useTemplateDisablement from './customServingRuntimes/useTemplateDisablement'; +import { getTokenNames } from './utils'; +import useServingRuntimeSecrets from './screens/projects/useServingRuntimeSecrets'; type ModelServingContextType = { refreshAllData: () => void; + filterTokens: (servingRuntime?: string) => SecretKind[]; dataConnections: ContextResourceData; servingRuntimeTemplates: ContextResourceData; servingRuntimeTemplateOrder: ContextResourceData; @@ -37,6 +46,7 @@ type ModelServingContextType = { inferenceServices: ContextResourceData; project: ProjectKind | null; preferredProject: ProjectKind | null; + serverSecrets: ContextResourceData; projects: ProjectKind[] | null; }; @@ -48,12 +58,14 @@ type ModelServingContextProviderProps = { export const ModelServingContext = React.createContext({ refreshAllData: () => undefined, + filterTokens: () => [], dataConnections: DEFAULT_CONTEXT_DATA, servingRuntimeTemplates: DEFAULT_CONTEXT_DATA, servingRuntimeTemplateOrder: DEFAULT_CONTEXT_DATA, servingRuntimeTemplateDisablement: DEFAULT_CONTEXT_DATA, servingRuntimes: DEFAULT_CONTEXT_DATA, inferenceServices: DEFAULT_CONTEXT_DATA, + serverSecrets: DEFAULT_CONTEXT_DATA, project: null, preferredProject: null, projects: null, @@ -77,6 +89,7 @@ const ModelServingContextProvider = conditionalArea( useTemplateDisablement(dashboardNamespace), ); + const serverSecrets = useContextResourceData(useServingRuntimeSecrets(namespace)); const servingRuntimes = useContextResourceData(useServingRuntimes(namespace)); const inferenceServices = useContextResourceData( useInferenceServices(namespace), @@ -102,6 +115,24 @@ const ModelServingContextProvider = conditionalArea { + if (!namespace || !servingRuntimeName) { + return []; + } + const { serviceAccountName } = getTokenNames(servingRuntimeName, namespace); + + const secrets = serverSecrets.data.filter( + (secret) => + secret.metadata.annotations?.['kubernetes.io/service-account.name'] === + serviceAccountName, + ); + + return secrets; + }, + [namespace, serverSecrets], + ); + if ( notInstalledError || servingRuntimes.error || @@ -109,6 +140,7 @@ const ModelServingContextProvider = conditionalArea @@ -158,6 +191,8 @@ const ModelServingContextProvider = conditionalArea void; + filterTokens: (servingRuntime?: string | undefined) => SecretKind[]; }; const InferenceServiceListView: React.FC = ({ inferenceServices: unfilteredInferenceServices, servingRuntimes, + refresh, + filterTokens, }) => { - const { - inferenceServices: { refresh }, - } = React.useContext(ModelServingContext); const { projects } = React.useContext(ProjectsContext); const [searchType, setSearchType] = React.useState(SearchType.NAME); const [search, setSearch] = React.useState(''); @@ -52,7 +52,10 @@ const InferenceServiceListView: React.FC = ({ clearFilters={resetFilters} servingRuntimes={servingRuntimes} inferenceServices={filteredInferenceServices} - refresh={refresh} + refresh={() => { + refresh(); + }} + filterTokens={filterTokens} enablePagination toolbarContent={ <> diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx index 4cacb90e94..41b2b66142 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ManageInferenceServiceModal from '~/pages/modelServing/screens/projects/InferenceServiceModal/ManageInferenceServiceModal'; import { Table } from '~/components/table'; -import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; +import { InferenceServiceKind, SecretKind, ServingRuntimeKind } from '~/k8sTypes'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; import { isModelMesh } from '~/pages/modelServing/utils'; @@ -12,17 +12,19 @@ import { getGlobalInferenceServiceColumns, getProjectInferenceServiceColumns } f import DeleteInferenceServiceModal from './DeleteInferenceServiceModal'; type InferenceServiceTableProps = { - clearFilters?: () => void; inferenceServices: InferenceServiceKind[]; servingRuntimes: ServingRuntimeKind[]; refresh: () => void; + clearFilters?: () => void; + filterTokens?: (servingRuntime?: string | undefined) => SecretKind[]; } & Partial, 'enablePagination' | 'toolbarContent'>>; const InferenceServiceTable: React.FC = ({ - clearFilters, inferenceServices, servingRuntimes, refresh, + filterTokens, + clearFilters, enablePagination, toolbarContent, }) => { @@ -34,6 +36,7 @@ const InferenceServiceTable: React.FC = ({ const mappedColumns = isGlobal ? getGlobalInferenceServiceColumns(projects) : getProjectInferenceServiceColumns(); + return ( <> = ({ : undefined, secrets: [], }, + secrets: filterTokens ? filterTokens(editInferenceService?.metadata.name) : [], }} onClose={(edited) => { if (edited) { diff --git a/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx b/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx index d63bdebd5f..b83db1f6e1 100644 --- a/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx +++ b/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx @@ -13,8 +13,18 @@ import ModelServingLoading from './ModelServingLoading'; const ModelServingGlobal: React.FC = () => { const { - servingRuntimes: { data: servingRuntimes, loaded: servingRuntimesLoaded }, - inferenceServices: { data: inferenceServices, loaded: inferenceServicesLoaded }, + servingRuntimes: { + data: servingRuntimes, + loaded: servingRuntimesLoaded, + refresh: refreshServingRuntimes, + }, + inferenceServices: { + data: inferenceServices, + loaded: inferenceServicesLoaded, + refresh: refreshInferenceServices, + }, + serverSecrets: { refresh: refreshServerSecrets }, + filterTokens, project: currentProject, preferredProject, projects, @@ -62,6 +72,12 @@ const ModelServingGlobal: React.FC = () => { { + refreshInferenceServices(); + refreshServingRuntimes(); + refreshServerSecrets(); + }} + filterTokens={filterTokens} /> ); diff --git a/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx b/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx index ef5f35c4f0..bfe47f5c07 100644 --- a/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx +++ b/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx @@ -13,7 +13,7 @@ jest.mock('~/pages/modelServing/useServingRuntimes', () => ({ const useServingRuntimesMock = jest.mocked(useServingRuntimes); describe('ManageInferenceServiceModal', () => { - it('should not rerender serving runtime selection', async () => { + it('should not re-render serving runtime selection', async () => { useServingRuntimesMock.mockReturnValue([ [ mockServingRuntimeK8sResource({ name: 'runtime1', displayName: 'Runtime 1' }), diff --git a/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTable.tsx b/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTable.tsx index e6f9e5d467..207a25a12c 100644 --- a/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTable.tsx +++ b/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTable.tsx @@ -27,6 +27,8 @@ const KServeInferenceServiceTable: React.FC = () => { servingRuntimes: { refresh: refreshServingRuntime }, dataConnections: { refresh: refreshDataConnections }, inferenceServices: { data: inferenceServices, refresh: refreshInferenceServices }, + serverSecrets: { refresh: refreshServerSecrets }, + filterTokens, } = React.useContext(ProjectDetailsContext); return ( @@ -67,6 +69,7 @@ const KServeInferenceServiceTable: React.FC = () => { secrets: [], }, inferenceServiceEditInfo: editKserveResources?.inferenceService, + secrets: filterTokens(editKserveResources?.inferenceService.metadata.name), }} onClose={(submit: boolean) => { setEditKServeResources(undefined); @@ -74,6 +77,7 @@ const KServeInferenceServiceTable: React.FC = () => { refreshServingRuntime(); refreshInferenceServices(); refreshDataConnections(); + refreshServerSecrets(); } }} /> diff --git a/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTableRow.tsx b/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTableRow.tsx index 393321f901..e6bffc8749 100644 --- a/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTableRow.tsx @@ -13,6 +13,9 @@ import InferenceServiceTableRow from '~/pages/modelServing/screens/global/Infere import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import ServingRuntimeDetails from '~/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeDetails'; import ResourceTr from '~/components/ResourceTr'; +import ServingRuntimeTokensTable from '~/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeTokensTable'; +import { isInferenceServiceTokenEnabled } from '~/pages/modelServing/screens/projects/utils'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; type KServeInferenceServiceTableRowProps = { obj: InferenceServiceKind; @@ -33,6 +36,8 @@ const KServeInferenceServiceTableRow: React.FC { + const isAuthorinoEnabled = useIsAreaAvailable(SupportedArea.K_SERVE_AUTH).status; + const [isExpanded, setExpanded] = React.useState(false); const { servingRuntimes: { data: servingRuntimes }, @@ -100,6 +105,21 @@ const KServeInferenceServiceTableRow: React.FC )} + {isAuthorinoEnabled && ( + + + + Token authorization + + + + + + + )} diff --git a/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeTokensTable.tsx b/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeTokensTable.tsx index d3a8f02413..89240e0d3f 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeTokensTable.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeTokensTable.tsx @@ -3,11 +3,11 @@ import { HelperText, HelperTextItem } from '@patternfly/react-core'; import { Table } from '~/components/table'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { tokenColumns } from '~/pages/modelServing/screens/global/data'; -import { ServingRuntimeKind } from '~/k8sTypes'; +import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; import ServingRuntimeTokenTableRow from '~/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeTokenTableRow'; type ServingRuntimeTokensTableProps = { - obj: ServingRuntimeKind; + obj: ServingRuntimeKind | InferenceServiceKind; isTokenEnabled: boolean; }; diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/AuthServingRuntimeSection.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/AuthServingRuntimeSection.tsx new file mode 100644 index 0000000000..b0dd5b7bcd --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/AuthServingRuntimeSection.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { + Alert, + Button, + Checkbox, + FormGroup, + FormSection, + Popover, + Stack, + StackItem, + getUniqueId, +} from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; +import { + CreatingInferenceServiceObject, + CreatingServingRuntimeObject, +} from '~/pages/modelServing/screens/types'; + +import ServingRuntimeTokenSection from './ServingRuntimeTokenSection'; + +type AuthServingRuntimeSectionProps = { + data: CreatingServingRuntimeObject | CreatingInferenceServiceObject; + setData: + | UpdateObjectAtPropAndValue + | UpdateObjectAtPropAndValue; + allowCreate: boolean; + publicRoute?: boolean; +}; + +const AuthServingRuntimeSection: React.FC = ({ + data, + setData, + allowCreate, + publicRoute, +}) => { + const createNewToken = React.useCallback(() => { + const name = 'default-name'; + const duplicated = data.tokens.filter((token) => token.name === name); + const duplicatedError = duplicated.length > 0 ? 'Duplicates are invalid' : ''; + setData('tokens', [ + ...data.tokens, + { + name, + uuid: getUniqueId('ml'), + error: duplicatedError, + }, + ]); + }, [data.tokens, setData]); + + return ( + + {!allowCreate && ( + + + + + + )} + {publicRoute && ( + + + + { + setData('externalRoute', check); + if (check && allowCreate) { + setData('tokenAuth', check); + if (data.tokens.length === 0) { + createNewToken(); + } + } + }} + /> + + + + )} + + + + {((publicRoute && data.externalRoute && !data.tokenAuth) || + (!publicRoute && !data.tokenAuth)) && ( + + + + )} + + ); +}; + +export default AuthServingRuntimeSection; diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx index 1447ff0bb7..ee0a74c171 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx @@ -1,25 +1,11 @@ import * as React from 'react'; -import { - Alert, - Button, - Checkbox, - Form, - FormGroup, - FormSection, - Modal, - Popover, - Stack, - StackItem, - getUniqueId, -} from '@patternfly/react-core'; +import { Form, Modal, Stack, StackItem } from '@patternfly/react-core'; import { EitherOrNone } from '@openshift/dynamic-plugin-sdk'; -import { HelpIcon } from '@patternfly/react-icons'; import { submitServingRuntimeResourcesWithDryRun, useCreateServingRuntimeObject, } from '~/pages/modelServing/screens/projects/utils'; import { TemplateKind, ProjectKind, AccessReviewResourceAttributes } from '~/k8sTypes'; -import { useAccessReview } from '~/api'; import { isModelServerEditInfoChanged, requestsUnderLimits, @@ -31,11 +17,12 @@ import useServingAcceleratorProfile from '~/pages/modelServing/screens/projects/ import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; import { NamespaceApplicationCase } from '~/pages/projects/types'; import { ServingRuntimeEditInfo } from '~/pages/modelServing/screens/types'; +import { useAccessReview } from '~/api'; import ServingRuntimeReplicaSection from './ServingRuntimeReplicaSection'; import ServingRuntimeSizeSection from './ServingRuntimeSizeSection'; -import ServingRuntimeTokenSection from './ServingRuntimeTokenSection'; import ServingRuntimeTemplateSection from './ServingRuntimeTemplateSection'; import ServingRuntimeNameSection from './ServingRuntimeNameSection'; +import AuthServingRuntimeSection from './AuthServingRuntimeSection'; type ManageServingRuntimeModalProps = { isOpen: boolean; @@ -140,20 +127,6 @@ const ManageServingRuntimeModal: React.FC = ({ }); }; - const createNewToken = React.useCallback(() => { - const name = 'default-name'; - const duplicated = createData.tokens.filter((token) => token.name === name); - const duplicatedError = duplicated.length > 0 ? 'Duplicates are invalid' : ''; - setCreateData('tokens', [ - ...createData.tokens, - { - name, - uuid: getUniqueId('ml'), - error: duplicatedError, - }, - ]); - }, [createData.tokens, setCreateData]); - return ( = ({ infoContent="Select a server size that will accommodate your largest model. See the product documentation for more information." /> - {!allowCreate && ( - - - - - - )} - - - - { - setCreateData('externalRoute', check); - if (check && allowCreate) { - setCreateData('tokenAuth', check); - if (createData.tokens.length === 0) { - createNewToken(); - } - } - }} - /> - - - - - - - {createData.externalRoute && !createData.tokenAuth && ( - - - - )} + diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenInput.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenInput.tsx index 4225bd7bcb..46e7556c2c 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenInput.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenInput.tsx @@ -13,14 +13,17 @@ import { import { ExclamationCircleIcon, MinusCircleIcon } from '@patternfly/react-icons'; import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; import { + CreatingInferenceServiceObject, CreatingServingRuntimeObject, ServingRuntimeToken, } from '~/pages/modelServing/screens/types'; import { translateDisplayNameForK8s } from '~/pages/projects/utils'; type ServingRuntimeTokenInputProps = { - data: CreatingServingRuntimeObject; - setData: UpdateObjectAtPropAndValue; + data: CreatingServingRuntimeObject | CreatingInferenceServiceObject; + setData: + | UpdateObjectAtPropAndValue + | UpdateObjectAtPropAndValue; token: ServingRuntimeToken; disabled?: boolean; }; diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenSection.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenSection.tsx index e43a2f25c8..ec938c4e28 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenSection.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTokenSection.tsx @@ -11,12 +11,17 @@ import { import { PlusCircleIcon } from '@patternfly/react-icons'; import IndentSection from '~/pages/projects/components/IndentSection'; import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; -import { CreatingServingRuntimeObject } from '~/pages/modelServing/screens/types'; +import { + CreatingInferenceServiceObject, + CreatingServingRuntimeObject, +} from '~/pages/modelServing/screens/types'; import ServingRuntimeTokenInput from './ServingRuntimeTokenInput'; type ServingRuntimeTokenSectionProps = { - data: CreatingServingRuntimeObject; - setData: UpdateObjectAtPropAndValue; + data: CreatingServingRuntimeObject | CreatingInferenceServiceObject; + setData: + | UpdateObjectAtPropAndValue + | UpdateObjectAtPropAndValue; allowCreate: boolean; createNewToken: () => void; }; diff --git a/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx b/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx index 58b518e941..5c95fc07e0 100644 --- a/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx +++ b/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx @@ -1,5 +1,13 @@ import * as React from 'react'; -import { Form, FormSection, Modal, Stack, StackItem } from '@patternfly/react-core'; +import { + Alert, + AlertActionCloseButton, + Form, + FormSection, + Modal, + Stack, + StackItem, +} from '@patternfly/react-core'; import { EitherOrNone } from '@openshift/dynamic-plugin-sdk'; import { getSubmitInferenceServiceResourceFn, @@ -7,7 +15,13 @@ import { useCreateInferenceServiceObject, useCreateServingRuntimeObject, } from '~/pages/modelServing/screens/projects/utils'; -import { TemplateKind, ProjectKind, InferenceServiceKind } from '~/k8sTypes'; +import { + TemplateKind, + ProjectKind, + InferenceServiceKind, + AccessReviewResourceAttributes, + SecretKind, +} from '~/k8sTypes'; import { requestsUnderLimits, resourcesArePositive } from '~/pages/modelServing/utils'; import useCustomServingRuntimesEnabled from '~/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled'; import { getServingRuntimeFromName } from '~/pages/modelServing/customServingRuntimes/utils'; @@ -28,8 +42,17 @@ import InferenceServiceFrameworkSection from '~/pages/modelServing/screens/proje import DataConnectionSection from '~/pages/modelServing/screens/projects/InferenceServiceModal/DataConnectionSection'; import { getProjectDisplayName, translateDisplayNameForK8s } from '~/pages/projects/utils'; import { containsOnlySlashes, isS3PathValid } from '~/utilities/string'; +import AuthServingRuntimeSection from '~/pages/modelServing/screens/projects/ServingRuntimeModal/AuthServingRuntimeSection'; +import { useAccessReview } from '~/api'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import KServeAutoscalerReplicaSection from './KServeAutoscalerReplicaSection'; +const accessReviewResource: AccessReviewResourceAttributes = { + group: 'rbac.authorization.k8s.io', + resource: 'rolebindings', + verb: 'create', +}; + type ManageKServeModalProps = { isOpen: boolean; onClose: (submit: boolean) => void; @@ -45,6 +68,7 @@ type ManageKServeModalProps = { editInfo?: { servingRuntimeEditInfo?: ServingRuntimeEditInfo; inferenceServiceEditInfo?: InferenceServiceKind; + secrets?: SecretKind[]; }; } >; @@ -59,28 +83,38 @@ const ManageKServeModal: React.FC = ({ const [createDataServingRuntime, setCreateDataServingRuntime, resetDataServingRuntime, sizes] = useCreateServingRuntimeObject(editInfo?.servingRuntimeEditInfo); const [createDataInferenceService, setCreateDataInferenceService, resetDataInferenceService] = - useCreateInferenceServiceObject(editInfo?.inferenceServiceEditInfo); + useCreateInferenceServiceObject( + editInfo?.inferenceServiceEditInfo, + editInfo?.servingRuntimeEditInfo?.servingRuntime, + editInfo?.secrets, + ); + + const isAuthorinoEnabled = useIsAreaAvailable(SupportedArea.K_SERVE_AUTH).status; + const currentProjectName = projectContext?.currentProject.metadata.name; + const namespace = currentProjectName || createDataInferenceService.project; + const isInferenceServiceNameWithinLimit = + translateDisplayNameForK8s(createDataInferenceService.name).length <= 253; + const [acceleratorProfileState, setAcceleratorProfileState, resetAcceleratorProfileData] = useServingAcceleratorProfile( editInfo?.servingRuntimeEditInfo?.servingRuntime, editInfo?.inferenceServiceEditInfo, ); + const customServingRuntimesEnabled = useCustomServingRuntimesEnabled(); + const [allowCreate] = useAccessReview({ + ...accessReviewResource, + namespace, + }); const [actionInProgress, setActionInProgress] = React.useState(false); const [error, setError] = React.useState(); - const isInferenceServiceNameWithinLimit = - translateDisplayNameForK8s(createDataInferenceService.name).length <= 253; + const [alertVisible, setAlertVisible] = React.useState(true); React.useEffect(() => { - if (projectContext?.currentProject) { - setCreateDataInferenceService('project', projectContext.currentProject.metadata.name); + if (currentProjectName && isOpen) { + setCreateDataInferenceService('project', currentProjectName); } - }, [projectContext, setCreateDataInferenceService]); - - const customServingRuntimesEnabled = useCustomServingRuntimesEnabled(); - - const namespace = - projectContext?.currentProject.metadata.name || createDataInferenceService.project; + }, [currentProjectName, setCreateDataInferenceService, isOpen]); // Serving Runtime Validation const baseInputValueValid = @@ -126,6 +160,7 @@ const ManageKServeModal: React.FC = ({ resetDataServingRuntime(); resetDataInferenceService(); resetAcceleratorProfileData(); + setAlertVisible(true); }; const setErrorModal = (e: Error) => { @@ -152,7 +187,7 @@ const ManageKServeModal: React.FC = ({ customServingRuntimesEnabled, namespace, editInfo?.servingRuntimeEditInfo, - true, + false, acceleratorProfileState, NamespaceApplicationCase.KSERVE_PROMOTION, projectContext?.currentProject, @@ -166,6 +201,8 @@ const ManageKServeModal: React.FC = ({ servingRuntimeName, false, acceleratorProfileState, + allowCreate, + editInfo?.secrets, ); Promise.all([ @@ -210,6 +247,27 @@ const ManageKServeModal: React.FC = ({ }} > + {!isAuthorinoEnabled && alertVisible && ( + + setAlertVisible(false)} />} + > +

+ The single model serving platform used by this project allows deployed models to + be accessible via external routes. It is recommended that token authorization be + enabled to protect these routes. The serving platform requires the Authorino + operator be installed on the cluster for token authorization. Contact a cluster + administrator to install the operator. +

+
+
+ )} = ({ infoContent="Select a server size that will accommodate your largest model. See the product documentation for more information." /> + {isAuthorinoEnabled && ( + + + + )} servingRuntime.metadata.annotations?.['enable-route'] === 'true'; +export const isInferenceServiceTokenEnabled = (inferenceService: InferenceServiceKind): boolean => + inferenceService.metadata.annotations?.['security.opendatahub.io/enable-auth'] === 'true'; + export const isGpuDisabled = (servingRuntime: ServingRuntimeKind): boolean => servingRuntime.metadata.annotations?.['opendatahub.io/disable-gpu'] === 'true'; @@ -155,11 +158,15 @@ export const defaultInferenceService: CreatingInferenceServiceObject = { }, minReplicas: 1, maxReplicas: 1, + externalRoute: false, + tokenAuth: false, + tokens: [], }; export const useCreateInferenceServiceObject = ( existingData?: InferenceServiceKind, existingServingRuntimeData?: ServingRuntimeKind, // upgrade path to already KServe models + secrets?: SecretKind[], ): [ data: CreatingInferenceServiceObject, setData: UpdateObjectAtPropAndValue, @@ -183,6 +190,12 @@ export const useCreateInferenceServiceObject = ( const existingMaxReplicas = existingData?.spec.predictor.maxReplicas || existingServingRuntimeData?.spec.replicas || 1; + const existingExternalRoute = false; // TODO: Change this in the future in case we have an External Route + const existingTokenAuth = + existingData?.metadata.annotations?.['security.opendatahub.io/enable-auth'] === 'true'; + + const existingTokens = useDeepCompareMemoize(getServingRuntimeTokens(secrets)); + React.useEffect(() => { if (existingName) { setCreateData('name', existingName); @@ -202,6 +215,9 @@ export const useCreateInferenceServiceObject = ( ); setCreateData('minReplicas', existingMinReplicas); setCreateData('maxReplicas', existingMaxReplicas); + setCreateData('externalRoute', existingExternalRoute); + setCreateData('tokenAuth', existingTokenAuth); + setCreateData('tokens', existingTokens); } }, [ existingName, @@ -212,6 +228,9 @@ export const useCreateInferenceServiceObject = ( existingMinReplicas, existingMaxReplicas, setCreateData, + existingExternalRoute, + existingTokenAuth, + existingTokens, ]); return createInferenceServiceState; @@ -321,7 +340,9 @@ export const getSubmitInferenceServiceResourceFn = ( servingRuntimeName?: string, isModelMesh?: boolean, acceleratorProfileState?: AcceleratorProfileState, -): ((opts: { dryRun?: boolean }) => Promise) => { + allowCreate?: boolean, + secrets?: SecretKind[], +): ((opts: { dryRun?: boolean }) => Promise) => { const inferenceServiceData = { ...createData, ...(servingRuntimeName !== undefined && { @@ -338,6 +359,9 @@ export const getSubmitInferenceServiceResourceFn = ( const existingStorage = inferenceServiceData.storage.type === InferenceServiceStorageType.EXISTING_STORAGE; + const createTokenAuth = createData.tokenAuth && !!allowCreate; + const inferenceServiceName = translateDisplayNameForK8s(inferenceServiceData.name); + return ({ dryRun = false }) => createInferenceServiceAndDataConnection( inferenceServiceData, @@ -346,12 +370,24 @@ export const getSubmitInferenceServiceResourceFn = ( isModelMesh, acceleratorProfileState, dryRun, + ).then((inferenceService) => + setUpTokenAuth( + createData, + inferenceServiceName, + createData.project, + createTokenAuth, + inferenceService, + secrets || [], + { + dryRun, + }, + ), ); }; export const submitInferenceServiceResourceWithDryRun = async ( ...params: Parameters -): Promise => { +): Promise => { const submitInferenceServiceResource = getSubmitInferenceServiceResourceFn(...params); await submitInferenceServiceResource({ dryRun: true }); return submitInferenceServiceResource({ dryRun: false }); diff --git a/frontend/src/pages/modelServing/screens/types.ts b/frontend/src/pages/modelServing/screens/types.ts index 3efe8d182a..5967f4e248 100644 --- a/frontend/src/pages/modelServing/screens/types.ts +++ b/frontend/src/pages/modelServing/screens/types.ts @@ -68,6 +68,9 @@ export type CreatingInferenceServiceObject = { format: InferenceServiceFormat; maxReplicas: number; minReplicas: number; + externalRoute: boolean; + tokenAuth: boolean; + tokens: ServingRuntimeToken[]; }; export enum InferenceServiceStorageType { diff --git a/frontend/src/pages/modelServing/utils.ts b/frontend/src/pages/modelServing/utils.ts index 0dbcdb7279..ab3491b01f 100644 --- a/frontend/src/pages/modelServing/utils.ts +++ b/frontend/src/pages/modelServing/utils.ts @@ -31,6 +31,7 @@ import { import { ContainerResources } from '~/types'; import { getDisplayNameFromK8sResource, translateDisplayNameForK8s } from '~/pages/projects/utils'; import { + CreatingInferenceServiceObject, CreatingServingRuntimeObject, ServingRuntimeEditInfo, ServingRuntimeSize, @@ -65,15 +66,15 @@ export const requestsUnderLimits = (resources: ContainerResources): boolean => isMemoryLimitLarger(resources.requests?.memory, resources.limits?.memory, true); export const setUpTokenAuth = async ( - fillData: CreatingServingRuntimeObject, - servingRuntimeName: string, + fillData: CreatingServingRuntimeObject | CreatingInferenceServiceObject, + deployedModelName: string, namespace: string, createTokenAuth: boolean, - owner: ServingRuntimeKind, + owner: ServingRuntimeKind | InferenceServiceKind, existingSecrets?: SecretKind[], opts?: K8sAPIOptions, ): Promise => { - const { serviceAccountName, roleBindingName } = getTokenNames(servingRuntimeName, namespace); + const { serviceAccountName, roleBindingName } = getTokenNames(deployedModelName, namespace); const serviceAccount = addOwnerReference( assembleServiceAccount(serviceAccountName, namespace), @@ -91,7 +92,7 @@ export const setUpTokenAuth = async ( ]) : Promise.resolve() ) - .then(() => createSecrets(fillData, servingRuntimeName, namespace, existingSecrets, opts)) + .then(() => createSecrets(fillData, deployedModelName, namespace, existingSecrets, opts)) .catch((error) => Promise.reject(error)); }; @@ -120,13 +121,13 @@ export const createRoleBindingIfMissing = async ( }); export const createSecrets = async ( - fillData: CreatingServingRuntimeObject, - servingRuntimeName: string, + fillData: CreatingServingRuntimeObject | CreatingInferenceServiceObject, + deployedModelName: string, namespace: string, existingSecrets?: SecretKind[], opts?: K8sAPIOptions, ): Promise => { - const { serviceAccountName } = getTokenNames(servingRuntimeName, namespace); + const { serviceAccountName } = getTokenNames(deployedModelName, namespace); const deletedSecrets = existingSecrets ?.map((secret) => secret.metadata.name) diff --git a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml index b9dbe2465a..a6d5a781c1 100644 --- a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml +++ b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml @@ -57,6 +57,8 @@ spec: type: boolean disableKServe: type: boolean + disableKServeAuth: + type: boolean disableModelMesh: type: boolean disableAcceleratorProfiles: diff --git a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml index b2749545cf..4abc7781c8 100644 --- a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml +++ b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml @@ -23,6 +23,7 @@ spec: disablePerformanceMetrics: true disableAcceleratorProfiles: true disableKServe: false + disableKServeAuth: false disableModelMesh: false disableDistributedWorkloads: true notebookController: diff --git a/manifests/overlays/odhdashboardconfig/odhdashboardconfig.yaml b/manifests/overlays/odhdashboardconfig/odhdashboardconfig.yaml index 8028226b98..c35edcdf58 100644 --- a/manifests/overlays/odhdashboardconfig/odhdashboardconfig.yaml +++ b/manifests/overlays/odhdashboardconfig/odhdashboardconfig.yaml @@ -22,6 +22,7 @@ spec: disablePerformanceMetrics: false disableAcceleratorProfiles: false disableKServe: false + disableKServeAuth: false disableModelMesh: false disableDistributedWorkloads: true disableModelRegistry: true