diff --git a/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts b/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts index 4fab3ccd15..5d543d3fb7 100644 --- a/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts +++ b/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts @@ -18,6 +18,10 @@ const DEFAULT_CLUSTER_SETTINGS: ClusterSettings = { cullerTimeout: DEFAULT_CULLER_TIMEOUT, userTrackingEnabled: false, notebookTolerationSettings: { enabled: false, key: 'NotebooksOnly' }, + modelServingPlatformEnabled: { + kServe: true, + modelMesh: false, + }, }; export const updateClusterSettings = async ( @@ -28,10 +32,30 @@ export const updateClusterSettings = async ( ): Promise<{ success: boolean; error: string }> => { const coreV1Api = fastify.kube.coreV1Api; const namespace = fastify.kube.namespace; - const { pvcSize, cullerTimeout, userTrackingEnabled, notebookTolerationSettings } = request.body; + const { + pvcSize, + cullerTimeout, + userTrackingEnabled, + notebookTolerationSettings, + modelServingPlatformEnabled, + } = request.body; const dashConfig = getDashboardConfig(); const isJupyterEnabled = checkJupyterEnabled(); try { + if ( + modelServingPlatformEnabled.kServe !== !dashConfig.spec.dashboardConfig.disableKServe || + modelServingPlatformEnabled.modelMesh !== !dashConfig.spec.dashboardConfig.disableModelMesh + ) { + await setDashboardConfig(fastify, { + spec: { + dashboardConfig: { + disableKServe: !modelServingPlatformEnabled.kServe, + disableModelMesh: !modelServingPlatformEnabled.modelMesh, + }, + }, + }); + } + await patchCM(fastify, segmentKeyCfg, { data: { segmentKeyEnabled: String(userTrackingEnabled) }, }).catch((e) => { @@ -41,7 +65,6 @@ export const updateClusterSettings = async ( if (isJupyterEnabled) { await setDashboardConfig(fastify, { spec: { - dashboardConfig: dashConfig.spec.dashboardConfig, notebookController: { enabled: isJupyterEnabled, pvcSize: `${pvcSize}Gi`, @@ -124,10 +147,14 @@ export const getClusterSettings = async ( ): Promise => { const coreV1Api = fastify.kube.coreV1Api; const namespace = fastify.kube.namespace; - const clusterSettings = { + const dashConfig = getDashboardConfig(); + const clusterSettings: ClusterSettings = { ...DEFAULT_CLUSTER_SETTINGS, + modelServingPlatformEnabled: { + kServe: !dashConfig.spec.dashboardConfig.disableKServe, + modelMesh: !dashConfig.spec.dashboardConfig.disableModelMesh, + }, }; - const dashConfig = getDashboardConfig(); const isJupyterEnabled = checkJupyterEnabled(); if (!dashConfig.spec.dashboardConfig.disableTracking) { try { diff --git a/backend/src/types.ts b/backend/src/types.ts index 307e4f516d..4287cc10e4 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -102,6 +102,10 @@ export type ClusterSettings = { cullerTimeout: number; userTrackingEnabled: boolean; notebookTolerationSettings: NotebookTolerationSettings | null; + modelServingPlatformEnabled: { + kServe: boolean; + modelMesh: boolean; + }; }; // Add a minimal QuickStart type here as there is no way to get types without pulling in frontend (React) modules diff --git a/frontend/src/__mocks__/mockClusterSettings.ts b/frontend/src/__mocks__/mockClusterSettings.ts index 884d25e8cc..5c726e8203 100644 --- a/frontend/src/__mocks__/mockClusterSettings.ts +++ b/frontend/src/__mocks__/mockClusterSettings.ts @@ -9,9 +9,14 @@ export const mockClusterSettings = ({ key: 'NotebooksOnlyChange', enabled: true, }, + modelServingPlatformEnabled = { + kServe: true, + modelMesh: true, + }, }: Partial): ClusterSettingsType => ({ userTrackingEnabled, cullerTimeout, pvcSize, notebookTolerationSettings, + modelServingPlatformEnabled, }); diff --git a/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts b/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts index b4b861499a..380fa926b6 100644 --- a/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts +++ b/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts @@ -6,6 +6,29 @@ test('Cluster settings', async ({ page }) => { // wait for page to load await page.waitForSelector('text=Save changes'); const submitButton = page.locator('[data-id="submit-cluster-settings"]'); + + // check serving platform field + const singlePlatformCheckbox = page.locator( + '[data-id="single-model-serving-platform-enabled-checkbox"]', + ); + const multiPlatformCheckbox = page.locator( + '[data-id="multi-model-serving-platform-enabled-checkbox"]', + ); + const warningAlert = page.locator('[data-id="serving-platform-warning-alert"]'); + await expect(singlePlatformCheckbox).toBeChecked(); + await expect(multiPlatformCheckbox).toBeChecked(); + await expect(submitButton).toBeDisabled(); + await multiPlatformCheckbox.uncheck(); + await expect(warningAlert).toBeVisible(); + expect(warningAlert.getByLabel('Info Alert')).toBeTruthy(); + await expect(submitButton).toBeEnabled(); + await singlePlatformCheckbox.uncheck(); + expect(warningAlert.getByLabel('Warning Alert')).toBeTruthy(); + await singlePlatformCheckbox.check(); + await multiPlatformCheckbox.check(); + await expect(warningAlert).toBeHidden(); + await expect(submitButton).toBeDisabled(); + // check PVC size field const pvcInputField = page.locator('[data-id="pvc-size-input"]'); const pvcHint = page.locator('[data-id="pvc-size-helper-text"]'); diff --git a/frontend/src/components/SettingSection.tsx b/frontend/src/components/SettingSection.tsx index cf6b35df47..c608576dcb 100644 --- a/frontend/src/components/SettingSection.tsx +++ b/frontend/src/components/SettingSection.tsx @@ -4,7 +4,7 @@ import { Card, CardBody, CardFooter, CardTitle, Stack, StackItem } from '@patter type SettingSectionProps = { children: React.ReactNode; title: string; - description?: string; + description?: React.ReactNode; footer?: React.ReactNode; }; diff --git a/frontend/src/pages/clusterSettings/ClusterSettings.tsx b/frontend/src/pages/clusterSettings/ClusterSettings.tsx index f8a20e6be3..55b1be7207 100644 --- a/frontend/src/pages/clusterSettings/ClusterSettings.tsx +++ b/frontend/src/pages/clusterSettings/ClusterSettings.tsx @@ -4,7 +4,11 @@ import { Button, Stack, StackItem } from '@patternfly/react-core'; import ApplicationsPage from '~/pages/ApplicationsPage'; import { useAppContext } from '~/app/AppContext'; import { fetchClusterSettings, updateClusterSettings } from '~/services/clusterSettingsService'; -import { ClusterSettingsType, NotebookTolerationFormSettings } from '~/types'; +import { + ClusterSettingsType, + ModelServingPlatformEnabled, + NotebookTolerationFormSettings, +} from '~/types'; import { addNotification } from '~/redux/actions/actions'; import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils'; import { useAppDispatch } from '~/redux/hooks'; @@ -12,6 +16,7 @@ import PVCSizeSettings from '~/pages/clusterSettings/PVCSizeSettings'; import CullerSettings from '~/pages/clusterSettings/CullerSettings'; import TelemetrySettings from '~/pages/clusterSettings/TelemetrySettings'; import TolerationSettings from '~/pages/clusterSettings/TolerationSettings'; +import ModelServingPlatformSettings from '~/pages/clusterSettings/ModelServingPlatformSettings'; import { DEFAULT_CONFIG, DEFAULT_PVC_SIZE, @@ -35,12 +40,15 @@ const ClusterSettings: React.FC = () => { enabled: false, key: isJupyterEnabled ? DEFAULT_TOLERATION_VALUE : '', }); + const [modelServingEnabledPlatforms, setModelServingEnabledPlatforms] = + React.useState(clusterSettings.modelServingPlatformEnabled); const dispatch = useAppDispatch(); React.useEffect(() => { fetchClusterSettings() .then((clusterSettings: ClusterSettingsType) => { setClusterSettings(clusterSettings); + setModelServingEnabledPlatforms(clusterSettings.modelServingPlatformEnabled); setLoaded(true); setLoadError(undefined); }) @@ -59,8 +67,16 @@ const ClusterSettings: React.FC = () => { enabled: notebookTolerationSettings.enabled, key: notebookTolerationSettings.key, }, + modelServingPlatformEnabled: modelServingEnabledPlatforms, }), - [pvcSize, cullerTimeout, userTrackingEnabled, clusterSettings, notebookTolerationSettings], + [ + pvcSize, + cullerTimeout, + userTrackingEnabled, + clusterSettings, + notebookTolerationSettings, + modelServingEnabledPlatforms, + ], ); const handleSaveButtonClicked = () => { @@ -72,6 +88,7 @@ const ClusterSettings: React.FC = () => { enabled: notebookTolerationSettings.enabled, key: notebookTolerationSettings.key, }, + modelServingPlatformEnabled: modelServingEnabledPlatforms, }; if (!_.isEqual(clusterSettings, newClusterSettings)) { if ( @@ -123,6 +140,13 @@ const ClusterSettings: React.FC = () => { provideChildrenPadding > + + + void; +}; + +const ModelServingPlatformSettings: React.FC = ({ + initialValue, + enabledPlatforms, + setEnabledPlatforms, +}) => { + const [alert, setAlert] = React.useState<{ variant: AlertVariant; message: string }>(); + + React.useEffect(() => { + if (!enabledPlatforms.kServe && !enabledPlatforms.modelMesh) { + setAlert({ + variant: AlertVariant.warning, + message: + 'Disabling both model serving platforms prevents new projects from deploying models. Models can still be deployed from existing projects that already have a serving platform.', + }); + } else { + if (initialValue.modelMesh && !enabledPlatforms.modelMesh) { + setAlert({ + variant: AlertVariant.info, + message: + 'Disabling the multi-model serving platform prevents models deployed in new projects and in existing projects with no deployed models from sharing model servers. Existing projects with deployed models will continue to use multi-model serving.', + }); + } else { + setAlert(undefined); + } + } + }, [enabledPlatforms, initialValue]); + + return ( + + + + { + const newEnabledPlatforms: ModelServingPlatformEnabled = { + ...enabledPlatforms, + kServe: enabled, + }; + setEnabledPlatforms(newEnabledPlatforms); + }} + aria-label="Single model serving platform enabled checkbox" + id="single-model-serving-platform-enabled-checkbox" + data-id="single-model-serving-platform-enabled-checkbox" + name="singleModelServingPlatformEnabledCheckbox" + /> + + + { + const newEnabledPlatforms: ModelServingPlatformEnabled = { + ...enabledPlatforms, + modelMesh: enabled, + }; + setEnabledPlatforms(newEnabledPlatforms); + }} + aria-label="Multi-model serving platform enabled checkbox" + id="multi-model-serving-platform-enabled-checkbox" + data-id="multi-model-serving-platform-enabled-checkbox" + name="multiModelServingPlatformEnabledCheckbox" + /> + + {alert && ( + + setAlert(undefined)} />} + /> + + )} + + + ); +}; + +export default ModelServingPlatformSettings; diff --git a/frontend/src/pages/clusterSettings/const.ts b/frontend/src/pages/clusterSettings/const.ts index cf8326eabe..5a4613778e 100644 --- a/frontend/src/pages/clusterSettings/const.ts +++ b/frontend/src/pages/clusterSettings/const.ts @@ -17,6 +17,10 @@ export const DEFAULT_CONFIG: ClusterSettingsType = { cullerTimeout: DEFAULT_CULLER_TIMEOUT, userTrackingEnabled: false, notebookTolerationSettings: null, + modelServingPlatformEnabled: { + kServe: true, + modelMesh: false, + }, }; export const DEFAULT_TOLERATION_VALUE = 'NotebooksOnly'; export const TOLERATION_FORMAT = /^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$/; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2d3dc86463..1f9d985015 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -157,6 +157,12 @@ export type ClusterSettingsType = { pvcSize: number | string; cullerTimeout: number; notebookTolerationSettings: TolerationSettings | null; + modelServingPlatformEnabled: ModelServingPlatformEnabled; +}; + +export type ModelServingPlatformEnabled = { + kServe: boolean; + modelMesh: boolean; }; /** @deprecated -- use SDK type */