import { errors, Link, MarketplaceItem, MarketplaceService, Notification, Service, } from "@kumori/aurora-interfaces"; import { eventHelper } from "../backend-handler"; import { environment } from "../environment"; import { getWebSocketStatus, initializeGlobalWebSocketClient, makeGlobalWebSocketRequest, } from "../websocket-manager"; import { deployServiceHelper } from "./deploy-service-helper"; interface DeploymentParameter { name: string; value: string | number | boolean; type: "string" | "number" | "boolean"; } interface DeploymentResourceSpec { name: string; resource: string; } interface DeploymentVolatileVolumeSpec { name: string; volume: { kind: string; size: number; unit: "G"; }; } interface Secret { secret: VARIANT; } interface CA { ca: VARIANT; } interface Volume { volume: VARIANT; } interface Domain { domain: VARIANT; } interface Port { port: VARIANT; } interface Certificate { certificate: VARIANT; } type DeploymentResource = | CA | Secret | Volume | Volume | Domain | Port | Certificate; let pendingLinks = new Map(); export const deployMarketplaceItem = async (item: MarketplaceService) => { const complexDeployment = item.deploymentData.serverChannels.find( (channel) => channel.isPublic, ); if (complexDeployment !== undefined) { try { const formData = await deployServiceHelper(item.deploymentData, item); const url = new URL( `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${item.deploymentData.tenant}/service/${item.deploymentData.name}`, ); url.searchParams.append("dryrun", "false"); url.searchParams.append("accept", "true"); url.searchParams.append("wait", "30000"); url.searchParams.append("validate", "true"); url.searchParams.append("dsl", "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 { if (item.deploymentData.links.length > 0) { pendingLinks.set(item.deploymentData.name, item.deploymentData.links); linkPendingServices(item.deploymentData, ""); } } } catch (err) { console.error("Error en la petición de despliegue de servicio:", err); } } else { const deploymentConfigParameters: DeploymentParameter[] = item.parameterList .filter( (resource) => resource.type === "string" || resource.type === "integer" || resource.type === "number" || resource.type === "bool" || resource.type === "boolean" || resource.type === "fileContent" || resource.type === "array", ) .map((resource) => { if (resource.type === "string") { return { name: resource.name || "", value: (resource as any).value as string, type: "string", }; } if (resource.type === "fileContent") { return { name: resource.name || "", value: (resource as any).value as string, type: "string", }; } if (resource.type === "bool" || resource.type === "boolean") { return { name: resource.name || "", value: (resource as any).value === "true" || (resource as any).value === true, type: "boolean", }; } if (resource.type === "number" || resource.type === "integer") { return { name: resource.name || "", value: Number((resource as any).value), type: "number", }; } if (resource.type === "array") { return { name: resource.name || "", value: (resource as any).value || [], type: "string", }; } return { name: resource.name || "", value: (resource as any).value as string, type: "string", }; }); const deploymentData: Service = item.deploymentData as unknown as Service; const url = new URL( `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${item.deploymentData.tenant}/service/${deploymentData.name}`, ); url.searchParams.append("dsl", "true"); const parametersObj = deploymentConfigParameters.reduce( (acc, param) => { const paramMetadata = item.parameterList.find( (p) => p.name === param.name, ); if ( paramMetadata && (paramMetadata as any).parent === "accesspolicies" ) { if (!acc.accesspolicies) { acc.accesspolicies = {}; } acc.accesspolicies[param.name] = param.value; } else { acc[param.name] = param.value; } return acc; }, {} as { [key: string]: any }, ); const resourcesObj = item.resourceList.reduce( (acc, resource: any) => { if (resource.type === "secret") { acc[resource.name] = { secret: resource.value as string, } as unknown as DeploymentResource; } else if (resource.type === "volume") { acc[resource.name] = { volume: { kind: "storage", type: resource.kind, size: Number(resource.value || 1), unit: "G", }, } as unknown as DeploymentResource; } else if (resource.type === "domain") { acc[resource.name] = { domain: resource.value, } as unknown as DeploymentResource; } else if (resource.type === "certificate") { acc[resource.name] = { certificate: resource.value, } as unknown as DeploymentResource; } else if (resource.type === "ca") { acc[resource.name] = { ca: resource.value, } as unknown as DeploymentResource; } else if (resource.type === "port") { acc[resource.name] = { port: resource.value, } as unknown as DeploymentResource; } return acc; }, {} as { [key: string]: DeploymentResource }, ); const isInbound = item.module === "builtins/inbound"; if (isInbound) { const hasCertificate = item.resourceList.some( (resource) => resource.type === "certificate" || resource.name.toLowerCase().includes("cert") || resource.name === "servercert", ); const hasDomain = item.resourceList.some( (resource) => resource.type === "domain" || resource.name.toLowerCase().includes("domain") || resource.name === "serverdomain", ); const hasPort = item.resourceList.some( (resource) => resource.type === "port" || resource.name.toLowerCase().includes("port"), ); let correctType = "tcp"; let requiredParameters: { [key: string]: any } = {}; if (hasCertificate && hasDomain) { correctType = "https"; requiredParameters = { type: "https", clientcert: false, websocket: false, remoteaddressheader: parametersObj.remoteaddressheader || "", cleanxforwardedfor: false, }; } else if (hasPort && !hasDomain && !hasCertificate) { correctType = "tcp"; requiredParameters = { type: "tcp", }; } else { console.warn( "Inbound configuration doesn't match any valid schema variant:", { hasCertificate, hasDomain, hasPort, resources: item.resourceList.map((r) => ({ name: r.name, type: r.type, })), }, ); } Object.assign(parametersObj, requiredParameters); } const serviceName = item.serviceName.startsWith("./") ? item.serviceName.split("./")[1] : item.serviceName.startsWith(".") ? item.serviceName.split(".")[1] : item.serviceName; const scale = item.type === "component" || item.serviceName === "" ? { hsize: item.deploymentData.role[0].hsize || 0 } : item.schema?.name === "builtins/inbound" ? { detail: {} } : item.type === "service" && item.roles && Array.isArray(item.roles) ? { detail: {}, } : { detail: {} }; const scaling: { simple: { [key: string]: { scale_up: { cpu: number; memory: number }; scale_down: { cpu: number; memory: number }; hysteresis: number; min_replicas: number; max_replicas: number; }; }; } = { simple: {}, }; scaling.simple[item.serviceName] = { scale_up: { cpu: parseInt(item.scaling?.cpu.up || "") || 0, memory: parseInt(item.scaling?.memory.up || "") || 0, }, scale_down: { cpu: parseInt(item.scaling?.cpu.down || "") || 0, memory: parseInt(item.scaling?.memory.down || "") || 0, }, hysteresis: parseInt(item.scaling?.histeresys || "") || 0, min_replicas: item.scaling?.instances.min || 0, max_replicas: item.scaling?.instances.max || 0, }; const itemBody = { type: "deployment", deployment: { name: deploymentData.name, up: null, meta: {}, config: { parameter: parametersObj, resource: resourcesObj, resilience: 0, scale: scale, }, artifact: { ref: { version: [ Number(item.version.split(".")[0]), Number(item.version.split(".")[1]), Number(item.version.split(".")[2]), ], kind: item.type, domain: item.domain, module: item.module, name: item.artifact, package: serviceName, }, }, }, comment: " ", meta: { targetAccount: deploymentData.account, targetEnvironment: deploymentData.environment, scaling: scaling, }, }; const response = await fetch(url.toString(), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(itemBody), 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_", }); } if (deploymentData.links && deploymentData.links.length > 0) { pendingLinks.set(deploymentData.name, deploymentData.links); try { await linkPendingServices(deploymentData, ""); } catch (linkError) { console.error("⭐ Error en linkPendingServices:", linkError); } } } }; export const getMarketplaceItems = async ( tenants: string[], security: string, ) => { await initializeGlobalWebSocketClient(security); const promises = tenants.map(async (tenant) => { try { const response = await makeGlobalWebSocketRequest( "marketplace:search_marketplace", { tenant }, 30000, "GET", tenant, ); const tenantItems: MarketplaceItem[] = []; const itemPromises = Object.entries(response.data.items).map( async ([key, value]) => { let cpu = 0; let memory = 0; (value as any).tags.forEach((tag: string) => { if (tag.includes("cpu")) { cpu = Number(tag.split("::")[1].split("vCPU")[0]); } else if (tag.includes("memory")) { const value = tag.split("::")[1]?.trim(); if (value?.includes("GB")) { const memStr = value.split("GB")[0].trim(); memory = Number(memStr); } else if (value?.includes("MB")) { const memStr = value.split("MB")[0].trim(); memory = Number(memStr) / 1024; } } }); const item: MarketplaceItem = { tenant: tenant.toString(), name: (value as any).name, logo: (value as any).icon, description: (value as any).name, version: (value as any).version, requirements: { cpu, memory }, status: "", instances: [], links: [], resources: [], domain: (value as any).domain, type: (value as any).artifactTypes[0], artifact: (value as any).artifact, schema: undefined, }; return item; }, ); const processedItems = await Promise.all(itemPromises); tenantItems.push(...processedItems); return tenantItems; } catch (error) { console.error(`Error al obtener items para el tenant ${tenant}:`, error); return []; } }); const itemsArrays = await Promise.all(promises); const items = itemsArrays.flat(); return { items }; }; export const getMarketplaceSchema = async ( tenant: string, marketplaceItem: MarketplaceItem, security: string, ): Promise< | { resources: { name: string; type: string; required: boolean; protocol?: string; defaultValue?: string; }[]; parameters: { name: string; type: string; required: boolean; protocol?: string; pattern?: string; parent?: string; defaultValue?: string | number; }[]; channels: { name: string; type: "client" | "server" | "duplex"; resource?: { type: "port" | "domain"; value: string }; protocol?: string; }[]; name?: string; roleName?: string; roles?: string[]; hasVariants?: boolean; variants?: any[]; } | undefined > => { await initializeGlobalWebSocketClient(security); try { const requestPayload = { tenant, module: marketplaceItem.name, version: marketplaceItem.version, domain: marketplaceItem.domain, artifact: marketplaceItem.artifact, }; const response = await makeGlobalWebSocketRequest( "marketplace:artifact_schema", requestPayload, 30000, "GET", tenant, ); const nameKeys = Object.keys(response.data); const schemaData = response.data[nameKeys[0]]; let roleName = ""; let roles: string[] = []; if (nameKeys[0] !== ".") { roles = schemaData.roles || []; roleName = roles.join(", "); } const schemas = schemaData.schemas || []; if (schemas.length > 0 && schemas[0].oneOf) { const schemaOptions = schemas[0].oneOf; const processedSchemas: any[] = []; for (const schemaRef of schemaOptions) { const refKey = schemaRef.$ref?.replace("#/$defs/", ""); const actualSchema = schemas[0].$defs?.[refKey]; if (actualSchema) { const schemaResult = processSchema(actualSchema, refKey); if (schemaResult) { processedSchemas.push({ name: nameKeys[0], roleName, roles, variant: refKey, ...schemaResult, }); } } } return { parameters: processedSchemas[0]?.parameters || [], resources: processedSchemas[0]?.resources || [], channels: processedSchemas[0]?.channels || [], name: nameKeys[0], roleName, roles, hasVariants: true, variants: processedSchemas, } as any; } const singleSchema = schemas[0]; if (singleSchema) { const schemaResult = processSchema(singleSchema, "default"); return { parameters: schemaResult?.parameters || [], resources: schemaResult?.resources || [], channels: schemaResult?.channels || [], name: nameKeys[0], roleName, roles, hasVariants: false, } as any; } return { parameters: [], resources: [], channels: [], name: nameKeys[0], roleName, roles, hasVariants: false, } as any; } catch (error) { console.error("Error obteniendo el schema del item:", error); throw error; } }; function processSchema(schema: any, schemaType: string) { let resources: { name: string; type: string; required: boolean; defaultValue?: string; }[] = []; let parameters: { name: string; type: string; required: boolean; defaultValue?: string | number; pattern?: string; parent?: string; }[] = []; let channels: { name: string; type: "client" | "server" | "duplex"; resource?: { type: "port" | "domain"; value: string; }; }[] = []; const configProps = schema.properties?.config?.properties; if (configProps) { Object.entries(configProps).forEach(([key, value]) => { const param = value as any; parameters.push({ name: key, type: param.type || "string", required: schema.properties?.config?.required?.includes(key) || false, defaultValue: param.default !== undefined ? param.default : param.enum ? param.enum[0] : param.const ? param.const : undefined, }); }); } const volumeTypes = [ "Registered", "NonReplicated", "Persisted", "Volatile", "Ephemeral", "Persistent", ]; const isVolumeResource = (resourceValue: any): boolean => { if (resourceValue.oneOf && Array.isArray(resourceValue.oneOf)) { return resourceValue.oneOf.some((option: any) => { if (option.oneOf && Array.isArray(option.oneOf)) { return option.oneOf.some((nestedOption: any) => { const typeName = nestedOption?.properties?.$kdsl?.const?.NamedType?.Name; return typeName && volumeTypes.includes(typeName); }); } const typeName = option?.properties?.$kdsl?.const?.NamedType?.Name; return typeName && volumeTypes.includes(typeName); }); } return false; }; const resourceProps = schema.properties?.resource; if (resourceProps?.properties) { Object.entries(resourceProps.properties).forEach(([key, value]) => { const resourceValue = value as any; if (isVolumeResource(resourceValue)) { resources.push({ name: key, type: "volume", required: resourceProps.required?.includes(key) || false, defaultValue: undefined, }); } else { let resourceType = ""; let defaultValue: string | undefined = undefined; if (resourceValue.properties?.["$kdsl"]?.const?.NamedType?.Name) { const typeName = resourceValue.properties["$kdsl"].const.NamedType.Name; resourceType = typeName.toLowerCase(); } else { resourceType = Object.keys(resourceValue.properties || {})[0]; } if (resourceValue.properties) { const innerResource = resourceValue.properties[resourceType] || resourceValue.properties.inner; if (innerResource) { if (innerResource.default !== undefined) { defaultValue = String(innerResource.default); } else if (innerResource.enum && innerResource.enum.length > 0) { defaultValue = String(innerResource.enum[0]); } else if (resourceType === "volume" && innerResource.oneOf) { const firstOption = innerResource.oneOf[0]; if (firstOption?.properties?.volume?.properties) { const sizeDefault = firstOption.properties.volume.properties.size?.default; const unitDefault = firstOption.properties.volume.properties.unit?.enum?.[0]; if (sizeDefault !== undefined && unitDefault) { defaultValue = `${sizeDefault}${unitDefault}`; } } } } } resources.push({ name: key, type: resourceType || "string", required: resourceProps.required?.includes(key) || false, defaultValue: defaultValue, }); } }); } const srvProps = schema.properties?.srv?.properties; if (srvProps) { if (srvProps.client?.properties) { Object.entries(srvProps.client.properties).forEach( ([channelName, channelData]) => { const channelInfo = channelData as any; let resource: { type: "port" | "domain"; value: string } | undefined; if (channelInfo.properties) { const port = channelInfo.properties.port?.const || channelInfo.properties.port?.enum?.[0]; const protocol = channelInfo.properties.protocol?.const || channelInfo.properties.protocol?.enum?.[0]; if (port && protocol) { resource = { type: "port", value: `${protocol}:${port}`, }; } } channels.push({ name: channelName, type: "client", resource, }); }, ); } if (srvProps.server?.properties) { Object.entries(srvProps.server.properties).forEach( ([channelName, channelData]) => { const channelInfo = channelData as any; let resource: { type: "port" | "domain"; value: string } | undefined; if (channelInfo.properties) { const port = channelInfo.properties.port?.const || channelInfo.properties.port?.enum?.[0]; const protocol = channelInfo.properties.protocol?.const || channelInfo.properties.protocol?.enum?.[0]; if (port && protocol) { resource = { type: "port", value: `${protocol}:${port}`, }; } } channels.push({ name: channelName, type: "server", resource, }); }, ); } if (srvProps.duplex?.properties) { Object.entries(srvProps.duplex.properties).forEach( ([channelName, channelData]) => { const channelInfo = channelData as any; let resource: { type: "port" | "domain"; value: string } | undefined; if (channelInfo.properties) { const port = channelInfo.properties.port?.const || channelInfo.properties.port?.enum?.[0]; const protocol = channelInfo.properties.protocol?.const || channelInfo.properties.protocol?.enum?.[0]; if (port && protocol) { resource = { type: "port", value: `${protocol}:${port}`, }; } } channels.push({ name: channelName, type: "duplex", resource, }); }, ); } } return { parameters, resources, channels, }; } const generateLinkBody = (data: Service, link: Link) => { let linkBody; if (link.origin === data.name) { const originInClient = data.clientChannels.find( (channel) => channel.name === link.originChannel || channel.from === link.originChannel, ); const originInServer = data.serverChannels.find( (channel) => channel.name === link.originChannel || channel.from === link.originChannel, ); const originInDuplex = data.duplexChannels.find( (channel) => channel.name === link.originChannel || channel.from === link.originChannel, ); 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 (originInServer || 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 (link.target === data.name) { const targetInClient = data.clientChannels.find( (channel) => channel.name === link.targetChannel || channel.from === link.targetChannel, ); const targetInServer = data.serverChannels.find( (channel) => channel.name === link.targetChannel || channel.from === link.targetChannel, ); const targetInDuplex = data.duplexChannels.find( (channel) => channel.name === link.targetChannel || channel.from === link.targetChannel, ); 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 (targetInServer || 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( `Servicio actual no involucrado en el enlace: current=${data.name}, 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; }; const linkPendingServices = async (service: Service, token: string) => { const links = pendingLinks.get(service.name); if (links) { await Promise.all( service.links.map(async (link, index) => { 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 loadMarketplaceItemsForTenant = async ( tenant: string, security: string, ) => { try { await initializeGlobalWebSocketClient(security); const response = await makeGlobalWebSocketRequest( "marketplace:search_marketplace", { tenant }, 30000, "GET", tenant, ); const items = response.data?.items || []; if (!Array.isArray(items) || items.length === 0) { return []; } return items.map((item: any): MarketplaceItem => ({ tenant, name: item.module, logo: item.icon, description: item.description, version: item.version, requirements: { cpu: item.requirements?.cpu || 0, memory: item.requirements?.memory || 0, }, status: "", instances: [], links: [], resources: [], domain: item.domain, type: item.artifactTypes?.[0], artifact: item.artifact, package: item.package, categories: item.categories || [], schema: undefined, })); } catch (error) { console.error( `Error loading marketplace items for tenant ${tenant}:`, error, ); return []; } };