diff --git a/backend/src/routes/api/modelRegistries/index.ts b/backend/src/routes/api/modelRegistries/index.ts new file mode 100644 index 0000000000..4f7891e16c --- /dev/null +++ b/backend/src/routes/api/modelRegistries/index.ts @@ -0,0 +1,180 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import { PatchUtils } from '@kubernetes/client-node'; +import { secureAdminRoute } from '../../../utils/route-security'; +import { KubeFastifyInstance, ModelRegistryKind, RecursivePartial } from '../../../types'; +import { MODEL_REGISTRY_NAMESPACE } from '../../../utils/constants'; + +export default async (fastify: KubeFastifyInstance): Promise => { + fastify.get( + '/', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ Querystring: { labelSelector: string } }>, + reply: FastifyReply, + ) => { + const { labelSelector } = request.query; + try { + const response = await fastify.kube.customObjectsApi.listNamespacedCustomObject( + 'modelregistry.opendatahub.io', + 'v1alpha1', + MODEL_REGISTRY_NAMESPACE, + 'modelregistries', + undefined, + undefined, + undefined, + labelSelector, + ); + return response.body; + } catch (e) { + fastify.log.error( + `ModelRegistries could not be listed, ${e.response?.body?.message || e.message}`, + ); + reply.send(e); + } + }, + ), + ); + + fastify.post( + '/', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ + Querystring: { dryRun?: string }; + Body: ModelRegistryKind; + }>, + reply: FastifyReply, + ) => { + const { dryRun } = request.query; + const modelRegistry = request.body; + try { + const response = await fastify.kube.customObjectsApi.createNamespacedCustomObject( + 'modelregistry.opendatahub.io', + 'v1alpha1', + MODEL_REGISTRY_NAMESPACE, + 'modelregistries', + request.body, + undefined, + dryRun, + ); + return response.body; + } catch (e) { + fastify.log.error( + `ModelRegistry ${modelRegistry.metadata.name} could not be created, ${ + e.response?.body?.message || e.message + }`, + ); + reply.send(e); + } + }, + ), + ); + + fastify.get( + '/:modelRegistryName', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ Params: { modelRegistryName: string } }>, + reply: FastifyReply, + ) => { + const { modelRegistryName } = request.params; + try { + const response = await fastify.kube.customObjectsApi.getNamespacedCustomObject( + 'modelregistry.opendatahub.io', + 'v1alpha1', + MODEL_REGISTRY_NAMESPACE, + 'modelregistries', + modelRegistryName, + ); + return response.body; + } catch (e) { + fastify.log.error( + `ModelRegistry ${modelRegistryName} could not be read, ${ + e.response?.body?.message || e.message + }`, + ); + reply.send(e); + } + }, + ), + ); + + fastify.patch( + '/:modelRegistryName', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ + Querystring: { dryRun?: string }; + Params: { modelRegistryName: string }; + Body: RecursivePartial; + }>, + reply: FastifyReply, + ) => { + const options = { + headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_PATCH }, + }; + const { dryRun } = request.query; + const { modelRegistryName } = request.params; + try { + const response = await fastify.kube.customObjectsApi.patchNamespacedCustomObject( + 'modelregistry.opendatahub.io', + 'v1alpha1', + MODEL_REGISTRY_NAMESPACE, + 'modelregistries', + modelRegistryName, + request.body, + dryRun, + undefined, + undefined, + options, + ); + return response.body; + } catch (e) { + fastify.log.error( + `ModelRegistry ${modelRegistryName} could not be modified, ${ + e.response?.body?.message || e.message + }`, + ); + reply.send(e); + } + }, + ), + ); + + fastify.delete( + '/:modelRegistryName', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ + Querystring: { dryRun?: string }; + Params: { modelRegistryName: string }; + }>, + reply: FastifyReply, + ) => { + const { dryRun } = request.query; + const { modelRegistryName } = request.params; + try { + const response = await fastify.kube.customObjectsApi.deleteNamespacedCustomObject( + 'modelregistry.opendatahub.io', + 'v1alpha1', + MODEL_REGISTRY_NAMESPACE, + 'modelregistries', + modelRegistryName, + undefined, + undefined, + undefined, + dryRun, + ); + return response.body; + } catch (e) { + fastify.log.error( + `ModelRegistry ${modelRegistryName} could not be deleted, ${ + e.response?.body?.message || e.message + }`, + ); + reply.send(e); + } + }, + ), + ); +}; diff --git a/backend/src/routes/api/service/modelregistry/index.ts b/backend/src/routes/api/service/modelregistry/index.ts index a236548397..b214d3055b 100644 --- a/backend/src/routes/api/service/modelregistry/index.ts +++ b/backend/src/routes/api/service/modelregistry/index.ts @@ -1,6 +1,6 @@ import httpProxy from '@fastify/http-proxy'; import { KubeFastifyInstance } from '../../../../types'; -import { DEV_MODE } from '../../../../utils/constants'; +import { DEV_MODE, MODEL_REGISTRY_NAMESPACE } from '../../../../utils/constants'; import { getParam, setParam } from '../../../../utils/proxy'; export default async (fastify: KubeFastifyInstance): Promise => { @@ -20,7 +20,7 @@ export default async (fastify: KubeFastifyInstance): Promise => { // kubectl port-forward -n svc/ : `http://${process.env.MODEL_REGISTRY_SERVICE_HOST}:${process.env.MODEL_REGISTRY_SERVICE_PORT}` : // Construct service URL - `http://${name}.odh-model-registries.svc.cluster.local:8080`; + `http://${name}.${MODEL_REGISTRY_NAMESPACE}.svc.cluster.local:8080`; // assign the `upstream` param so we can dynamically set the upstream URL for http-proxy setParam(request, 'upstream', upstream); diff --git a/backend/src/types.ts b/backend/src/types.ts index b487889eeb..927f51cbf9 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1029,6 +1029,36 @@ export type TrustyAIKind = K8sResourceCommon & { }; export type ModelRegistryKind = K8sResourceCommon & { + metadata: { + name: string; + namespace: string; + }; + spec: { + grpc: { + port: number; + }; + rest: { + port: number; + serviceRoute: string; + }; + mysql?: { + database: string; + host: string; + port?: number; + }; + postgres: { + database: string; + host?: string; + passwordSecret?: { + key: string; + name: string; + }; + port: number; + skipDBCreation?: boolean; + sslMode?: string; + username?: string; + }; + }; status?: { conditions?: K8sCondition[]; }; diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 4d39f675f1..a71e41e991 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -141,3 +141,5 @@ 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/frontend/src/services/modelRegistryService.ts b/frontend/src/services/modelRegistryService.ts new file mode 100644 index 0000000000..608412d818 --- /dev/null +++ b/frontend/src/services/modelRegistryService.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { ModelRegistryKind } from '~/k8sTypes'; +import { RecursivePartial } from '~/typeHelpers'; + +export const listModelRegistriesBackend = ( + labelSelector?: string, +): Promise => { + const url = '/api/modelRegistries'; + return axios + .get(url, { params: { labelSelector } }) + .then((response) => response.data.items) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const createModelRegistryBackend = (data: ModelRegistryKind): Promise => { + const url = '/api/modelRegistries'; + return axios + .post(url, data) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const getModelRegistryBackend = (modelRegistryName: string): Promise => { + const url = `/api/modelRegistries/${modelRegistryName}`; + return axios + .get(url) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const updateModelRegistryBackend = ( + modelRegistryName: string, + patch: RecursivePartial, +): Promise => { + const url = `/api/modelRegistries/${modelRegistryName}`; + return axios + .patch(url, patch) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const deleteModelRegistryBackend = ( + modelRegistryName: string, +): Promise => { + const url = `/api/modelRegistries/${modelRegistryName}`; + return axios + .delete(url) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; diff --git a/manifests/base/cluster-role.yaml b/manifests/base/cluster-role.yaml index c816375095..510bab4f33 100644 --- a/manifests/base/cluster-role.yaml +++ b/manifests/base/cluster-role.yaml @@ -180,3 +180,15 @@ rules: - get resources: - dscinitializations + - apiGroups: + - modelregistry.opendatahub.io + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + resources: + - modelregistries