diff --git a/.env.development b/.env.development index 98bc1009ea..763f00540c 100644 --- a/.env.development +++ b/.env.development @@ -19,3 +19,4 @@ TRUSTYAI_TAIS_SERVICE_PORT=9443 MODEL_REGISTRY_NAME=modelregistry-sample MODEL_REGISTRY_SERVICE_HOST=localhost MODEL_REGISTRY_SERVICE_PORT=8085 +MODEL_REGISTRY_NAMESPACE=odh-model-registries \ No newline at end of file diff --git a/Makefile b/Makefile index 3c58e0a770..c16e1756c4 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ ifdef NAMESPACE 'oc port-forward -n ${NAMESPACE} svc/ds-pipeline-md-${DSPA_NAME} ${METADATA_ENVOY_SERVICE_PORT}:8443' \ 'oc port-forward -n ${NAMESPACE} svc/ds-pipeline-${DSPA_NAME} ${DS_PIPELINE_DSPA_SERVICE_PORT}:8443' \ 'oc port-forward -n ${NAMESPACE} svc/${TRUSTYAI_NAME}-tls ${TRUSTYAI_TAIS_SERVICE_PORT}:443' \ - 'oc port-forward -n odh-model-registries svc/${MODEL_REGISTRY_NAME} ${MODEL_REGISTRY_SERVICE_PORT}:8080' + 'oc port-forward -n ${MODEL_REGISTRY_NAMESPACE} svc/${MODEL_REGISTRY_NAME} ${MODEL_REGISTRY_SERVICE_PORT}:8080' else $(error Missing NAMESPACE variable) endif diff --git a/backend/src/routes/api/modelRegistries/index.ts b/backend/src/routes/api/modelRegistries/index.ts index 50ed8bd732..2345582674 100644 --- a/backend/src/routes/api/modelRegistries/index.ts +++ b/backend/src/routes/api/modelRegistries/index.ts @@ -6,6 +6,7 @@ import { deleteModelRegistryAndSecret, getDatabasePassword, getModelRegistry, + getModelRegistryNamespace, listModelRegistries, patchModelRegistryAndUpdatePassword, } from './modelRegistryUtils'; @@ -17,6 +18,8 @@ type ModelRegistryAndDBPassword = { // Lists ModelRegistries directly (does not look up passwords from associated Secrets, you must make a direct request to '/:modelRegistryName' for that) export default async (fastify: KubeFastifyInstance): Promise => { + const modelRegistryNamespace = await getModelRegistryNamespace(fastify); + fastify.get( '/', secureAdminRoute(fastify)( @@ -26,7 +29,7 @@ export default async (fastify: KubeFastifyInstance): Promise => { ) => { const { labelSelector } = request.query; try { - return listModelRegistries(fastify, labelSelector); + return listModelRegistries(fastify, modelRegistryNamespace, labelSelector); } catch (e) { fastify.log.error( `ModelRegistries could not be listed, ${e.response?.body?.message || e.message}`, @@ -52,7 +55,13 @@ export default async (fastify: KubeFastifyInstance): Promise => { const { dryRun } = request.query; const { modelRegistry, databasePassword } = request.body; try { - return createModelRegistryAndSecret(fastify, modelRegistry, databasePassword, !!dryRun); + return createModelRegistryAndSecret( + fastify, + modelRegistry, + modelRegistryNamespace, + databasePassword, + !!dryRun, + ); } catch (e) { fastify.log.error( `ModelRegistry ${modelRegistry.metadata.name} could not be created, ${ @@ -75,8 +84,16 @@ export default async (fastify: KubeFastifyInstance): Promise => { ) => { const { modelRegistryName } = request.params; try { - const modelRegistry = await getModelRegistry(fastify, modelRegistryName); - const databasePassword = await getDatabasePassword(fastify, modelRegistry); + const modelRegistry = await getModelRegistry( + fastify, + modelRegistryName, + modelRegistryNamespace, + ); + const databasePassword = await getDatabasePassword( + fastify, + modelRegistry, + modelRegistryNamespace, + ); return { modelRegistry, databasePassword } satisfies ModelRegistryAndDBPassword; } catch (e) { fastify.log.error( @@ -110,6 +127,7 @@ export default async (fastify: KubeFastifyInstance): Promise => { const modelRegistry = await patchModelRegistryAndUpdatePassword( fastify, modelRegistryName, + modelRegistryNamespace, patchBody, databasePassword, !!dryRun, @@ -141,7 +159,12 @@ export default async (fastify: KubeFastifyInstance): Promise => { const { dryRun } = request.query; const { modelRegistryName } = request.params; try { - deleteModelRegistryAndSecret(fastify, modelRegistryName, !!dryRun); + deleteModelRegistryAndSecret( + fastify, + modelRegistryName, + modelRegistryNamespace, + !!dryRun, + ); } catch (e) { fastify.log.error( `ModelRegistry ${modelRegistryName} could not be deleted, ${ diff --git a/backend/src/routes/api/modelRegistries/modelRegistryUtils.ts b/backend/src/routes/api/modelRegistries/modelRegistryUtils.ts index 967aa5c4f4..d4a2e35a4f 100644 --- a/backend/src/routes/api/modelRegistries/modelRegistryUtils.ts +++ b/backend/src/routes/api/modelRegistries/modelRegistryUtils.ts @@ -1,11 +1,16 @@ -import { MODEL_REGISTRY_NAMESPACE } from '../../../utils/constants'; import { KubeFastifyInstance, ModelRegistryKind, RecursivePartial } from '../../../types'; import { PatchUtils, V1Secret, V1Status } from '@kubernetes/client-node'; +import { getClusterStatus } from '../../../utils/dsc'; const MODEL_REGISTRY_API_GROUP = 'modelregistry.opendatahub.io'; const MODEL_REGISTRY_API_VERSION = 'v1alpha1'; const MODEL_REGISTRY_PLURAL = 'modelregistries'; +export const getModelRegistryNamespace = async (fastify: KubeFastifyInstance): Promise => { + const modelRegistryNamespace = await getClusterStatus(fastify); + return modelRegistryNamespace.components.modelregistry.registriesNamespace; +}; + const base64encode = (value?: string): string => { // This usage of toString is fine for encoding // eslint-disable-next-line no-restricted-properties @@ -28,12 +33,13 @@ const getDatabaseSpec = ( export const listModelRegistries = async ( fastify: KubeFastifyInstance, + modelRegistryNamespace: string, labelSelector?: string, ): Promise<{ items: ModelRegistryKind[] }> => { const response = await (fastify.kube.customObjectsApi.listNamespacedCustomObject( MODEL_REGISTRY_API_GROUP, MODEL_REGISTRY_API_VERSION, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, MODEL_REGISTRY_PLURAL, undefined, undefined, @@ -47,6 +53,7 @@ export const listModelRegistries = async ( const createDatabasePasswordSecret = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, databasePassword?: string, dryRun = false, ): Promise => { @@ -59,7 +66,7 @@ const createDatabasePasswordSecret = async ( apiVersion: 'v1', metadata: { generateName: `${modelRegistry.metadata.name}-db-`, - namespace: MODEL_REGISTRY_NAMESPACE, + namespace: modelRegistryNamespace, annotations: { 'template.openshift.io/expose-database_name': "{.data['database-name']}", 'template.openshift.io/expose-username': "{.data['database-user']}", @@ -74,7 +81,7 @@ const createDatabasePasswordSecret = async ( type: 'Opaque', }; const response = await fastify.kube.coreV1Api.createNamespacedSecret( - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, secret, undefined, dryRun ? 'All' : undefined, @@ -85,6 +92,7 @@ const createDatabasePasswordSecret = async ( const createModelRegistry = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, secret?: V1Secret, dryRun = false, ): Promise => { @@ -108,7 +116,7 @@ const createModelRegistry = async ( const response = await (fastify.kube.customObjectsApi.createNamespacedCustomObject( MODEL_REGISTRY_API_GROUP, MODEL_REGISTRY_API_VERSION, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, MODEL_REGISTRY_PLURAL, modelRegistryWithSecretRef, undefined, @@ -121,6 +129,7 @@ const createModelRegistry = async ( export const createModelRegistryAndSecret = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, databasePassword?: string, dryRunOnly = false, ): Promise => { @@ -128,9 +137,15 @@ export const createModelRegistryAndSecret = async ( const dbSpec = getDatabaseSpec(modelRegistry); const newSecret = databasePassword && dbSpec - ? await createDatabasePasswordSecret(fastify, modelRegistry, databasePassword, dryRun) + ? await createDatabasePasswordSecret( + fastify, + modelRegistry, + modelRegistryNamespace, + databasePassword, + dryRun, + ) : undefined; - return createModelRegistry(fastify, modelRegistry, newSecret, dryRun); + return createModelRegistry(fastify, modelRegistry, modelRegistryNamespace, newSecret, dryRun); }; // Dry run both MR and Secret creation first so there are no changes if either would fail const dryRunResult = await createBoth(true); @@ -143,11 +158,12 @@ export const createModelRegistryAndSecret = async ( export const getModelRegistry = async ( fastify: KubeFastifyInstance, modelRegistryName: string, + modelRegistryNamespace: string, ): Promise => { const response = await (fastify.kube.customObjectsApi.getNamespacedCustomObject( MODEL_REGISTRY_API_GROUP, MODEL_REGISTRY_API_VERSION, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, MODEL_REGISTRY_PLURAL, modelRegistryName, // getNamespacedCustomObject doesn't support TS generics and returns body as `object`, so we assert its real type @@ -158,6 +174,7 @@ export const getModelRegistry = async ( const getDatabasePasswordSecret = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, ): Promise<{ secret?: V1Secret; passwordDataKey?: string }> => { const secretRef = getDatabaseSpec(modelRegistry)?.passwordSecret; if (!secretRef) { @@ -165,7 +182,7 @@ const getDatabasePasswordSecret = async ( } const response = await fastify.kube.coreV1Api.readNamespacedSecret( secretRef.name, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, ); return { secret: response.body, passwordDataKey: secretRef.key }; }; @@ -173,19 +190,27 @@ const getDatabasePasswordSecret = async ( export const getDatabasePassword = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, ): Promise => { - const { secret, passwordDataKey } = await getDatabasePasswordSecret(fastify, modelRegistry); + const { secret, passwordDataKey } = await getDatabasePasswordSecret( + fastify, + modelRegistry, + modelRegistryNamespace, + ); return base64decode(secret.data[passwordDataKey]); }; const deleteDatabasePasswordSecret = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, dryRun = false, ): Promise => { let existingSecret: V1Secret | undefined; try { - existingSecret = (await getDatabasePasswordSecret(fastify, modelRegistry)).secret; + existingSecret = ( + await getDatabasePasswordSecret(fastify, modelRegistry, modelRegistryNamespace) + ).secret; } catch (e) { // If the secret doesn't exist, don't try to delete and cause a 404 error, just do nothing. // The user may have deleted their own secret and we don't want to block deleting the model registry. @@ -193,7 +218,7 @@ const deleteDatabasePasswordSecret = async ( } const response = await fastify.kube.coreV1Api.deleteNamespacedSecret( existingSecret.metadata.name, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, undefined, dryRun ? 'All' : undefined, ); @@ -203,13 +228,14 @@ const deleteDatabasePasswordSecret = async ( const patchModelRegistry = async ( fastify: KubeFastifyInstance, modelRegistryName: string, + modelRegistryNamespace: string, patchBody: RecursivePartial, dryRun = false, ): Promise => { const response = await (fastify.kube.customObjectsApi.patchNamespacedCustomObject( MODEL_REGISTRY_API_GROUP, MODEL_REGISTRY_API_VERSION, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, MODEL_REGISTRY_PLURAL, modelRegistryName, patchBody, @@ -225,17 +251,22 @@ const patchModelRegistry = async ( const updateDatabasePassword = async ( fastify: KubeFastifyInstance, modelRegistry: ModelRegistryKind, + modelRegistryNamespace: string, databasePassword?: string, dryRun = false, ): Promise => { - const { secret, passwordDataKey } = await getDatabasePasswordSecret(fastify, modelRegistry); + const { secret, passwordDataKey } = await getDatabasePasswordSecret( + fastify, + modelRegistry, + modelRegistryNamespace, + ); if (!secret) { return; } if (databasePassword) { await fastify.kube.coreV1Api.patchNamespacedSecret( secret.metadata.name, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, { data: { ...secret.data, @@ -246,20 +277,33 @@ const updateDatabasePassword = async ( dryRun ? 'All' : undefined, ); } else { - await deleteDatabasePasswordSecret(fastify, modelRegistry); + await deleteDatabasePasswordSecret(fastify, modelRegistry, modelRegistryNamespace); } }; export const patchModelRegistryAndUpdatePassword = async ( fastify: KubeFastifyInstance, modelRegistryName: string, + modelRegistryNamespace: string, patchBody: RecursivePartial, databasePassword?: string, dryRunOnly = false, ): Promise => { const patchBoth = async (dryRun = false) => { - const modelRegistry = await patchModelRegistry(fastify, modelRegistryName, patchBody, dryRun); - await updateDatabasePassword(fastify, modelRegistry, databasePassword, dryRun); + const modelRegistry = await patchModelRegistry( + fastify, + modelRegistryName, + modelRegistryNamespace, + patchBody, + dryRun, + ); + await updateDatabasePassword( + fastify, + modelRegistry, + modelRegistryNamespace, + databasePassword, + dryRun, + ); return modelRegistry; }; // Dry run both patches first so there are no changes if either would fail @@ -273,14 +317,15 @@ export const patchModelRegistryAndUpdatePassword = async ( export const deleteModelRegistryAndSecret = async ( fastify: KubeFastifyInstance, modelRegistryName: string, + modelRegistryNamespace: string, dryRunOnly = false, ): Promise => { - const modelRegistry = await getModelRegistry(fastify, modelRegistryName); + const modelRegistry = await getModelRegistry(fastify, modelRegistryName, modelRegistryNamespace); const deleteBoth = async (dryRun = false) => { const response = await fastify.kube.customObjectsApi.deleteNamespacedCustomObject( MODEL_REGISTRY_API_GROUP, MODEL_REGISTRY_API_VERSION, - MODEL_REGISTRY_NAMESPACE, + modelRegistryNamespace, MODEL_REGISTRY_PLURAL, modelRegistryName, undefined, @@ -288,7 +333,7 @@ export const deleteModelRegistryAndSecret = async ( undefined, dryRun ? 'All' : undefined, ); - await deleteDatabasePasswordSecret(fastify, modelRegistry); + await deleteDatabasePasswordSecret(fastify, modelRegistry, modelRegistryNamespace); return response.body; }; // Dry run both deletes first so there are no changes if either would fail diff --git a/backend/src/routes/api/service/modelregistry/index.ts b/backend/src/routes/api/service/modelregistry/index.ts index 6f307b10e9..5f65162848 100644 --- a/backend/src/routes/api/service/modelregistry/index.ts +++ b/backend/src/routes/api/service/modelregistry/index.ts @@ -1,5 +1,5 @@ +import { getModelRegistryNamespace } from '../../../api/modelRegistries/modelRegistryUtils'; import { ServiceAddressAnnotation } from '../../../../types'; -import { MODEL_REGISTRY_NAMESPACE } from '../../../../utils/constants'; import { proxyService } from '../../../../utils/proxy'; export default proxyService( @@ -7,7 +7,7 @@ export default proxyService( { addressAnnotation: ServiceAddressAnnotation.EXTERNAL_REST, internalPort: 8080, - namespace: MODEL_REGISTRY_NAMESPACE, + namespace: getModelRegistryNamespace, }, { // Use port forwarding for local development: diff --git a/backend/src/types.ts b/backend/src/types.ts index 9bcbb0cb31..97099d2ff2 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1004,6 +1004,11 @@ type ComponentNames = | 'workbenches'; export type DataScienceClusterKindStatus = { + components: { + modelregistry: { + registriesNamespace: string + } + } conditions: K8sCondition[]; installedComponents: { [key in ComponentNames]?: boolean }; phase?: string; diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 4c9c5547a7..e935938365 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -142,5 +142,3 @@ export const THANOS_RBAC_PORT = '9092'; export const THANOS_INSTANCE_NAME = 'thanos-querier'; export const THANOS_NAMESPACE = 'openshift-monitoring'; export const LABEL_SELECTOR_DASHBOARD_RESOURCE = `${KnownLabels.DASHBOARD_RESOURCE}=true`; - -export const MODEL_REGISTRY_NAMESPACE = 'odh-model-registries'; diff --git a/backend/src/utils/proxy.ts b/backend/src/utils/proxy.ts index b7b327402f..d2cbecab7d 100644 --- a/backend/src/utils/proxy.ts +++ b/backend/src/utils/proxy.ts @@ -37,7 +37,7 @@ export const proxyService = internalPort: number | string; prefix?: string; suffix?: string; - namespace?: string; + namespace?: string | ((fastify: KubeFastifyInstance) => Promise); }, { constructUrl: (resource: K) => string; @@ -51,6 +51,9 @@ export const proxyService = tls = true, ) => async (fastify: KubeFastifyInstance): Promise => { + if (typeof service.namespace === 'function') { + service.namespace = await service.namespace(fastify); + } fastify.register(httpProxy, { upstream: '', prefix: service.namespace ? ':name' : '/:namespace/:name', diff --git a/backend/src/utils/route-security.ts b/backend/src/utils/route-security.ts index 66c560a78b..8a00f63739 100644 --- a/backend/src/utils/route-security.ts +++ b/backend/src/utils/route-security.ts @@ -4,7 +4,7 @@ import { createCustomError } from './requestUtils'; import { isUserAdmin } from './adminUtils'; import { getNamespaces } from './notebookUtils'; import { logRequestDetails } from './fileUtils'; -import { DEV_MODE, MODEL_REGISTRY_NAMESPACE } from './constants'; +import { DEV_MODE } from './constants'; import { K8sNamespacedResourceCommon, KubeFastifyInstance, @@ -12,6 +12,7 @@ import { NotebookState, OauthFastifyRequest, } from '../types'; +import { getModelRegistryNamespace } from '../routes/api/modelRegistries/modelRegistryUtils'; const testAdmin = async ( fastify: KubeFastifyInstance, @@ -73,7 +74,11 @@ const requestSecurityGuard = async ( const isReadRequest = request.method.toLowerCase() === 'get'; // Check to see if a request was made against one of our namespaces - if (![notebookNamespace, dashboardNamespace, MODEL_REGISTRY_NAMESPACE].includes(namespace)) { + if ( + ![notebookNamespace, dashboardNamespace, await getModelRegistryNamespace(fastify)].includes( + namespace, + ) + ) { // Not a valid namespace -- cannot make direct calls to just any namespace no matter who you are fastify.log.error( `User requested a resource that was not in our namespaces. Namespace: ${namespace}`, diff --git a/frontend/src/__mocks__/mockDscStatus.ts b/frontend/src/__mocks__/mockDscStatus.ts index b589cb3795..a3ad631cc6 100644 --- a/frontend/src/__mocks__/mockDscStatus.ts +++ b/frontend/src/__mocks__/mockDscStatus.ts @@ -12,6 +12,11 @@ export const mockDscStatus = ({ conditions = [], phase = 'Ready', }: MockDscStatus): DataScienceClusterKindStatus => ({ + components: { + modelregistry: { + registriesNamespace: 'odh-model-registries', + }, + }, conditions: [ ...[ { diff --git a/frontend/src/__mocks__/mockModelRegistry.ts b/frontend/src/__mocks__/mockModelRegistry.ts index b0b2abd01a..66cda6238c 100644 --- a/frontend/src/__mocks__/mockModelRegistry.ts +++ b/frontend/src/__mocks__/mockModelRegistry.ts @@ -1,4 +1,3 @@ -import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; import { ModelRegistryKind } from '~/k8sTypes'; type MockModelRegistryType = { @@ -8,7 +7,7 @@ type MockModelRegistryType = { export const mockModelRegistry = ({ name = 'modelregistry-sample', - namespace = MODEL_REGISTRY_DEFAULT_NAMESPACE, + namespace = 'odh-model-registries', }: MockModelRegistryType): ModelRegistryKind => ({ apiVersion: 'modelregistry.opendatahub.io/v1alpha1', kind: 'ModelRegistry', diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts index 912d675f5c..b9e0075edc 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { mockK8sResourceList } from '~/__mocks__'; +import { mockDscStatus, mockK8sResourceList } from '~/__mocks__'; import { mockComponents } from '~/__mocks__/mockComponents'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList'; @@ -72,6 +72,15 @@ const initIntercepts = ({ ], allowed = true, }: HandlersProps) => { + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + 'model-registry-operator': true, + }, + }), + ); + cy.interceptOdh( 'GET /api/config', mockDashboardConfig({ diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionArchive.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionArchive.cy.ts index bd9bf54f53..03610b9aed 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionArchive.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionArchive.cy.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { mockK8sResourceList } from '~/__mocks__'; +import { mockDscStatus, mockK8sResourceList } from '~/__mocks__'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList'; import { ServiceModel } from '~/__tests__/cypress/cypress/utils/models'; @@ -47,6 +47,15 @@ const initIntercepts = ({ mockModelVersion({ id: '3', name: 'model version 3' }), ], }: HandlersProps) => { + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + 'model-registry-operator': true, + }, + }), + ); + cy.interceptOdh( 'GET /api/config', mockDashboardConfig({ diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionDetails.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionDetails.cy.ts index 03165c7151..a0d747537b 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionDetails.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersionDetails.cy.ts @@ -11,6 +11,7 @@ import { mockServingRuntimeK8sResource, mockInferenceServiceK8sResource, mockProjectK8sResource, + mockDscStatus, } from '~/__mocks__'; import { @@ -33,6 +34,16 @@ const initIntercepts = () => { disableModelRegistry: false, }), ); + + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + 'model-registry-operator': true, + }, + }), + ); + cy.interceptOdh('GET /api/components', { query: { installed: 'true' } }, mockComponents()); cy.interceptK8sList( diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts index 78bb89178d..a536f5f371 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { mockK8sResourceList } from '~/__mocks__'; +import { mockDscStatus, mockK8sResourceList } from '~/__mocks__'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { mockModelVersionList } from '~/__mocks__/mockModelVersionList'; import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList'; @@ -47,6 +47,15 @@ const initIntercepts = ({ mockModelVersion({ id: '2', name: 'model version' }), ], }: HandlersProps) => { + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + 'model-registry-operator': true, + }, + }), + ); + cy.interceptOdh( 'GET /api/config', mockDashboardConfig({ diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts index 9495185943..19d02673ef 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { mockK8sResourceList } from '~/__mocks__'; +import { mockDscStatus, mockK8sResourceList } from '~/__mocks__'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList'; import { ServiceModel } from '~/__tests__/cypress/cypress/utils/models'; @@ -58,6 +58,15 @@ const initIntercepts = ({ }), ); + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + 'model-registry-operator': true, + }, + }), + ); + cy.interceptK8sList( ServiceModel, mockK8sResourceList([ diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts index 98bdd9ce41..ad5dbf699a 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts @@ -1,4 +1,4 @@ -import { mockK8sResourceList, mockProjectK8sResource } from '~/__mocks__'; +import { mockDscStatus, mockK8sResourceList, mockProjectK8sResource } from '~/__mocks__'; import { mock200Status } from '~/__mocks__/mockK8sStatus'; import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; import { be } from '~/__tests__/cypress/cypress/utils/should'; @@ -51,6 +51,7 @@ const initIntercepts = ({ isEmpty = false, hasPermission = true }: HandlersProps } else { asProductAdminUser(); } + cy.interceptOdh('GET /api/dsc/status', mockDscStatus({})); cy.interceptK8sList( ModelRegistryModel, mockK8sResourceList([ diff --git a/frontend/src/concepts/modelRegistry/apiHooks/__tests__/useModelRegistryServices.spec.ts b/frontend/src/concepts/modelRegistry/apiHooks/__tests__/useModelRegistryServices.spec.ts index f23dd42879..3ee218d370 100644 --- a/frontend/src/concepts/modelRegistry/apiHooks/__tests__/useModelRegistryServices.spec.ts +++ b/frontend/src/concepts/modelRegistry/apiHooks/__tests__/useModelRegistryServices.spec.ts @@ -93,10 +93,10 @@ describe('useModelRegistryServices', () => { expect(isLoaded).toBe(true); expect(mockGetResource).toHaveBeenCalledTimes(2); expect(mockGetResource).toHaveBeenCalledWith({ - queryOptions: { name: 'service-1', ns: 'odh-model-registries' }, + queryOptions: { name: 'service-1', ns: 'test-namespace' }, }); expect(mockGetResource).toHaveBeenCalledWith({ - queryOptions: { name: 'service-2', ns: 'odh-model-registries' }, + queryOptions: { name: 'service-2', ns: 'test-namespace' }, }); }); diff --git a/frontend/src/concepts/modelRegistry/apiHooks/useModelRegistryServices.ts b/frontend/src/concepts/modelRegistry/apiHooks/useModelRegistryServices.ts index 4d3761aba9..de6f5bf4a9 100644 --- a/frontend/src/concepts/modelRegistry/apiHooks/useModelRegistryServices.ts +++ b/frontend/src/concepts/modelRegistry/apiHooks/useModelRegistryServices.ts @@ -3,13 +3,11 @@ import { k8sGetResource } from '@openshift/dynamic-plugin-sdk-utils'; import useFetchState, { FetchStateCallbackPromise, NotReadyError } from '~/utilities/useFetchState'; import { AccessReviewResourceAttributes, ServiceKind } from '~/k8sTypes'; import { ServiceModel, useAccessReview, useRulesReview, listServices } from '~/api'; -import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; const accessReviewResource: AccessReviewResourceAttributes = { group: 'user.openshift.io', resource: 'services', verb: 'list', - namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, }; const getServiceByName = (name: string, namespace: string): Promise => @@ -37,6 +35,7 @@ const listServicesOrFetchThemByNames = async ( allowList: boolean, accessReviewLoaded: boolean, rulesReviewLoaded: boolean, + namespace: string, serviceNames?: string[], ): Promise => { if (!accessReviewLoaded || !rulesReviewLoaded) { @@ -44,8 +43,8 @@ const listServicesOrFetchThemByNames = async ( } const services = allowList - ? await listServices(MODEL_REGISTRY_DEFAULT_NAMESPACE) - : await fetchServices(serviceNames || [], MODEL_REGISTRY_DEFAULT_NAMESPACE); + ? await listServices(namespace) + : await fetchServices(serviceNames || [], namespace); return services; }; @@ -58,7 +57,7 @@ export type ModelRegistryServicesResult = { }; export const useModelRegistryServices = (namespace: string): ModelRegistryServicesResult => { - const [allowList, accessReviewLoaded] = useAccessReview(accessReviewResource); + const [allowList, accessReviewLoaded] = useAccessReview({ ...accessReviewResource, namespace }); const [rulesReviewStatus, rulesReviewLoaded, refreshRulesReview] = useRulesReview(namespace); const serviceNames = React.useMemo(() => { @@ -79,9 +78,10 @@ export const useModelRegistryServices = (namespace: string): ModelRegistryServic allowList, accessReviewLoaded, rulesReviewLoaded, + namespace, serviceNames, ), - [allowList, accessReviewLoaded, rulesReviewLoaded, serviceNames], + [allowList, accessReviewLoaded, rulesReviewLoaded, serviceNames, namespace], ); const [modelRegistryServices, isLoaded, error] = useFetchState(callback, [], { diff --git a/frontend/src/concepts/modelRegistry/const.ts b/frontend/src/concepts/modelRegistry/const.ts index ced9db3475..111b11ae7f 100644 --- a/frontend/src/concepts/modelRegistry/const.ts +++ b/frontend/src/concepts/modelRegistry/const.ts @@ -1,2 +1 @@ -export const MODEL_REGISTRY_DEFAULT_NAMESPACE = 'odh-model-registries'; // The default namespace for the model registry service, controlled by the operator. export const MODEL_REGISTRY_API_VERSION = 'v1alpha3'; diff --git a/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx b/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx index 7e75eb56ac..565cda2156 100644 --- a/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx +++ b/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ServiceKind } from '~/k8sTypes'; import useModelRegistryEnabled from '~/concepts/modelRegistry/useModelRegistryEnabled'; import { useModelRegistryServices } from '~/concepts/modelRegistry/apiHooks/useModelRegistryServices'; -import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; +import { AreaContext } from '~/concepts/areas/AreaContext'; export type ModelRegistrySelectorContextType = { modelRegistryServicesLoaded: boolean; @@ -42,12 +42,13 @@ export const ModelRegistrySelectorContextProvider: React.FC< const EnabledModelRegistrySelectorContextProvider: React.FC< ModelRegistrySelectorContextProviderProps > = ({ children }) => { + const { dscStatus } = React.useContext(AreaContext); const { modelRegistryServices = [], isLoaded, error, refreshRulesReview, - } = useModelRegistryServices(MODEL_REGISTRY_DEFAULT_NAMESPACE); + } = useModelRegistryServices(dscStatus?.components.modelregistry.registriesNamespace || ''); const [preferredModelRegistry, setPreferredModelRegistry] = React.useState< ServiceKind | undefined >(undefined); diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 190c07e300..229ac3b7b7 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -1355,6 +1355,11 @@ export type K8sResourceListResult> /** We don't need or should ever get the full kind, this is the status section */ export type DataScienceClusterKindStatus = { + components: { + modelregistry: { + registriesNamespace: string; + }; + }; conditions: K8sCondition[]; installedComponents: { [key in StackComponent]?: boolean }; phase?: string; diff --git a/frontend/src/pages/modelRegistrySettings/CreateModal.tsx b/frontend/src/pages/modelRegistrySettings/CreateModal.tsx index a115321dfb..56e0da2872 100644 --- a/frontend/src/pages/modelRegistrySettings/CreateModal.tsx +++ b/frontend/src/pages/modelRegistrySettings/CreateModal.tsx @@ -11,13 +11,13 @@ import { import PasswordInput from '~/components/PasswordInput'; import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; import { ModelRegistryKind } from '~/k8sTypes'; -import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; import { ModelRegistryModel } from '~/api'; import { createModelRegistryBackend } from '~/services/modelRegistrySettingsService'; import { isValidK8sName, translateDisplayNameForK8s } from '~/concepts/k8s/utils'; 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'; type CreateModalProps = { isOpen: boolean; @@ -44,6 +44,7 @@ const CreateModal: React.FC = ({ isOpen, onClose, refresh }) = const [isPasswordTouched, setIsPasswordTouched] = React.useState(false); const [isDatabaseTouched, setIsDatabaseTouched] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false); + const { dscStatus } = React.useContext(AreaContext); const onBeforeClose = () => { setIsSubmitting(false); @@ -75,7 +76,7 @@ const CreateModal: React.FC = ({ isOpen, onClose, refresh }) = kind: 'ModelRegistry', metadata: { name: nameDesc.k8sName || translateDisplayNameForK8s(nameDesc.name), - namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + namespace: dscStatus?.components.modelregistry.registriesNamespace || '', annotations: { 'openshift.io/description': nameDesc.description, 'openshift.io/display-name': nameDesc.name.trim(), diff --git a/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx b/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx index f420262b3f..b1d4f74512 100644 --- a/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx +++ b/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx @@ -15,20 +15,22 @@ import { useGroups } from '~/api'; import RoleBindingPermissions from '~/concepts/roleBinding/RoleBindingPermissions'; import { useContextResourceData } from '~/utilities/useContextResourceData'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; import { SupportedArea } from '~/concepts/areas'; import { RoleBindingPermissionsRoleType } from '~/concepts/roleBinding/types'; import { useModelRegistryNamespaceCR } from '~/concepts/modelRegistry/context/useModelRegistryNamespaceCR'; +import { AreaContext } from '~/concepts/areas/AreaContext'; import useModelRegistryRoleBindings from './useModelRegistryRoleBindings'; import ProjectsSettingsTab from './ProjectsTab/ProjectsSettingsTab'; const ModelRegistriesManagePermissions: React.FC = () => { + const { dscStatus } = React.useContext(AreaContext); + const modelRegistryNamespace = dscStatus?.components.modelregistry.registriesNamespace; const [activeTabKey, setActiveTabKey] = React.useState('users'); const [ownerReference, setOwnerReference] = React.useState(); const [groups] = useGroups(); const roleBindings = useContextResourceData(useModelRegistryRoleBindings()); const { mrName } = useParams(); - const state = useModelRegistryNamespaceCR(MODEL_REGISTRY_DEFAULT_NAMESPACE, mrName || ''); + const state = useModelRegistryNamespaceCR(modelRegistryNamespace || '', mrName || ''); const [modelRegistryCR, crLoaded] = state; const filteredRoleBindings = roleBindings.data.filter( (rb) => rb.metadata.labels?.['app.kubernetes.io/name'] === mrName, @@ -109,7 +111,7 @@ const ModelRegistriesManagePermissions: React.FC = () => { 'app.kubernetes.io/name': mrName || '', component: SupportedArea.MODEL_REGISTRY, }} - projectName={MODEL_REGISTRY_DEFAULT_NAMESPACE} + projectName={modelRegistryNamespace || ''} description={ <> To enable access for all cluster users, add{' '} @@ -148,7 +150,7 @@ const ModelRegistriesManagePermissions: React.FC = () => { 'app.kubernetes.io/name': mrName || '', component: SupportedArea.MODEL_REGISTRY, }} - projectName={MODEL_REGISTRY_DEFAULT_NAMESPACE} + projectName={modelRegistryNamespace || ''} isProjectSubject={activeTabKey === 'projects'} roleBindingPermissionsRB={{ ...roleBindings, data: filteredRoleBindings }} /> diff --git a/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts b/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts index 38d1a06fca..277f623e88 100644 --- a/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts +++ b/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts @@ -1,14 +1,16 @@ import * as React from 'react'; import { listRoleBindings } from '~/api'; -import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; +import { AreaContext } from '~/concepts/areas/AreaContext'; import { KnownLabels, RoleBindingKind } from '~/k8sTypes'; import useFetchState, { FetchState } from '~/utilities/useFetchState'; const useModelRegistryRoleBindings = (): FetchState => { + const { dscStatus } = React.useContext(AreaContext); + const getRoleBindings = React.useCallback( () => listRoleBindings( - MODEL_REGISTRY_DEFAULT_NAMESPACE, + dscStatus?.components.modelregistry.registriesNamespace, KnownLabels.LABEL_SELECTOR_MODEL_REGISTRY, ).catch((e) => { if (e.statusObject?.code === 404) { @@ -16,7 +18,7 @@ const useModelRegistryRoleBindings = (): FetchState => { } throw e; }), - [], + [dscStatus?.components.modelregistry.registriesNamespace], ); return useFetchState(getRoleBindings, []);