import { environment } from "../environment"; import { deployServiceHelper } from "./deploy-service-helper"; import { eventHelper } from "../backend-handler"; import { initializeGlobalWebSocketClient, getWebSocketStatus, makeGlobalWebSocketRequest, } from "../websocket-manager"; import { errors, Link, Notification, Service } from "@kumori/aurora-interfaces"; import { Revision } from "@kumori/aurora-interfaces/interfaces/revision-interface"; let pendingLinks = new Map(); /** * Function to deploy a service * @param data Service object with the data of the service */ export const deployService = async (data: Service, token: string) => { try { const url = new URL( `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}`, ); url.searchParams.append("dryrun", "false"); url.searchParams.append("accept", "true"); url.searchParams.append("wait", "30000"); url.searchParams.append("validate", "true"); if (data.dsl) { url.searchParams.append("dsl", "true"); } if (data.serviceData) { const formData = new FormData(); formData.append("bundle", data.serviceData); formData.append( "meta", JSON.stringify({ targetAccount: data.account, targetEnvironment: data.environment, }), ); formData.append("labels", JSON.stringify({ project: data.project })); formData.append("comment", " "); const response = await fetch(url.toString(), { method: "POST", body: formData, credentials: "include", }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const jsonResponse = await response.json(); const isTimeout = jsonResponse?.events?.some( (event: any) => event.content === "_timeout_", ); if (isTimeout) { console.error("Timeout en la petición:", { isOk: false, code: "TIMEOUT", error: "_timeout_", }); } } else { const formData = await deployServiceHelper(data); if (data.download) { return; } const response = await fetch(url.toString(), { method: "POST", body: formData, credentials: "include", }); if (!response.ok) { const jsonResponse = await response.json(); data.error = { code: jsonResponse.code, message: jsonResponse.content, timestamp: new Date().toISOString(), }; eventHelper.service.publish.deploymentError(data); throw new Error(`HTTP error! status: ${response.status}`); } const jsonResponse = await response.json(); const isTimeout = jsonResponse?.events?.some( (event: any) => event.content === "_timeout_", ); if (isTimeout) { console.error("Timeout en la petición:", { isOk: false, code: "TIMEOUT", error: "_timeout_", }); } else { if (data.links.length > 0) { pendingLinks.set(data.name, data.links); linkPendingServices(data, token); } } } } catch (err) { console.error("Error en la petición de despliegue de servicio:", err); // data.error = { // code: (err as any).code, // message: (err as any).content, // timestamp: new Date().toISOString(), // } // eventHelper.service.publish.deploymentError(data); } }; export const redeployService = async (data: Service) => { try { const formData = await deployServiceHelper(data); const url = new URL( `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}/revision/${data.currentRevision}`, ); url.searchParams.append("dryrun", "false"); url.searchParams.append("accept", "true"); url.searchParams.append("wait", "30000"); url.searchParams.append("validate", "true"); const response = await fetch(url.toString(), { method: "POST", body: formData, credentials: "include", }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const jsonResponse = await response.json(); const isTimeout = jsonResponse?.events?.some( (event: any) => event.content === "_timeout_", ); if (isTimeout) { console.error("Timeout en la petición:", { isOk: false, code: "TIMEOUT", error: "_timeout_", }); } else { eventHelper.service.publish.deployed(data); } } catch (err) { console.error("Error en la petición de redepliegue de servicio:", err); } }; export const deleteService = async (data: Service, security: string) => { try { await initializeGlobalWebSocketClient(security); const status = getWebSocketStatus(); const deleteBody = { tenant: data.tenant, service: data.name, wait: 0, remove_links: true, }; const response = await makeGlobalWebSocketRequest( "service:delete_service", deleteBody, 30000, "DELETE", data.name, ); return response; } catch (err) { console.error("Error in service deletion WebSocket request:", err); eventHelper.service.publish.deletionError(data); throw err; } }; export const restartService = async (data: Service, security: string) => { try { await initializeGlobalWebSocketClient(security); const status = getWebSocketStatus(); const restartBody = { tenant: data.tenant, service: data.name, }; const response = await makeGlobalWebSocketRequest( "service:restart", restartBody, 30000, "RESTART", data.name, ); const updatedService: Service = { ...data, status: { code: "PENDING", message: "", timestamp: "", args: [], }, }; eventHelper.service.publish.restarted(updatedService); return response; } catch (err) { console.error("Error in service restart WebSocket request:", err); eventHelper.service.publish.restartError(data); throw err; } }; const generateLinkBody = (data: Service, link: Link) => { const isOrigin = data.name === link.origin; const isTarget = data.name === link.target; const originInClient = isOrigin ? data.clientChannels.find( (channel) => channel.name === link.originChannel || channel.from === link.originChannel, ) : undefined; const originInServer = isOrigin ? data.serverChannels.find( (channel) => channel.name === link.originChannel || channel.from === link.originChannel, ) : undefined; const originInDuplex = isOrigin ? data.duplexChannels.find( (channel) => channel.name === link.originChannel || channel.from === link.originChannel, ) : undefined; const targetInClient = isTarget ? data.clientChannels.find( (channel) => channel.name === link.targetChannel || channel.from === link.targetChannel, ) : undefined; const targetInServer = isTarget ? data.serverChannels.find( (channel) => channel.name === link.targetChannel || channel.from === link.targetChannel, ) : undefined; const targetInDuplex = isTarget ? data.duplexChannels.find( (channel) => channel.name === link.targetChannel || channel.from === link.targetChannel, ) : undefined; let linkBody; if (originInClient) { linkBody = { client_tenant: data.tenant, client_service: data.name, client_channel: link.originChannel, server_tenant: data.tenant, server_service: link.target, server_channel: link.targetChannel, }; } else if (targetInClient) { linkBody = { client_tenant: data.tenant, client_service: data.name, client_channel: link.targetChannel, server_tenant: data.tenant, server_service: link.origin, server_channel: link.originChannel, }; } else if (originInServer) { linkBody = { client_tenant: data.tenant, client_service: link.target, client_channel: link.targetChannel, server_tenant: data.tenant, server_service: data.name, server_channel: link.originChannel, }; } else if (targetInServer) { linkBody = { client_tenant: data.tenant, client_service: link.origin, client_channel: link.originChannel, server_tenant: data.tenant, server_service: data.name, server_channel: link.targetChannel, }; } else if (originInDuplex) { linkBody = { client_tenant: data.tenant, client_service: link.target, client_channel: link.targetChannel, server_tenant: data.tenant, server_service: data.name, server_channel: link.originChannel, }; } else if (targetInDuplex) { linkBody = { client_tenant: data.tenant, client_service: link.origin, client_channel: link.originChannel, server_tenant: data.tenant, server_service: data.name, server_channel: link.targetChannel, }; } else { console.warn( `No se encontraron canales para el enlace: origin=${link.origin}, target=${link.target}`, ); linkBody = { client_tenant: data.tenant, client_service: link.origin, client_channel: link.originChannel, server_tenant: data.tenant, server_service: link.target, server_channel: link.targetChannel, }; } return linkBody; }; export const linkPendingServices = async (service: Service, token: string) => { const links = pendingLinks.get(service.name); if (links) { await Promise.all( links.map(async (link) => { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const linkBody = generateLinkBody(service, link); const linkResponse = await makeGlobalWebSocketRequest( "link:link_service", linkBody, 30000, "LINK", service.name, ); const notification: Notification = { type: "success", subtype: errors.service.linked.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { service: service.name, tenant: service.tenant, }, userError: true, }; eventHelper.notification.publish.creation(notification); } catch (linkErr) { const notification: Notification = { type: "error", subtype: errors.service.linkError.subtype, date: Date.now().toString(), info_content: { code: (linkErr as any).error.code, message: (linkErr as any).error.content, }, status: "unread", callToAction: false, data: { service: service.name, tenant: service.tenant, }, }; eventHelper.notification.publish.creation(notification); } }), ); } }; export const requestRevisionData = async (service: Service, token: string) => { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const revisionBody = { tenant: service.tenant, service: service.name, revision: "latest", }; const response = await makeGlobalWebSocketRequest( "revision:revision_info", revisionBody, 30000, "GET_REVISION", service.name, "service", service, ); return response; } catch (err) { console.error("Error in service deletion WebSocket request:", err); throw err; } }; export const updateService = async ( data: Service, token: string, scale?: number, ) => { try { await initializeGlobalWebSocketClient(token); // await unlinkServices(data, token); // const newLinks: Link[] = data.links.filter((link) => !link.delete); // const newLinksToCreate = newLinks.filter((link) => link.delete === false); // console.log("DEBUG - newLinks", newLinksToCreate); // if (newLinksToCreate.length > 0) { // const serviceWithNewLinks: Service = { // ...data, // links: newLinksToCreate, // }; // pendingLinks.set(data.name, newLinksToCreate); // await linkPendingServices(serviceWithNewLinks, token); // } const url = new URL( `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}/revision/${data.currentRevision}`, ); if (data.dsl) { url.searchParams.append("dsl", "true"); } if (data.serviceData) { const formData = new FormData(); formData.append("type", "update-bundle"); formData.append("bundle", data.serviceData); formData.append( "meta", JSON.stringify({ targetAccount: data.account, targetEnvironment: data.environment, }), ); formData.append("labels", JSON.stringify({ project: data.project })); formData.append("comment", " "); const response = await fetch(url.toString(), { method: "POST", body: formData, credentials: "include", }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const jsonResponse = await response.json(); const isTimeout = jsonResponse?.events?.some( (event: any) => event.content === "_timeout_", ); if (isTimeout) { console.error("Timeout en la petición:", { isOk: false, code: "TIMEOUT", error: "_timeout_", }); } } else { const parameterObject: Record = {}; if (data.parameters && data.parameters.length > 0) { data.parameters.forEach((param) => { const key = param.configKey || param.name; const paramType = param.type?.toLowerCase(); if (paramType === "number" || paramType === "integer") { parameterObject[key] = Number(param.value) || 0; } else if (paramType === "boolean") { parameterObject[key] = param.value === "true"; } else { parameterObject[key] = param.value; } }); } const resourceObject: Record = {}; if (data.resources && data.resources.length > 0) { data.resources.forEach((resource) => { if (resource.type === "volume") { resourceObject[resource.name] = { volume: { kind: "storage", size: parseInt(resource.value) || 1, unit: "G", type: resource.kind, }, }; } else if (resource.type === "secret") { resourceObject[resource.name] = { secret: `${data.tenant}/${resource.value}`, }; } else if (resource.type === "domain") { resourceObject[resource.name] = { domain: `${data.tenant}/${resource.value}`, }; } else if (resource.type === "ca") { resourceObject[resource.name] = { ca: `${data.tenant}/${resource.value}`, }; } else if (resource.type === "certificate") { resourceObject[resource.name] = { certificate: `${data.tenant}/${resource.value}`, }; } else if (resource.type === "port") { resourceObject[resource.name] = { port: `${data.tenant}/${resource.value}`, }; } }); } let previousRevision = 1; if (data.currentRevision) { previousRevision = parseInt(data.currentRevision.toString(), 10); if (isNaN(previousRevision)) { console.warn("currentRevision is not a valid number, using 1"); previousRevision = 1; } } else { previousRevision = getLatestRevision(data.revisions) || 1; } const scaleConfig: any = {}; if (data.role && data.role.length > 0) { scaleConfig.detail = {}; data.role.forEach((role) => { scaleConfig.detail[role.name] = { hsize: role.hsize || scale || data.minReplicas || 1, }; }); } else { scaleConfig.hsize = scale || data.minReplicas || 1; } const meta = { scaling: { simple: data.role?.reduce( (acc, role) => { if (role.scalling && role.name) { acc[role.name] = { scale_up: { cpu: Math.min(parseInt(role.scalling.cpu.up) || 0, 100), memory: Math.min( parseInt(role.scalling.memory.up) || 0, 100, ), }, scale_down: { cpu: Math.min(parseInt(role.scalling.cpu.down) || 0, 100), memory: Math.min( parseInt(role.scalling.memory.down) || 0, 100, ), }, hysteresis: parseInt(role.scalling.histeresys) || 0, min_replicas: role.scalling.instances.min || 0, max_replicas: role.scalling.instances.max || 0, }; } return acc; }, {} as Record, ) || {}, }, }; const updateBody = { spec: { type: "update-config", comment: "Service configuration update", config: { parameter: parameterObject, resource: resourceObject, resilience: 0, scale: scaleConfig, }, meta: meta, }, tenant: data.tenant, service: data.name, previous: previousRevision, }; const response = await makeGlobalWebSocketRequest( "revision:update_revision", updateBody, 30000, "UPDATE_CONFIG", data.name, ); const updatedService: Service = { ...data, status: { code: "PENDING", message: "", timestamp: "", args: [], }, }; eventHelper.service.publish.updated(updatedService); const updateNotification: Notification = { type: "success", subtype: errors.service.updated.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { service: data.name, tenant: data.tenant, }, }; eventHelper.notification.publish.creation(updateNotification); return response; } } catch (err) { console.error("Error updating service configuration via WebSocket:", err); const notification: Notification = { type: "error", subtype: errors.service.updateError.subtype, date: Date.now().toString(), info_content: { code: (err as any).error.code, message: (err as any).error.content, }, status: "unread", callToAction: false, data: { service: data.name, tenant: data.tenant, }, userError: true, }; eventHelper.notification.publish.creation(notification); eventHelper.service.publish.updateError(data); throw err; } }; export const unlinkServices = async (service: Service, token: string) => { const deleteLinks: Link[] = service.links.filter( (link) => link.delete === true, ); if (deleteLinks.length > 0) { await Promise.all( deleteLinks.map(async (link) => { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const unlinkBody = generateLinkBody(service, link); const unlinkResponse = await makeGlobalWebSocketRequest( "link:unlink_service", unlinkBody, 30000, "UNLINK", service.name, ); const unlinkNotification: Notification = { type: "success", subtype: errors.service.unlinking.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { service: service.name, tenant: service.tenant, }, }; eventHelper.notification.publish.creation(unlinkNotification); } catch (unlinkErr) { console.error("Error unlinking service via WebSocket:", unlinkErr); const notification: Notification = { type: "error", subtype: errors.service.unlinkError.subtype, date: Date.now().toString(), info_content: { code: (unlinkErr as any).error.code, message: (unlinkErr as any).error.content, }, status: "unread", callToAction: false, data: { service: service.name, tenant: service.tenant, }, userError: true, }; eventHelper.notification.publish.creation(notification); } }), ); } }; export const updateServiceLinks = async (link: Link, token: string) => { const deleteLink = link.delete || false; const originClient = link.client === link.originChannel; const linkBody = { client_tenant: link.tenant, client_service: originClient ? link.origin : link.target, client_channel: originClient ? link.originChannel : link.targetChannel, server_tenant: link.tenant, server_service: originClient ? link.target : link.origin, server_channel: originClient ? link.targetChannel : link.originChannel, }; if (deleteLink) { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const unlinkResponse = await makeGlobalWebSocketRequest( "link:unlink_service", linkBody, 30000, "UNLINK", link.origin, ); // const unlinkNotification: Notification = { // type: "success", // subtype: "service-unlinked", // date: Date.now().toString(), // status: "unread", // callToAction: false, // data: { // service: link.origin, // tenant: link.tenant, // }, // }; // eventHelper.notification.publish.creation(unlinkNotification); } catch (unlinkErr) { console.error("Error unlinking service via WebSocket:", unlinkErr); const notification: Notification = { type: "error", subtype: errors.service.unlinkError.subtype, date: Date.now().toString(), info_content: { code: (unlinkErr as any).error.code, message: (unlinkErr as any).error.content, }, status: "unread", callToAction: false, data: { service: link.origin, tenant: link.tenant, }, userError: true, }; eventHelper.notification.publish.creation(notification); } } else { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const linkResponse = await makeGlobalWebSocketRequest( "link:link_service", linkBody, 30000, "LINK", link.origin, ); const notification: Notification = { type: "success", subtype: errors.service.linked.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { service: link.origin, tenant: link.tenant, }, userError: true, }; eventHelper.notification.publish.creation(notification); } catch (linkErr) { const notification: Notification = { type: "error", subtype: errors.service.linkError.subtype, date: Date.now().toString(), info_content: { code: (linkErr as any).error.code, message: (linkErr as any).error.content, }, status: "unread", callToAction: false, data: { service: link.origin, tenant: link.tenant, }, }; eventHelper.notification.publish.creation(notification); } } }; export const changeRevision = async (data: Service, token: string) => { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const revisionBody = { tenant: data.tenant, service: data.name, previous: Number(data.lastDeployed), spec: { type: "rollback", labels: {}, target: Number(data.currentRevision), comment: "", }, }; const revisionResponse = await makeGlobalWebSocketRequest( "revision:update_revision", revisionBody, 30000, "UPDATE_REVISION", data.name, ); const notification: Notification = { type: "success", subtype: errors.deployment.revisionUpdated.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { service: data, }, }; eventHelper.notification.publish.creation(notification); } catch (error) { const notification: Notification = { type: "error", subtype: errors.deployment.revisionUpdateError.subtype, date: Date.now().toString(), info_content: { code: (error as any).error.code, message: (error as any).error.content, }, status: "unread", callToAction: false, data: { service: data, }, }; eventHelper.notification.publish.creation(notification); } }; function getLatestRevision(revisions: Revision[]): number | null { if (!revisions || revisions.length === 0) { return null; } return Math.max(...revisions.map((revision) => Number(revision.id))); } export async function restartInstance( service: Service, roleId: string, instanceId: string, token: string, ) { try { await initializeGlobalWebSocketClient(token); const status = getWebSocketStatus(); const revisionBody = { tenant: service.tenant, service: service.name, role: roleId, instance: instanceId === "all" ? undefined : instanceId, }; const revisionResponse = await makeGlobalWebSocketRequest( "service:restart", revisionBody, 30000, "RESTART_INSTANCE", service.name, ); const notification: Notification = { type: "success", subtype: errors.deployment.instanceRestarted.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { service: service, }, }; eventHelper.notification.publish.creation(notification); } catch (error) { const notification: Notification = { type: "error", subtype: errors.deployment.instanceUpdateError.subtype, date: Date.now().toString(), info_content: { code: (error as any).error.code, message: (error as any).error.content, }, status: "unread", callToAction: false, data: { service: service, }, }; eventHelper.notification.publish.creation(notification); } }