diff --git a/backend/src/routes/api/connection-types/connectionTypeUtils.ts b/backend/src/routes/api/connection-types/connectionTypeUtils.ts new file mode 100644 index 0000000000..a3d2110ae8 --- /dev/null +++ b/backend/src/routes/api/connection-types/connectionTypeUtils.ts @@ -0,0 +1,190 @@ +import { PatchUtils, V1ConfigMap } from '@kubernetes/client-node'; +import { KnownLabels, KubeFastifyInstance, RecursivePartial } from '../../../types'; +import { getNamespaces } from '../../../utils/notebookUtils'; +import { errorHandler } from '../../../utils'; + +const isConnectionTypeConfigMap = (configMap: V1ConfigMap): boolean => + configMap.metadata.labels && + configMap.metadata.labels[KnownLabels.DASHBOARD_RESOURCE] === 'true' && + configMap.metadata.labels[KnownLabels.CONNECTION_TYPE] === 'true'; + +const isExistingConnectionType = async ( + fastify: KubeFastifyInstance, + name: string, +): Promise => { + const coreV1Api = fastify.kube.coreV1Api; + const { dashboardNamespace } = getNamespaces(fastify); + + const response = await coreV1Api.readNamespacedConfigMap(name, dashboardNamespace); + return isConnectionTypeConfigMap(response.body); +}; + +export const listConnectionTypes = async (fastify: KubeFastifyInstance): Promise => { + const { dashboardNamespace } = getNamespaces(fastify); + const coreV1Api = fastify.kube.coreV1Api; + const connectionTypes: V1ConfigMap[] = []; + + let _continue: string = undefined; + let remainingItemCount = 1; + try { + while (remainingItemCount) { + const response = await coreV1Api.listNamespacedConfigMap( + dashboardNamespace, + undefined, + undefined, + _continue, + undefined, + `${KnownLabels.DASHBOARD_RESOURCE} = true, ${KnownLabels.CONNECTION_TYPE} = true`, + ); + connectionTypes.push(...(response?.body?.items ?? [])); + remainingItemCount = response?.body.metadata?.remainingItemCount; + _continue = response?.body.metadata?._continue; + } + return connectionTypes; + } catch (e) { + fastify.log.error(`Error fetching configmaps for connection types: `, e); + throw new Error(`Failed to list connection types: ${errorHandler(e)}.`); + } +}; + +export const getConnectionType = async ( + fastify: KubeFastifyInstance, + name: string, +): Promise => { + const { dashboardNamespace } = getNamespaces(fastify); + const coreV1Api = fastify.kube.coreV1Api; + try { + const response = await coreV1Api.readNamespacedConfigMap(name, dashboardNamespace); + if (!isConnectionTypeConfigMap(response.body)) { + throw new Error(`object is not a connection type.`); + } + return response.body; + } catch (e) { + fastify.log.error(`Error fetching connection type: `, e); + throw new Error(`Failed to get connection type: ${errorHandler(e)}.`); + } +}; + +export const createConnectionType = async ( + fastify: KubeFastifyInstance, + connectionType: V1ConfigMap, +): Promise<{ success: boolean; error: string }> => { + const coreV1Api = fastify.kube.coreV1Api; + const { dashboardNamespace } = getNamespaces(fastify); + + if (!isConnectionTypeConfigMap(connectionType)) { + const error = 'Unable to add connection type, incorrect labels.'; + fastify.log.error(error); + return { success: false, error }; + } + + try { + await coreV1Api.createNamespacedConfigMap(dashboardNamespace, connectionType); + return { success: true, error: '' }; + } catch (e) { + const error = `Unable to add connection type: ${errorHandler(e)}.`; + fastify.log.error(error); + return { success: false, error }; + } +}; + +export const updateConnectionType = async ( + fastify: KubeFastifyInstance, + name: string, + connectionType: V1ConfigMap, +): Promise<{ success: boolean; error: string }> => { + const coreV1Api = fastify.kube.coreV1Api; + const { dashboardNamespace } = getNamespaces(fastify); + + if (!isConnectionTypeConfigMap(connectionType)) { + const error = 'Unable to add connection type, incorrect labels.'; + fastify.log.error(error); + return { success: false, error }; + } + + try { + const validConnectionType = await isExistingConnectionType(fastify, name); + if (!validConnectionType) { + const error = `Unable to update connection type, object is not a connection type`; + fastify.log.error(error); + return { success: false, error }; + } + + await coreV1Api.replaceNamespacedConfigMap(name, dashboardNamespace, connectionType); + return { success: true, error: '' }; + } catch (e) { + const error = `Unable to update connection type: ${errorHandler(e)}.`; + fastify.log.error(error); + return { success: false, error }; + } +}; + +export const patchConnectionType = async ( + fastify: KubeFastifyInstance, + name: string, + partialConfigMap: RecursivePartial, +): Promise<{ success: boolean; error: string }> => { + const coreV1Api = fastify.kube.coreV1Api; + const { dashboardNamespace } = getNamespaces(fastify); + + if ( + (partialConfigMap.metadata.labels?.[KnownLabels.DASHBOARD_RESOURCE] && + partialConfigMap.metadata.labels[KnownLabels.DASHBOARD_RESOURCE] !== 'true') || + (partialConfigMap.metadata.labels?.[KnownLabels.CONNECTION_TYPE] && + partialConfigMap.metadata.labels[KnownLabels.CONNECTION_TYPE] !== 'true') + ) { + const error = 'Unable to update connection type, incorrect labels.'; + fastify.log.error(error); + return { success: false, error }; + } + + try { + const validConnectionType = await isExistingConnectionType(fastify, name); + if (!validConnectionType) { + const error = `Unable to update connection type, object is not a connection type`; + fastify.log.error(error); + return { success: false, error }; + } + const options = { + headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_PATCH }, + }; + + await coreV1Api.patchNamespacedConfigMap( + name, + dashboardNamespace, + partialConfigMap, + undefined, + undefined, + undefined, + undefined, + options, + ); + return { success: true, error: '' }; + } catch (e) { + const error = `Unable to update connection type: ${errorHandler(e)}.`; + fastify.log.error(error); + return { success: false, error }; + } +}; + +export const deleteConnectionType = async ( + fastify: KubeFastifyInstance, + name: string, +): Promise<{ success: boolean; error: string }> => { + const { dashboardNamespace } = getNamespaces(fastify); + const coreV1Api = fastify.kube.coreV1Api; + try { + const validConnectionType = await isExistingConnectionType(fastify, name); + if (!validConnectionType) { + const error = `Unable to delete connection type, object is not a connection type`; + fastify.log.error(error); + return { success: false, error }; + } + await coreV1Api.deleteNamespacedConfigMap(name, dashboardNamespace); + return { success: true, error: '' }; + } catch (e) { + const error = `Unable to delete connection type: ${errorHandler(e)}.`; + fastify.log.error(error); + return { success: false, error }; + } +}; diff --git a/backend/src/routes/api/connection-types/index.ts b/backend/src/routes/api/connection-types/index.ts new file mode 100644 index 0000000000..9dc4d1e05d --- /dev/null +++ b/backend/src/routes/api/connection-types/index.ts @@ -0,0 +1,91 @@ +import { V1ConfigMap } from '@kubernetes/client-node'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { KubeFastifyInstance, RecursivePartial } from '../../../types'; +import { secureAdminRoute } from '../../../utils/route-security'; +import { + getConnectionType, + listConnectionTypes, + createConnectionType, + updateConnectionType, + patchConnectionType, + deleteConnectionType, +} from './connectionTypeUtils'; + +module.exports = async (fastify: KubeFastifyInstance) => { + fastify.get( + '/', + secureAdminRoute(fastify)(async (request: FastifyRequest, reply: FastifyReply) => + listConnectionTypes(fastify) + .then((res) => res) + .catch((res) => { + reply.send(res); + }), + ), + ); + + fastify.get( + '/:name', + secureAdminRoute(fastify)( + async (request: FastifyRequest<{ Params: { name: string } }>, reply: FastifyReply) => + getConnectionType(fastify, request.params.name) + .then((res) => res) + .catch((res) => { + reply.send(res); + }), + ), + ); + + fastify.post( + '/', + secureAdminRoute(fastify)( + async (request: FastifyRequest<{ Body: V1ConfigMap }>, reply: FastifyReply) => + createConnectionType(fastify, request.body) + .then((res) => res) + .catch((res) => { + reply.send(res); + }), + ), + ); + + fastify.put( + '/:name', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ Params: { name: string }; Body: V1ConfigMap }>, + reply: FastifyReply, + ) => + updateConnectionType(fastify, request.params.name, request.body) + .then((res) => res) + .catch((res) => { + reply.send(res); + }), + ), + ); + + fastify.patch( + '/:name', + secureAdminRoute(fastify)( + async ( + request: FastifyRequest<{ Params: { name: string }; Body: RecursivePartial }>, + reply: FastifyReply, + ) => + patchConnectionType(fastify, request.params.name, request.body) + .then((res) => res) + .catch((res) => { + reply.send(res); + }), + ), + ); + + fastify.delete( + '/:name', + secureAdminRoute(fastify)( + async (request: FastifyRequest<{ Params: { name: string } }>, reply: FastifyReply) => + deleteConnectionType(fastify, request.params.name) + .then((res) => res) + .catch((res) => { + reply.send(res); + }), + ), + ); +}; diff --git a/backend/src/types.ts b/backend/src/types.ts index 0da7f13d37..28249c0e84 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -981,6 +981,7 @@ export enum KnownLabels { PROJECT_SHARING = 'opendatahub.io/project-sharing', MODEL_SERVING_PROJECT = 'modelmesh-enabled', DATA_CONNECTION_AWS = 'opendatahub.io/managed', + CONNECTION_TYPE = 'opendatahub.io/connection-type', } type ComponentNames = diff --git a/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts b/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts index cbb9557561..93f7aacc55 100644 --- a/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts +++ b/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts @@ -8,14 +8,14 @@ describe('utils', () => { it('should serialize / deserialize connection type fields', () => { const ct = mockConnectionTypeConfigMapObj({}); const configMap = toConnectionTypeConfigMap(ct); - expect(typeof configMap.data.fields).toBe('string'); + expect(typeof configMap.data?.fields).toBe('string'); expect(ct).toEqual(toConnectionTypeConfigMapObj(toConnectionTypeConfigMap(ct))); }); it('should serialize / deserialize connection type with missing fields', () => { const ct = mockConnectionTypeConfigMapObj({ fields: undefined }); const configMap = toConnectionTypeConfigMap(ct); - expect(configMap.data.fields).toBeUndefined(); + expect(configMap.data?.fields).toBeUndefined(); expect(ct).toEqual(toConnectionTypeConfigMapObj(configMap)); }); }); diff --git a/frontend/src/concepts/connectionTypes/types.ts b/frontend/src/concepts/connectionTypes/types.ts index 289643f57d..82a5b959d5 100644 --- a/frontend/src/concepts/connectionTypes/types.ts +++ b/frontend/src/concepts/connectionTypes/types.ts @@ -95,14 +95,14 @@ export type ConnectionTypeConfigMap = K8sResourceCommon & { 'opendatahub.io/connection-type': 'true'; }; }; - data: { + data?: { // JSON of type ConnectionTypeField fields?: string; }; }; export type ConnectionTypeConfigMapObj = Omit & { - data: { + data?: { fields?: ConnectionTypeField[]; }; }; diff --git a/frontend/src/concepts/connectionTypes/utils.ts b/frontend/src/concepts/connectionTypes/utils.ts index 7fb9121a2f..dac5355724 100644 --- a/frontend/src/concepts/connectionTypes/utils.ts +++ b/frontend/src/concepts/connectionTypes/utils.ts @@ -7,12 +7,16 @@ export const toConnectionTypeConfigMapObj = ( configMap: ConnectionTypeConfigMap, ): ConnectionTypeConfigMapObj => ({ ...configMap, - data: { fields: configMap.data.fields ? JSON.parse(configMap.data.fields) : undefined }, + data: configMap.data + ? { fields: configMap.data.fields ? JSON.parse(configMap.data.fields) : undefined } + : undefined, }); export const toConnectionTypeConfigMap = ( obj: ConnectionTypeConfigMapObj, ): ConnectionTypeConfigMap => ({ ...obj, - data: { fields: obj.data.fields ? JSON.stringify(obj.data.fields) : undefined }, + data: obj.data + ? { fields: obj.data.fields ? JSON.stringify(obj.data.fields) : undefined } + : undefined, }); diff --git a/frontend/src/services/connectionTypesService.ts b/frontend/src/services/connectionTypesService.ts new file mode 100644 index 0000000000..d4e9bcf63e --- /dev/null +++ b/frontend/src/services/connectionTypesService.ts @@ -0,0 +1,85 @@ +import axios from '~/utilities/axios'; +import { ResponseStatus } from '~/types'; +import { + ConnectionTypeConfigMap, + ConnectionTypeConfigMapObj, +} from '~/concepts/connectionTypes/types'; +import { + toConnectionTypeConfigMap, + toConnectionTypeConfigMapObj, +} from '~/concepts/connectionTypes/utils'; + +export const fetchConnectionTypes = (): Promise => { + const url = `/api/connection-types`; + return axios + .get(url) + .then((response) => + response.data.map((cm: ConnectionTypeConfigMap) => toConnectionTypeConfigMapObj(cm)), + ) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const fetchConnectionType = (name: string): Promise => { + const url = `/api/connection-types/${name}`; + return axios + .get(url) + .then((response) => toConnectionTypeConfigMapObj(response.data)) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const createConnectionType = ( + connectionType: ConnectionTypeConfigMapObj, +): Promise => { + const url = `/api/connection-types`; + return axios + .post(url, toConnectionTypeConfigMap(connectionType)) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const updateConnectionType = ( + connectionType: ConnectionTypeConfigMapObj, +): Promise => { + const url = `/api/connection-types/${connectionType.metadata.name}`; + return axios + .put(url, toConnectionTypeConfigMap(connectionType)) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const updateConnectionTypeEnabled = ( + name: string, + enabled: boolean, +): Promise => { + const url = `/api/connection-types/${name}`; + return axios + .patch(url, [ + { + op: 'replace', + path: '/metadata/annotations/opendatahub.io~1enabled', + value: enabled, + }, + ]) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; + +export const deleteConnectionType = (name: string): Promise => { + const url = `/api/connection-types/${name}`; + return axios + .delete(url) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +};