diff --git a/backend/src/types.ts b/backend/src/types.ts index 4024b82a55..59a132bd14 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -40,6 +40,7 @@ export type DashboardConfig = K8sResourceCommon & { disableHardwareProfiles: boolean; disableDistributedWorkloads: boolean; disableModelRegistry: boolean; + disableModelRegistrySecureDB: boolean; disableServingRuntimeParams: boolean; disableConnectionTypes: boolean; disableStorageClasses: boolean; diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 9e1a94a3c7..1091725da0 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -65,6 +65,7 @@ export const blankDashboardCR: DashboardConfig = { disableHardwareProfiles: true, disableDistributedWorkloads: false, disableModelRegistry: false, + disableModelRegistrySecureDB: true, disableServingRuntimeParams: false, disableConnectionTypes: false, disableStorageClasses: false, diff --git a/docs/dashboard-config.md b/docs/dashboard-config.md index c4eeb8d7a4..ef40f7304b 100644 --- a/docs/dashboard-config.md +++ b/docs/dashboard-config.md @@ -36,6 +36,7 @@ The following are a list of features that are supported, along with there defaul | disablePerformanceMetrics | false | Disables Endpoint Performance tab from Model Serving metrics. | | disableDistributedWorkloads | false | Disables Distributed Workload Metrics from the dashboard. | | disableModelRegistry | false | Disables Model Registry from the dashboard. | +| disableModelRegistrySecureDB | true | Disables Model Registry Secure DB from the dashboard. | | disableServingRuntimeParams | false | Disables Serving Runtime params from the dashboard. | | disableStorageClasses | false | Disables storage classes settings nav item from the dashboard. | | disableNIMModelServing | true | Disables components of NIM Model UI from the dashboard. | diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index d3724fa78b..eaa8b4919b 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -26,6 +26,7 @@ export type MockDashboardConfigType = { disableTrustyBiasMetrics?: boolean; disableDistributedWorkloads?: boolean; disableModelRegistry?: boolean; + disableModelRegistrySecureDB?: boolean; disableServingRuntimeParams?: boolean; disableConnectionTypes?: boolean; disableStorageClasses?: boolean; @@ -59,6 +60,7 @@ export const mockDashboardConfig = ({ disableTrustyBiasMetrics = false, disableDistributedWorkloads = false, disableModelRegistry = false, + disableModelRegistrySecureDB = true, disableServingRuntimeParams = false, disableConnectionTypes = true, disableStorageClasses = false, @@ -169,6 +171,7 @@ export const mockDashboardConfig = ({ disableHardwareProfiles, disableDistributedWorkloads, disableModelRegistry, + disableModelRegistrySecureDB, disableServingRuntimeParams, disableConnectionTypes, disableStorageClasses, diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts index 42e4df9c24..39b4809034 100644 --- a/frontend/src/concepts/areas/const.ts +++ b/frontend/src/concepts/areas/const.ts @@ -28,6 +28,7 @@ export const allFeatureFlags: string[] = Object.keys({ disableHardwareProfiles: false, disableDistributedWorkloads: false, disableModelRegistry: false, + disableModelRegistrySecureDB: true, disableServingRuntimeParams: false, disableConnectionTypes: false, disableStorageClasses: false, @@ -129,6 +130,10 @@ export const SupportedAreasStateMap: SupportedAreasState = { featureFlags: ['disableServingRuntimeParams'], reliantAreas: [SupportedArea.K_SERVE, SupportedArea.MODEL_SERVING], }, + [SupportedArea.MODEL_REGISTRY_SECURE_DB]: { + featureFlags: ['disableModelRegistrySecureDB'], + reliantAreas: [SupportedArea.MODEL_REGISTRY], + }, [SupportedArea.NIM_MODEL]: { featureFlags: ['disableNIMModelServing'], reliantAreas: [SupportedArea.K_SERVE], diff --git a/frontend/src/concepts/areas/types.ts b/frontend/src/concepts/areas/types.ts index 73710595a4..d8c2d2bf17 100644 --- a/frontend/src/concepts/areas/types.ts +++ b/frontend/src/concepts/areas/types.ts @@ -57,15 +57,14 @@ export enum SupportedArea { PERFORMANCE_METRICS = 'performance-metrics', TRUSTY_AI = 'trusty-ai', NIM_MODEL = 'nim-model', + SERVING_RUNTIME_PARAMS = 'serving-runtime-params', /* Distributed Workloads areas */ DISTRIBUTED_WORKLOADS = 'distributed-workloads', /* Model Registry areas */ MODEL_REGISTRY = 'model-registry', - - /* Alter parameters areas */ - SERVING_RUNTIME_PARAMS = 'serving-runtime-params', + MODEL_REGISTRY_SECURE_DB = 'model-registry-secure-db', } /** Components deployed by the Operator. Part of the DSC Status. */ diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 4b89950116..e6e4cd51a8 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -1192,6 +1192,7 @@ export type DashboardCommonConfig = { disableHardwareProfiles: boolean; disableDistributedWorkloads: boolean; disableModelRegistry: boolean; + disableModelRegistrySecureDB: boolean; disableServingRuntimeParams: boolean; disableConnectionTypes: boolean; disableStorageClasses: boolean; diff --git a/frontend/src/pages/modelRegistrySettings/CreateMRSecureDBSection.tsx b/frontend/src/pages/modelRegistrySettings/CreateMRSecureDBSection.tsx new file mode 100644 index 0000000000..2ee7aafa8f --- /dev/null +++ b/frontend/src/pages/modelRegistrySettings/CreateMRSecureDBSection.tsx @@ -0,0 +1,260 @@ +import React, { useState } from 'react'; +import { FormGroup, Radio, Alert, MenuItem, MenuGroup } from '@patternfly/react-core'; +import SearchSelector from '~/components/searchSelector/SearchSelector'; +import { translateDisplayNameForK8s } from '~/concepts/k8s/utils'; +import { RecursivePartial } from '~/typeHelpers'; +import { PemFileUpload } from './PemFileUpload'; + +export enum SecureDBRType { + CLUSTER_WIDE = 'cluster-wide', + OPENSHIFT = 'openshift', + EXISTING = 'existing', + NEW = 'new', +} + +export interface SecureDBInfo { + type: SecureDBRType; + configMap: string; + key: string; + certificate: string; + nameSpace: string; + isValid: boolean; +} + +interface CreateMRSecureDBSectionProps { + secureDBInfo: SecureDBInfo; + modelRegistryNamespace: string; + nameDesc: { name: string }; + existingCertKeys: string[]; + existingCertConfigMaps: string[]; + existingCertSecrets: string[]; + setSecureDBInfo: (info: SecureDBInfo) => void; +} + +export const CreateMRSecureDBSection: React.FC = ({ + secureDBInfo, + modelRegistryNamespace, + nameDesc, + existingCertKeys, + existingCertConfigMaps, + existingCertSecrets, + setSecureDBInfo, +}) => { + const [searchValue, setSearchValue] = useState(''); + + const hasContent = (value: string): boolean => !!value.trim().length; + + const isValid = (info?: RecursivePartial) => { + const fullInfo: SecureDBInfo = { ...secureDBInfo, ...info }; + if ([SecureDBRType.CLUSTER_WIDE, SecureDBRType.OPENSHIFT].includes(fullInfo.type)) { + return true; + } + if (fullInfo.type === SecureDBRType.EXISTING) { + return hasContent(fullInfo.configMap) && hasContent(fullInfo.key); + } + if (fullInfo.type === SecureDBRType.NEW) { + return hasContent(fullInfo.certificate); + } + return false; + }; + + const handleSecureDBTypeChange = (type: SecureDBRType) => { + const newInfo = { + type, + nameSpace: '', + key: '', + configMap: '', + certificate: '', + }; + setSecureDBInfo({ + ...newInfo, + isValid: isValid(newInfo), + }); + }; + + const getFilteredExistingCAResources = () => ( + <> + + {existingCertConfigMaps + .filter((configMap) => configMap.toLowerCase().includes(searchValue.toLowerCase())) + .map((configMap, index) => ( + { + setSearchValue(''); + const newInfo = { + ...secureDBInfo, + configMap, + key: '', + }; + setSecureDBInfo({ + ...newInfo, + isValid: isValid(newInfo), + }); + }} + > + {configMap} + + ))} + + + {existingCertSecrets + .filter((secret) => secret.toLowerCase().includes(searchValue.toLowerCase())) + .map((secret, index) => ( + { + setSearchValue(''); + const newInfo = { + ...secureDBInfo, + configMap: secret, + key: '', + }; + setSecureDBInfo({ + ...newInfo, + isValid: isValid(newInfo), + }); + }} + > + {secret} + + ))} + + + ); + + return ( + <> + handleSecureDBTypeChange(SecureDBRType.CLUSTER_WIDE)} + label="Use cluster-wide CA bundle" + description={ + <> + Use the ca-bundle.crt bundle in the{' '} + odh-trusted-ca-bundle ConfigMap. + + } + id="cluster-wide-ca" + /> + handleSecureDBTypeChange(SecureDBRType.OPENSHIFT)} + label="Use OpenShift AI CA bundle" + description={ + <> + Use the odh-ca-bundle.crt bundle in the{' '} + odh-trusted-ca-bundle ConfigMap. + + } + id="openshift-ca" + /> + handleSecureDBTypeChange(SecureDBRType.EXISTING)} + label="Choose from existing certificates" + description={ + <> + You can select the key of any ConfigMap or Secret in the{' '} + {modelRegistryNamespace} namespace. + + } + id="existing-ca" + /> + {secureDBInfo.type === SecureDBRType.EXISTING && ( + <> + + setSearchValue(newValue)} + onSearchClear={() => setSearchValue('')} + searchValue={searchValue} + toggleText={secureDBInfo.configMap || 'Select a ConfigMap or a Secret'} + > + {getFilteredExistingCAResources()} + + + + setSearchValue(newValue)} + onSearchClear={() => setSearchValue('')} + searchValue={searchValue} + toggleText={secureDBInfo.key || 'Select a key'} + > + {existingCertKeys + .filter((item) => item.toLowerCase().includes(searchValue.toLowerCase())) + .map((item, index) => ( + { + setSearchValue(''); + const newInfo = { + ...secureDBInfo, + key: item, + }; + setSecureDBInfo({ ...newInfo, isValid: isValid(newInfo) }); + }} + > + {item} + + ))} + + + + )} + handleSecureDBTypeChange(SecureDBRType.NEW)} + label="Upload new certificate" + id="new-ca" + /> + {secureDBInfo.type === SecureDBRType.NEW && ( + <> + + Uploading a certificate below creates the{' '} + {translateDisplayNameForK8s(nameDesc.name)}-db-credential ConfigMap + with the ca.crt key. If you'd like to upload the certificate as a + Secret instead, see the documentation for more details. + + + { + const newInfo = { + ...secureDBInfo, + certificate: value, + }; + setSecureDBInfo({ ...newInfo, isValid: isValid(newInfo) }); + }} + /> + + + )} + + ); +}; diff --git a/frontend/src/pages/modelRegistrySettings/CreateModal.tsx b/frontend/src/pages/modelRegistrySettings/CreateModal.tsx index 2c4687e404..6017028bb5 100644 --- a/frontend/src/pages/modelRegistrySettings/CreateModal.tsx +++ b/frontend/src/pages/modelRegistrySettings/CreateModal.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Button, + Checkbox, Form, FormGroup, HelperText, @@ -18,6 +19,8 @@ import NameDescriptionField from '~/concepts/k8s/NameDescriptionField'; import { NameDescType } from '~/pages/projects/types'; import FormSection from '~/components/pf-overrides/FormSection'; import { AreaContext } from '~/concepts/areas/AreaContext'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { CreateMRSecureDBSection, SecureDBInfo, SecureDBRType } from './CreateMRSecureDBSection'; type CreateModalProps = { onClose: () => void; @@ -37,6 +40,15 @@ const CreateModal: React.FC = ({ onClose, refresh }) => { const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); const [database, setDatabase] = React.useState(''); + const [addSecureDB, setAddSecureDB] = React.useState(false); + const [secureDBInfo, setSecureDBInfo] = React.useState({ + type: SecureDBRType.CLUSTER_WIDE, + nameSpace: '', + configMap: '', + certificate: '', + key: '', + isValid: true, + }); const [isHostTouched, setIsHostTouched] = React.useState(false); const [isPortTouched, setIsPortTouched] = React.useState(false); const [isUsernameTouched, setIsUsernameTouched] = React.useState(false); @@ -44,6 +56,18 @@ const CreateModal: React.FC = ({ onClose, refresh }) => { const [isDatabaseTouched, setIsDatabaseTouched] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false); const { dscStatus } = React.useContext(AreaContext); + const secureDbEnabled = useIsAreaAvailable(SupportedArea.MODEL_REGISTRY_SECURE_DB).status; + + const modelRegistryNamespace = dscStatus?.components?.modelregistry?.registriesNamespace || ''; + + // the 3 following consts are temporary hard-coded values to be replaced as part of RHOAIENG-15899 + const existingCertConfigMaps = [ + 'config-service-cabundle', + 'odh-trusted-ca-bundle', + 'foo-ca-bundle', + ]; + const existingCertSecrets = ['builder-dockercfg-b7gdr', 'builder-token-hwsps', 'foo-secret']; + const existingCertKeys = ['service-ca.crt', 'foo-ca.crt']; const onBeforeClose = () => { setIsSubmitting(false); @@ -120,7 +144,8 @@ const CreateModal: React.FC = ({ onClose, refresh }) => { hasContent(password) && hasContent(port) && hasContent(username) && - hasContent(database); + hasContent(database) && + (!addSecureDB || secureDBInfo.isValid); return ( = ({ onClose, refresh }) => { )} + {!secureDbEnabled && ( + <> + + setAddSecureDB(value)} + id="add-secure-db" + name="add-secure-db" + /> + + {addSecureDB && ( + + )} + + )} diff --git a/frontend/src/pages/modelRegistrySettings/PemFileUpload.tsx b/frontend/src/pages/modelRegistrySettings/PemFileUpload.tsx new file mode 100644 index 0000000000..801b9690df --- /dev/null +++ b/frontend/src/pages/modelRegistrySettings/PemFileUpload.tsx @@ -0,0 +1,80 @@ +import { + DropEvent, + FileUpload, + FormHelperText, + HelperText, + HelperTextItem, +} from '@patternfly/react-core'; +import React from 'react'; + +export const PemFileUpload: React.FC<{ onChange: (value: string) => void }> = ({ onChange }) => { + const [value, setValue] = React.useState(''); + const [filename, setFilename] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + const [isRejected, setIsRejected] = React.useState(false); + + const handleFileInputChange = (_event: DropEvent, file: File) => { + setFilename(file.name); + }; + + const handleTextChange = (_event: React.ChangeEvent, val: string) => { + setValue(val); + }; + + const handleDataChange = (_event: DropEvent, val: string) => { + onChange(val); + setValue(val); + }; + + const handleClear = () => { + setFilename(''); + setValue(''); + setIsRejected(false); + }; + + const handleFileRejected = () => { + setIsRejected(true); + }; + + const handleFileReadStarted = () => { + setIsLoading(true); + }; + + const handleFileReadFinished = () => { + setIsLoading(false); + }; + + return ( + <> + + + + + {isRejected ? 'Must be a PEM file' : 'Upload a PEM file'} + + + + + ); +}; diff --git a/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml b/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml index ed4dd72dda..75d3a354e2 100644 --- a/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml +++ b/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml @@ -73,6 +73,8 @@ spec: type: boolean disableModelRegistry: type: boolean + disableModelRegistrySecureDB: + type: boolean disableServingRuntimeParams: type: boolean disableStorageClasses: