import { Channel, Resource, Service, Tenant, Usage } from "@kumori/aurora-interfaces"; import { Revision } from "@kumori/aurora-interfaces/interfaces/revision-interface"; import { getTimestamp } from "../utils/utils"; interface Role { name: string; instances: any[]; logo?: string; category?: string; version?: string; description?: string; resource?: any[]; parameters?: { [key: string]: string }[]; registry?: string; imageTag?: string; entrypoint?: string; cmd?: string; scalling?: { cpu: { up: string; down: string }; memory: { up: string; down: string }; instances: { max: number; min: number }; histeresys: string; }; hsize?: number; } interface HandleServiceEventParams { entityId: string; eventData: any; parentParts: { [entity: string]: string }; servicesMap: Map; revisionsMap: Map; roleMap: Map; tenantsMap: Map; pendingRevisionErrors: Array<{ service: string; revision: Revision }>; pendingProjects: Array<{ tenant: string; project: string }>; } interface HandleServiceEventResult { service: Service; serviceFullKey: string; wasDeployed: boolean; pendingProject: { tenant: string; project: string } | null; updatedPendingRevisionErrors: Array<{ service: string; revision: Revision }>; } interface HandleServiceOperationSuccessParams { action: string; entityName: string; originalData: any; responsePayload: any; servicesMap: Map; revisionsMap: Map; roleMap: Map; } interface HandleServiceOperationSuccessResult { updatedService: Service | null; shouldDelete: boolean; eventType: "deployed" | "updated" | "deleting" | null; processRevisionData: boolean; revisionData: any; serviceId: string; } interface HandleServiceOperationErrorParams { action: string; entityName: string; originalData: any; error: any; servicesMap: Map; roleMap: Map; } interface HandleServiceOperationErrorResult { eventType: | "deploymentError" | "updateError" | "deletionError" | "revisionError" | null; serviceId: string; shouldResetRoles: boolean; } const parseKeyPath = (key: string): { [entity: string]: string } => { if (!key) { return {}; } const cleanKey = key?.startsWith("/") ? key.slice(1) : key; const parts = cleanKey.split("/"); const result: { [entity: string]: string } = {}; for (let i = 0; i < parts.length; i += 2) { if (parts[i] && parts[i + 1]) { result[parts[i]] = parts[i + 1]; } } return result; }; const getDefaultUsage = (eventData?: any): Usage => ({ current: { cpu: 0, memory: 0, storage: 0, volatileStorage: 0, nonReplicatedStorage: 0, persistentStorage: 0, }, limit: { cpu: { max: eventData.vcpu || 0, min: eventData.vcpu || 0 }, memory: { max: eventData.ram || 0, min: eventData.ram || 0 }, storage: { max: eventData.shared_disk || 0, min: eventData.shared_disk || 0 }, volatileStorage: { max: eventData.volatile_disk || 0, min: eventData.volatile_disk || 0 }, nonReplicatedStorage: { max: eventData.persistent_disk || 0, min: eventData.persistent_disk || 0 }, persistentStorage: { max: eventData.persistent_disk || 0, min: eventData.persistent_disk || 0 }, }, cost: 0, }); const determineFinalStatusAndError = ( existingService: Service | undefined, eventData: any, pendingRevisionErrors: Array<{ service: string; revision: Revision }>, entityId: string, currentRevision: Revision | undefined, ): { finalStatus: any; finalError: any; pendingErrorIndex: number; } => { const incomingStatus = eventData.status.state; const incomingTs = getTimestamp(incomingStatus.timestamp); const currentTs = getTimestamp(existingService?.status?.timestamp); const revisionTs = currentRevision?.status ? getTimestamp(currentRevision.status.timestamp) : 0; const bestCandidateTs = Math.max(incomingTs, revisionTs); const isNewer = !existingService || bestCandidateTs > currentTs; const removingStatus = { code: "REMOVING_SERVICE", message: `Service ${entityId} is being removed`, args: [], timestamp: new Date().toISOString(), }; if (eventData.meta?.deleted || existingService?.status?.code === "REMOVING_SERVICE") { return { finalStatus: removingStatus, finalError: undefined, pendingErrorIndex: -1 }; } let finalStatus = existingService?.status; let finalError = existingService?.error; if (isNewer) { const revisionHasError = !!currentRevision?.errorCode; finalStatus = (revisionHasError || (currentRevision?.status && revisionTs >= incomingTs)) ? currentRevision!.status : incomingStatus; finalError = eventData.status.error ?? undefined; } const pendingErrorIndex = pendingRevisionErrors.findIndex( (pending) => pending.service === entityId, ); if (pendingErrorIndex !== -1) { const pendingError = pendingRevisionErrors[pendingErrorIndex]; finalStatus = pendingError.revision.status; finalError = { code: pendingError.revision.errorCode || "", message: pendingError.revision.errorMsg || "", timestamp: pendingError.revision.status.timestamp || "", }; } return { finalStatus, finalError, pendingErrorIndex }; }; export const handleServiceEvent = ({ entityId, eventData, parentParts, servicesMap, revisionsMap, roleMap, tenantsMap, pendingRevisionErrors, pendingProjects, }: HandleServiceEventParams): HandleServiceEventResult => { const serviceFullKey = `${parentParts.tenant}/${entityId}`; const serviceTenantId = parentParts.tenant; const currentRevisionKey = `${eventData.id.parent.name}/service/${entityId}/revision/${eventData.spec.ctstamp || eventData.status.revision}`; const currentRevision = revisionsMap.get(currentRevisionKey); const projectLabel = eventData.meta?.labels?.project; const serviceRoles = roleMap.get(serviceFullKey) || []; const serviceUsage = currentRevision?.usage || getDefaultUsage(eventData); const serviceRevisions: Revision[] = []; revisionsMap.forEach((revision, key) => { if (key.startsWith(`${eventData.id.parent.name}/service/${entityId}/revision/`)) { serviceRevisions.push(revision); } }); const environmentPath = eventData.spec.environment; const pathParts = parseKeyPath(environmentPath); const existingService = servicesMap.get(serviceFullKey); const { finalStatus, finalError, pendingErrorIndex } = determineFinalStatusAndError( existingService, eventData, pendingRevisionErrors, entityId, currentRevision, ); const updatedPendingRevisionErrors = [...pendingRevisionErrors]; if (pendingErrorIndex !== -1) { updatedPendingRevisionErrors.splice(pendingErrorIndex, 1); } const baseServiceData = { tenant: serviceTenantId, account: eventData.spec.metadata.targetAccount || pathParts.account, environment: eventData.spec.metadata.targetEnvironment || pathParts.environment, name: entityId, revisions: [...serviceRevisions].sort( (a, b) => Number(b.id) - Number(a.id), ), status: finalStatus || eventData.status.state, error: finalError, role: serviceRoles.length > 0 ? serviceRoles : existingService?.role || [], usage: serviceUsage, lastDeployed: eventData.spec.ctstamp, project: projectLabel || existingService?.project || "", currentRevision: String(eventData.spec.ctstamp || eventData.status.revision || ""), startedAt: currentRevision?.createdAt || existingService?.startedAt || "", }; let newService: Service; if (existingService) { newService = { ...existingService, ...baseServiceData, }; } else { newService = { id: serviceFullKey, logo: "", description: "", links: [], resources: [], parameters: [], minReplicas: 0, maxReplicas: 0, registry: "", imageName: "", entrypoint: "", cmd: "", serverChannels: [], clientChannels: [], duplexChannels: [], cloudProvider: "", ...baseServiceData, }; } const oldStatusCode = existingService?.status?.code; const newStatusCode = newService.status.code; const wasDeployed = existingService !== undefined && oldStatusCode !== "SERVICE_READY" && newStatusCode === "SERVICE_READY" && eventData.status.deployed === true; let pendingProject: { tenant: string; project: string } | null = null; if (projectLabel) { const tenant = tenantsMap.get(serviceTenantId); if (!tenant) { const existingPending = pendingProjects.find( (pending) => pending.tenant === serviceTenantId && pending.project === projectLabel, ); if (!existingPending) { pendingProject = { tenant: serviceTenantId, project: projectLabel, }; } } } return { service: newService, serviceFullKey, wasDeployed, pendingProject, updatedPendingRevisionErrors, }; }; export const handleServiceOperationSuccess = ({ action, entityName, originalData, responsePayload, servicesMap, revisionsMap, roleMap, }: HandleServiceOperationSuccessParams): HandleServiceOperationSuccessResult => { const serviceId = originalData ? `${originalData.tenant}/${entityName}` : entityName; if (action === "GET_CHANNELS") { return { updatedService: null, shouldDelete: false, eventType: null, processRevisionData: false, revisionData: null, serviceId, }; } if (action === "GET_REVISION") { const revisionData = responsePayload?.data; const service = servicesMap.get(serviceId); if (!service) { return { updatedService: null, shouldDelete: false, eventType: null, processRevisionData: false, revisionData: null, serviceId, }; } if (!revisionData?.solution) { return { updatedService: { ...service, role: [] }, shouldDelete: false, eventType: null, processRevisionData: false, revisionData: null, serviceId, }; } return { updatedService: service, shouldDelete: false, eventType: null, processRevisionData: true, revisionData, serviceId, }; } if (action === "DELETE") { const existingService = servicesMap.get(serviceId); if (existingService) { return { updatedService: { ...existingService, status: { code: "REMOVING_SERVICE", message: `Service ${entityName} is being removed`, args: [], timestamp: new Date().toISOString(), }, }, shouldDelete: false, eventType: "deleting", processRevisionData: false, revisionData: null, serviceId, }; } return { updatedService: null, shouldDelete: false, eventType: null, processRevisionData: false, revisionData: null, serviceId, }; } if (originalData) { return { updatedService: { ...originalData, status: { code: "SERVICE_READY", message: `Service (${entityName}) is ready.`, args: [], timestamp: new Date().toISOString(), }, }, shouldDelete: false, eventType: action === "CREATE" ? "deployed" : action === "UPDATE" ? "updated" : null, processRevisionData: false, revisionData: null, serviceId, }; } return { updatedService: null, shouldDelete: false, eventType: null, processRevisionData: false, revisionData: null, serviceId, }; }; export const handleServiceOperationError = ({ action, entityName, originalData, error, servicesMap, roleMap, }: HandleServiceOperationErrorParams): HandleServiceOperationErrorResult => { const serviceId = originalData ? `${originalData.tenant}/${entityName}` : entityName; if (action === "CREATE") { return { eventType: "deploymentError", serviceId, shouldResetRoles: false }; } if (action === "UPDATE") { return { eventType: "updateError", serviceId, shouldResetRoles: false }; } if (action === "DELETE") { return { eventType: "deletionError", serviceId, shouldResetRoles: false }; } if (action === "GET_REVISION") { const service = servicesMap.get(serviceId); if (service) { return { eventType: "revisionError", serviceId, shouldResetRoles: true }; } } return { eventType: null, serviceId, shouldResetRoles: false }; }; export const mapChannelsFromApiData = ( apiData: any, entityId: string, existingResources?: Resource[], ): { serverChannels: Channel[]; clientChannels: Channel[]; duplexChannels: Channel[]; } => { const serverChannels: Channel[] = []; const clientChannels: Channel[] = []; const duplexChannels: Channel[] = []; const inboundSuffix = "_inbound.inbound"; const publicChannelNames = new Set(); const collectPublic = (section: any) => { if (!section) return; Object.keys(section).forEach((channelName) => { if (channelName.endsWith(inboundSuffix)) { const baseName = channelName.slice(0, -inboundSuffix.length); if (section[baseName]) { publicChannelNames.add(baseName); } } }); }; collectPublic(apiData.server); collectPublic(apiData.client); collectPublic(apiData.duplex); const mapChannels = (section: any, target: Channel[]) => { if (!section) return; Object.entries(section).forEach( ([channelName, channelData]: [string, any]) => { if (channelName.endsWith(inboundSuffix)) return; const hasGatewayResource = existingResources?.some( (r) => (r.type === "port" || r.type === "domain") && (r.name === `${channelName}_port` || r.name === `${channelName}_domain`), ) ?? false; target.push({ name: channelName, from: entityId, to: "", protocol: channelData.protocol as "http" | "tcp" | "https", port: channelData.port, portNum: channelData.port, isPublic: publicChannelNames.has(channelName) || hasGatewayResource, }); }, ); }; mapChannels(apiData.server, serverChannels); mapChannels(apiData.client, clientChannels); mapChannels(apiData.duplex, duplexChannels); return { serverChannels, clientChannels, duplexChannels }; };