import { ComponentSpec } from "@kumori/kumori-module-generator"; import { Parameter } from "@kumori/kumori-module-generator/dist/types"; import { getReferenceDomain } from "../websocket-manager"; import { createVolume } from "./resources-api-service"; import { buildServiceDeploymentModule, ComponentSpec as ComponentSpecDSL, ServiceSpec as ServiceSpecDSL, } from "@kumori/kumori-dsl-generator"; import { MarketplaceService, Resource, Service, } from "@kumori/aurora-interfaces"; export type ResourceBundle = | { secret: { name: string; configResource: string } } | { volume: { name: string; configResource: string } } | { certificate: { name: string; configResource: string } } | { domain: { name: string; configResource: string } } | { port: { name: string; configResource: string } } | { ca: { name: string; configResource: string } }; export interface Role { name: string; artifact: { artifactKind: "component" | "service"; moduleDomain: string; moduleName: string; moduleVersion: [number, number, number]; artifactName: string; config: { parameters: Parameter[]; resources: ResourceBundle[]; scale?: { detail: { [key: string]: number } }; }; }; } export interface Connector { clientRole: string; clientChannel: string; serverRole: string; serverChannel: string; } export interface DeploymentParameter { name: string; value: string; type: "string" | "bool" | "number"; } export type DeploymentResource = | { secret: { name: string; resource: string } } | { volume: { name: string; resource: string } } | { volume: { name: string; volume: { size: number; unit: "M"; type: "nonreplicated" }; }; } | { volume: { name: string; volume: { size: number; unit: "M"; type: "persistent" }; }; } | { volume: { name: string; volume: { size: number; unit: "M"; type: "volatile" }; }; } | { certificate: { name: string; resource: string } } | { domain: { name: string; resource: string } } | { ca: { name: string; resource: string } } | { port: { name: string; resource: string } }; export interface ServiceSpecForm { tenantId: string; accountId: string; environmentId: string; serviceId: string; cpuRequirements: number; memoryRequirements: number; registryUrl: string; registryCredentialsSecret?: string; registryCredentialSecret?: string; imageTag: string; clientChannels: Array<{ name: string }>; channels: Array<{ channelName: string; containerPort: number; isPublic: boolean; protocol: "HTTPS" | "TCP"; publicPort?: string; domain?: string; certificate?: string; ca?: string; certificateResource?: string; withMtls?: boolean; caResource?: string; }>; clientChannelsExtra?: Array<{ name: string }>; defaultExecutable: { cmd?: string; entryPoint?: string }; config: { parameters: Array<{ name: string; value: string; type: string; content?: string; size?: number; }>; resources: Array<{ name: string; type: string; size?: number; value?: string; content?: string; kind?: string; key?: string; }>; }; environment?: any; fileSystem?: any; scaling?: { cpu: { up: string; down: string; }; memory: { up: string; down: string; }; instances: { max: number; min: number; }; histeresys: string; }; hsize?: number; } export interface ServiceWithLocalComponentSpec { type: "service"; subtype: "service_with_local_component"; tenantId: string; accountId: string; environmentId: string; deploymentId: string; moduleDomain: string; moduleId: string; moduleVersion: [number, number, number]; labels: Record; bundleTargetDir: string; bundleTargetKind: string; channels: { client: string[]; server: Array<{ name: string; port?: number }>; }; config: { parameters: any[]; resources: any[]; }; roles: Role[]; topology: Connector[]; local_components: { [key: string]: ComponentSpec; }; deploymentConfig: { parameters: DeploymentParameter[]; resources: DeploymentResource[]; scale: { detail: { [key: string]: number } }; meta: any; }; } interface ArtifactConfigParameterDto { name: string; defaultValue?: string | boolean | number; value?: string; type: ParameterTypeDto; } type ParameterTypeDto = "string" | "number" | "bool"; type ComponentEnvironmentDto = | ComponentEnvironmentParamDto | ComponentEnvironmentSecretDto; interface ComponentEnvironmentParamDto { varName: string; param: string; } interface ComponentEnvironmentSecretDto { varName: string; secret: string; } type ArtifactConfigResource = | CA | Secret | Volume | Domain | Port | Certificate; interface ArtifactConfigResourceSpec { name: string; } 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 ComponentFilesystemDto = FilesystemVolumeDto | FilesystemFileDto; interface FilesystemVolumeDto { path: string; resourceVolume: string; } interface FilesystemFileDto { path: string; param?: string; value?: string; } interface ServiceSpecDSLWithLocalComponent extends ServiceSpecDSL { local_components?: { [key: string]: ComponentSpecDSL; }; } /** * Function to return the value if it is not null or undefined, otherwise it returns the default value * @param value Value to check * @param defaultValue Default value to return * @returns The value if it is not null or undefined, otherwise it returns the default value */ export function withDefaultValue( value: T | null | undefined, defaultValue: T, ): T { return value != null ? value : defaultValue; } /** * Function to format the parameters of a service deployment. * @param formParams Parameters to format. * @returns An object with the parameters, environment, fileSystem and resources formatted. */ export function handleParametersToGenerateData( formParams: Array<{ name: string; value: string | boolean | number; type: string; content?: string; size?: number; }>, formResources: Array<{ name: string; type: string; size?: number; value?: string; content?: string; kind?: string; key?: string; }>, ): { parameters: any[]; environment: any[]; fileSystem: any[]; resources: any[]; } { const parametersResult: ArtifactConfigParameterDto[] = []; const environmentResult: ComponentEnvironmentDto[] = []; const resourcesResult: ArtifactConfigResource[] = []; const fileSystemResult: ComponentFilesystemDto[] = []; formParams.forEach((parameter, index) => { switch (parameter.type) { case "secret": environmentResult.push({ varName: parameter.name, secret: parameter.name as string, }); resourcesResult.push({ secret: { name: parameter.value as string } }); break; case "file": parametersResult.push({ name: "CONFIG_FILE_" + index, type: "string", defaultValue: (parameter.value as string) || "", }); fileSystemResult.push({ path: `${parameter.name}`, param: "CONFIG_FILE_" + index, }); break; case "fileContent": parametersResult.push({ name: "CONFIG_FILE_" + index, type: "string", defaultValue: parameter.content || (parameter.value as string) || "", }); fileSystemResult.push({ path: (parameter.value as string) || `${parameter.name}`, param: "CONFIG_FILE_" + index, }); break; case "volume": if (parameter.content) { resourcesResult.push({ volume: { name: parameter.name } }); fileSystemResult.push({ path: parameter.content, resourceVolume: parameter.name, }); } else { resourcesResult.push({ volume: { name: parameter.name } }); fileSystemResult.push({ path: parameter.value as string, resourceVolume: parameter.name, }); } break; case "string": parametersResult.push({ name: parameter.name, type: "string", defaultValue: parameter.value as string, }); environmentResult.push({ varName: parameter.name, param: parameter.name, }); break; case "number": parametersResult.push({ name: parameter.name, type: "number", defaultValue: Number(parameter.value), }); environmentResult.push({ varName: parameter.name, param: parameter.name, }); break; case "boolean": case "bool": parametersResult.push({ name: parameter.name, type: "bool", defaultValue: parameter.value === "true" || parameter.value === true, }); environmentResult.push({ varName: parameter.name, param: parameter.name, }); break; case "serviceUrl": parametersResult.push({ name: parameter.name, type: "string", defaultValue: parameter.value as string, }); environmentResult.push({ varName: parameter.name, param: parameter.name, }); break; default: console.warn(`Unknown parameter type: ${parameter.type}`); parametersResult.push({ name: parameter.name, type: "string", defaultValue: parameter.value?.toString() || "", }); environmentResult.push({ varName: parameter.name, param: parameter.name, }); break; } }); formResources.forEach((resource) => { switch (resource.type) { case "secret": resourcesResult.push({ secret: { name: resource.name } }); environmentResult.push({ varName: resource.name, secret: resource.name as string, }); break; case "volume": resourcesResult.push({ volume: { name: resource.name } }); fileSystemResult.push({ path: resource.key || "", resourceVolume: resource.name, }); break; // case "certificate": // resourcesResult.push({ certificate: { name: resource.name } }); // break; // case "domain": // resourcesResult.push({ domain: { name: resource.name } }); // break; // case "port": // resourcesResult.push({ port: { name: resource.name } }); // break; default: console.warn(`Unknown resource type: ${resource.type}`); break; } }); return { parameters: parametersResult, environment: environmentResult, fileSystem: fileSystemResult, resources: resourcesResult, }; } /** * Function to transform the Service object to a ServiceSpecForm object. * @param service Service object with the data of the service. * @returns A ServiceSpecForm object with the data of the service. */ export function transformServiceToForm(service: Service): ServiceSpecForm { return { tenantId: service.tenant, accountId: service.account, environmentId: service.environment, serviceId: service.name, cpuRequirements: service.usage.limit.cpu.max, memoryRequirements: service.usage.limit.memory.max, registryUrl: service.registry, registryCredentialsSecret: service.registryCredentialSecret, imageTag: service.imageName, clientChannels: service.clientChannels.map((ch) => ({ name: ch.name })), channels: service.serverChannels.map((ch) => ({ channelName: ch.name, containerPort: ch.port ?? 0, isPublic: ch.isPublic ?? false, protocol: ch.protocol === "http" ? "HTTPS" : ch.protocol === "tcp" ? "TCP" : "TCP", publicPort: ch.portNum?.toString() || "", domain: ch.portNum?.toString() || "", certificateResource: ch.certificateResource, withMtls: ch.withMtls, caResource: ch.caResource, })), clientChannelsExtra: service.duplexChannels.map((ch) => ({ name: ch.name, })), defaultExecutable: { cmd: service.cmd, entryPoint: service.entrypoint, }, config: { parameters: service.parameters.map((param) => ({ name: param.name, value: param.value?.toString() || "", type: param.type, })), resources: service.resources.map((r) => ({ name: r.name, type: r.type, size: r.maxItems, value: r.value, kind: r.kind, key: r.key, })), }, environment: [service.environment], fileSystem: service.resources .filter((resource) => resource.type === "volume") .map((resource) => ({ path: resource.key, resourceVolume: resource.name, })), scaling: service.role[0].scalling, hsize: service.role[0].hsize }; } /** * Function to generate a ServiceWithLocalComponentSpec object from a ServiceSpecForm object. * @param form ServiceSpecForm object with the data of the service. * @returns ServiceWithLocalComponentSpec object with the data of the service. */ export async function generateServiceSpec( form: ServiceSpecForm, marketplaceItem?: MarketplaceService, ): Promise { const formParams = form.config.parameters; const formResources = form.config.resources; const { parameters, environment, fileSystem, resources: componentResources, } = handleParametersToGenerateData(formParams, formResources); const serverConfigDomainResources: ArtifactConfigResource[] = form.channels .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic) .map((channel) => ({ domain: { name: `${channel.channelName}_domain` } })); const hasDefaultCertChannel = form.channels.some( (ch) => ch.protocol === "HTTPS" && ch.isPublic && !ch.certificateResource, ); if (hasDefaultCertChannel) { serverConfigDomainResources.push({ certificate: { name: "main_inbound_servercert" }, }); } const customCertResources: ArtifactConfigResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.certificateResource, ) .map((ch) => ({ certificate: { name: `${ch.channelName}_cert` } })); const customCaResources: ArtifactConfigResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.withMtls && ch.caResource, ) .map((ch) => ({ ca: { name: `${ch.channelName}_ca` } })); const serviceConfigPortResources: ArtifactConfigResource[] = form.channels .filter((channel) => channel.protocol === "TCP" && channel.isPublic) .map((channel) => ({ port: { name: `${channel.channelName}_port` } })); const serviceConfigResources: ArtifactConfigResource[] = form.config.resources .filter( (resource) => resource.type === "volume" || resource.type === "secret", ) .map((resource) => { if (resource.type === "secret") { return { secret: { name: resource.name } }; } if (resource.type === "volume") { return { volume: { name: resource.name } }; } return { secret: { name: "" } }; }); const volatileVolumeResources: ArtifactConfigResource[] = form.config.parameters .filter((param) => param.type === "volume" && param.size) .map((param) => ({ volume: { name: param.name } })); const roleVolatileVolumeResources: ResourceBundle[] = form.config.parameters .filter((param) => param.type === "volume" && param.size) .map((param) => ({ volume: { name: param.name, configResource: param.name }, })); const rolesParameters: Parameter[] = form.config.parameters .filter( (resource) => resource.type === "string" || resource.type === "boolean" || resource.type === "number" || resource.type === "fileContent", ) .map((resource) => { if (resource.type === "string" || resource.type === "fileContent") { return { name: resource.name, type: "string", configParam: resource.name, }; } if (resource.type === "boolean") { return { name: resource.name, type: "bool", configParam: resource.name, }; } if (resource.type === "number") { return { name: resource.name, type: "number", configParam: resource.name, }; } return { name: resource.name, type: "string", configParam: resource.name, }; }); const rolesResources: ResourceBundle[] = form.config.resources .filter( (resource) => resource.type === "volume" || resource.type === "secret", ) .map((resource) => { if (resource.type === "secret") { return { secret: { name: resource.name, configResource: resource.name }, }; } if (resource.type === "volume") { return { volume: { name: resource.name, configResource: resource.name }, }; } return { secret: { name: "", configResource: "" } }; }); const roles: Role[] = form.channels .filter((channel) => channel.isPublic) .map((channel) => { if (channel.protocol === "HTTPS") { return { name: `${channel.channelName}_inbound`, artifact: { artifactKind: "service", moduleDomain: "kumori.systems", moduleName: "builtins/inbound", moduleVersion: [1, 3, 0], artifactName: "", config: { parameters: [ { name: "type", value: "https", type: "string" }, // { name: "websocket", value: "true", type: "bool", configParam: "websocket" }, ], resources: [ { certificate: { name: "servercert", configResource: channel.certificateResource ? `${channel.channelName}_cert` : "main_inbound_servercert", }, }, { domain: { name: "serverdomain", configResource: `${channel.channelName}_domain`, }, }, ...(channel.withMtls && channel.caResource ? [ { ca: { name: "clientcertca", configResource: `${channel.channelName}_ca`, }, }, ] : []), ], }, }, }; } return { name: `${channel.channelName}_inbound`, artifact: { artifactKind: "service", moduleDomain: " ", moduleName: "builtins/inbound", moduleVersion: [1, 3, 0], artifactName: "", config: { parameters: [{ name: "type", value: "tcp", type: "string" }], resources: [ { port: { name: "port", configResource: `${channel.channelName}_port`, }, }, ], }, }, }; }); const topologyServerChannels: Connector[] = form.channels.flatMap( (channel) => { const result: Connector[] = [ { clientRole: "self", clientChannel: channel.channelName, serverRole: withDefaultValue(form.serviceId, ""), serverChannel: channel.channelName, }, ]; if (channel.isPublic) { result.push({ clientRole: `${channel.channelName}_inbound`, clientChannel: "inbound", serverRole: withDefaultValue(form.serviceId, ""), serverChannel: channel.channelName, }); } return result; }, ); const topologyClientsChannels: Connector[] = (form.clientChannels || []).map( (channel) => ({ clientRole: withDefaultValue(form.serviceId, ""), clientChannel: channel.name, serverRole: "self", serverChannel: channel.name, }), ); const topology: Connector[] = [ ...topologyServerChannels, ...topologyClientsChannels, ]; const deploymentConfigParameters: DeploymentParameter[] = form.config.parameters .filter( (resource) => resource.type === "string" || resource.type === "number" || resource.type === "boolean" || resource.type === "fileContent", ) .map((resource) => { if (resource.type === "string") { return { name: resource.name, value: resource.value, type: "string" }; } if (resource.type === "fileContent") { return { name: resource.name, value: resource.content || "", type: "string", }; } if (resource.type === "boolean") { return { name: resource.name, value: resource.value.toString(), type: "bool", }; } if (resource.type === "number") { return { name: resource.name, value: resource.value, type: "number" }; } return { name: resource.name, value: resource.value, type: "string" }; }); const createdVolatileVolumes = new Map(); if (marketplaceItem) { //TODO: Quitar cuando se soporten los volumenes inline? const volatileResources = form.config.resources.filter( (resource) => resource.type === "volume" && resource.kind === "volatile" && resource.size, ); for (const resource of volatileResources) { try { const volumeName = `${form.tenantId}-${form.serviceId}-${resource.name}`; const volumeResource: Resource = { name: volumeName, kind: "volatile", value: String(resource.size || 1), type: "volume", maxItems: resource.size, }; await createVolume(form.tenantId, volumeResource, ""); createdVolatileVolumes.set(resource.name, volumeName); } catch (error) { console.error( `Error creating volatile volume for ${resource.name}:`, error, ); throw error; } } } const deploymentConfigResources: DeploymentResource[] = form.config.resources .filter( (resource) => resource.type === "secret" || resource.type === "volume", ) .map((resource) => { if (resource.type === "secret") { return { secret: { name: resource.name, resource: withDefaultValue(resource.value, ""), }, }; } if (resource.size) { const volType = (resource as any).kind || "nonreplicated"; switch (volType) { case "persistent": return { volume: { name: resource.name, volume: { size: (resource.size || 0) * 1000, unit: "M" as const, type: "persistent" as const, }, }, } as { volume: { name: string; volume: { size: number; unit: "M"; type: "persistent" }; }; }; case "volatile": if (createdVolatileVolumes.has(resource.name)) { return { volume: { name: resource.name, resource: createdVolatileVolumes.get(resource.name)!, }, }; } return { volume: { name: resource.name, volume: { size: (resource.size || 0) * 1000, unit: "M" as const, type: "volatile" as const, }, }, } as { volume: { name: string; volume: { size: number; unit: "M"; type: "volatile" }; }; }; case "nonreplicated": default: return { volume: { name: resource.name, volume: { size: (resource.size || 0) * 1000, unit: "M" as const, type: "nonreplicated" as const, }, }, } as { volume: { name: string; volume: { size: number; unit: "M"; type: "nonreplicated" }; }; }; } } return { volume: { name: resource.name, resource: withDefaultValue(resource.value, ""), }, }; }); const deploymentConfigDomain: DeploymentResource[] = form.channels .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic) .map((channel) => ({ domain: { name: `${channel.channelName}_domain`, resource: withDefaultValue(channel.domain, ""), }, })); const hasDefaultCertChannelDeployment = form.channels.some( (ch) => ch.protocol === "HTTPS" && ch.isPublic && !ch.certificateResource, ); if (hasDefaultCertChannelDeployment) { deploymentConfigDomain.push({ certificate: { name: "main_inbound_servercert", resource: `wildcard-${form.tenantId}-domain-${getReferenceDomain()}`, }, }); } const customCertDeploymentResources: DeploymentResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.certificateResource, ) .map((ch) => ({ certificate: { name: `${ch.channelName}_cert`, resource: ch.certificateResource!, }, })); const customCaDeploymentResources: DeploymentResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.withMtls && ch.caResource, ) .map((ch) => ({ ca: { name: `${ch.channelName}_ca`, resource: ch.caResource!, }, })); const deploymentConfigPort: DeploymentResource[] = form.channels .filter((channel) => channel.protocol === "TCP" && channel.isPublic) .map((channel) => ({ port: { name: `${channel.channelName}_port`, resource: withDefaultValue(channel.publicPort, ""), }, })); const component: ComponentSpec = { type: "component", tenantId: withDefaultValue(form.tenantId, ""), accountId: withDefaultValue(form.accountId, ""), environmentId: withDefaultValue(form.environmentId, ""), deploymentId: "", moduleDomain: "kumori.examples", moduleId: withDefaultValue(form.serviceId, ""), moduleVersion: [0, 0, 1], labels: {}, bundleTargetDir: "", bundleTargetKind: "", channels: { client: form.clientChannels.map((ch) => ch.name) ?? [], server: form.channels .filter((ch) => ch.channelName) .map((ch) => { if (ch.containerPort === 0) { return { name: ch.channelName }; } else return { name: ch.channelName, port: ch.containerPort }; }) ?? [], }, cpuRequirements: withDefaultValue(form.cpuRequirements, 0) * 1000, memoryRequirements: withDefaultValue(form.memoryRequirements, 0) * 1000, registryUrl: withDefaultValue(form.registryUrl, "docker.io").trim(), imageTag: withDefaultValue(form.imageTag, "").trim(), registryCredentialsSecret: form.config.resources.map((r) => { if (r.type === "secret" && r.key === "registryCredentials") { return r.name; } })[0], config: { parameters, resources: componentResources, }, environment: environment, filesystem: fileSystem ?? [], defaultExecutable: { cmd: withDefaultValue(form.defaultExecutable.cmd, undefined), entrypoint: withDefaultValue( form.defaultExecutable.entryPoint, undefined, ), }, }; 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[form.serviceId] = { scale_up: { cpu: parseInt(form.scaling?.cpu.up || "") || 0, memory: parseInt(form.scaling?.memory.up || "") || 0, }, scale_down: { cpu: parseInt(form.scaling?.cpu.down || "") || 0, memory: parseInt(form.scaling?.memory.down || "") || 0, }, hysteresis: parseInt(form.scaling?.histeresys || "") || 0, min_replicas: form.scaling?.instances.min || 0, max_replicas: form.scaling?.instances.max || 0, }; const autoscalingDefined = form.scaling; const serviceSpec: ServiceWithLocalComponentSpec = { type: "service", subtype: "service_with_local_component", tenantId: withDefaultValue(form.tenantId, ""), accountId: withDefaultValue(form.accountId, ""), environmentId: withDefaultValue(form.environmentId, ""), deploymentId: "", moduleDomain: "kumori.examples", moduleId: withDefaultValue(form.serviceId, ""), moduleVersion: [0, 0, 1], labels: {}, bundleTargetDir: "", bundleTargetKind: "", channels: { client: form.clientChannels.map((ch) => ch.name) ?? [], server: form.channels .filter((ch) => ch.channelName) .map((ch) => { if (ch.containerPort === 0) { return { name: ch.channelName }; } else return { name: ch.channelName, port: ch.containerPort }; }) ?? [], }, config: { parameters, resources: [ ...serverConfigDomainResources, ...customCertResources, ...customCaResources, ...serviceConfigPortResources, ...serviceConfigResources, ...volatileVolumeResources, ], }, roles: [ marketplaceItem !== null && marketplaceItem !== undefined ? { name: withDefaultValue(marketplaceItem.deploymentData.name, ""), artifact: { artifactKind: marketplaceItem.type === "service" ? "service" : "component", moduleDomain: withDefaultValue(marketplaceItem.domain, ""), moduleName: withDefaultValue(marketplaceItem.module, ""), artifactName: withDefaultValue(marketplaceItem.serviceName, ""), moduleVersion: [ Number(marketplaceItem.version.split(".")[0]), Number(marketplaceItem.version.split(".")[1]), Number(marketplaceItem.version.split(".")[2]), ] as [number, number, number], config: { parameters: [...rolesParameters], resources: [...rolesResources, ...roleVolatileVolumeResources], ...(marketplaceItem.type === "service" && { scale: { detail: (marketplaceItem.roles ?? []).reduce( (acc, role) => ({ ...acc, [withDefaultValue(role, "")]: (marketplaceItem.deploymentData.role ?? []).find((r) => r.name === withDefaultValue(role, ""))?.hsize || 1, }), {}, ), }, }), }, }, } : { name: withDefaultValue(form.serviceId, ""), artifact: { artifactKind: "component" as const, moduleDomain: "mod.local", moduleName: "", artifactName: withDefaultValue(form.serviceId, ""), moduleVersion: [0, 0, 0] as [number, number, number], config: { parameters: [...rolesParameters], resources: [...rolesResources, ...roleVolatileVolumeResources], }, }, }, ...roles, ], topology, local_components: marketplaceItem !== null && marketplaceItem !== undefined ? {} : { [withDefaultValue(form.serviceId, "")]: component, }, deploymentConfig: { parameters: [...deploymentConfigParameters], resources: [ ...deploymentConfigDomain, ...customCertDeploymentResources, ...customCaDeploymentResources, ...deploymentConfigPort, ...deploymentConfigResources, ], scale: { detail: { [withDefaultValue(form.serviceId, "")]: form.hsize || 1, }, }, meta: autoscalingDefined ? { scaling: scaling, } : {}, }, }; return serviceSpec; } /** * Function to generate a ServiceSpecDSL for marketplace items with package. * @param form ServiceSpecForm object with the data of the service. * @param marketplaceItem MarketplaceService object with marketplace data. * @returns ServiceSpecDSL object for buildServiceDeploymentModule. */ async function generateServiceSpecDSL( form: ServiceSpecForm, marketplaceItem?: MarketplaceService, ): Promise { const formParams = form.config.parameters; const formResources = form.config.resources; const { parameters, environment, fileSystem, resources: componentResources, } = handleParametersToGenerateData(formParams, formResources); const serverConfigDomainResources: ArtifactConfigResource[] = form.channels .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic) .map((channel) => ({ domain: { name: `${channel.channelName}_domain` } })); const hasDefaultCertChannel = form.channels.some( (ch) => ch.protocol === "HTTPS" && ch.isPublic && !ch.certificateResource, ); if (hasDefaultCertChannel) { serverConfigDomainResources.push({ certificate: { name: "main_inbound_servercert" }, }); } const customCertResources: ArtifactConfigResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.certificateResource, ) .map((ch) => ({ certificate: { name: `${ch.channelName}_cert` } })); const customCaResources: ArtifactConfigResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.withMtls && ch.caResource, ) .map((ch) => ({ ca: { name: `${ch.channelName}_ca` } })); const serviceConfigPortResources: ArtifactConfigResource[] = form.channels .filter((channel) => channel.protocol === "TCP" && channel.isPublic) .map((channel) => ({ port: { name: `${channel.channelName}_port` } })); const serviceConfigResources: ArtifactConfigResource[] = form.config.resources .filter( (resource) => resource.type === "volume" || resource.type === "secret", ) .map((resource) => { if (resource.type === "secret") { return { secret: { name: resource.name } }; } if (resource.type === "volume") { return { volume: { name: resource.name } }; } return { secret: { name: "" } }; }); const volatileVolumeResources: ArtifactConfigResource[] = form.config.parameters .filter((param) => param.type === "volume" && param.size) .map((param) => ({ volume: { name: param.name } })); const rolesParameters: Parameter[] = form.config.parameters .filter( (resource) => resource.type === "string" || resource.type === "boolean" || resource.type === "number" || resource.type === "file", ) .map((resource, index) => { if (resource.type === "string") { return { name: resource.name, type: "string", configParam: resource.name, }; } if (resource.type === "file") { return { name: "CONFIG_FILE_" + index, type: "string", configParam: "CONFIG_FILE_" + index, }; } if (resource.type === "boolean" || resource.type === "bool") { return { name: resource.name, type: "bool", configParam: resource.name, }; } if (resource.type === "number") { return { name: resource.name, type: "number", configParam: resource.name, }; } return { name: resource.name, type: "string", configParam: resource.name, }; }); const rolesResources: ResourceBundle[] = form.config.resources .filter( (resource) => resource.type === "volume" || resource.type === "secret", ) .map((resource) => { if (resource.type === "secret") { return { secret: { name: resource.name, configResource: resource.name || "" }, }; } return { volume: { name: resource.name, configResource: resource.name } }; }); const inboundRoles = form.channels .filter((channel) => channel.isPublic) .map((channel) => { if (channel.protocol === "HTTPS") { return { name: `${channel.channelName}_inbound`, artifact: { artifactKind: "service" as const, moduleDomain: "kumori", moduleName: "builtin", moduleVersion: [1, 3, 0] as [number, number, number], artifactName: "HTTPInbound", packageLocation: "", config: { parameters: [ { name: "type", value: "https", type: "string" as const }, ], resources: [ { certificate: { name: "servercert", configResource: channel.certificateResource ? `${channel.channelName}_cert` : "main_inbound_servercert", }, }, { domain: { name: "serverdomain", configResource: `${channel.channelName}_domain`, }, }, ...(channel.withMtls && channel.caResource ? [ { ca: { name: "clientcertca", configResource: `${channel.channelName}_ca`, }, }, ] : []), ], }, }, }; } return { name: `${channel.channelName}_inbound`, artifact: { artifactKind: "service" as const, moduleDomain: "kumori", moduleName: "builtin", moduleVersion: [1, 3, 0] as [number, number, number], artifactName: "TCPInbound", packageLocation: "", config: { parameters: [ { name: "type", value: "tcp", type: "string" as const }, ], resources: [ { port: { name: "port", configResource: `${channel.channelName}_port`, }, }, ], }, }, }; }); const topologyServerChannels = form.channels.flatMap((channel) => { const result = [ { clientRole: "self", clientChannel: channel.channelName, serverRole: withDefaultValue(form.serviceId, ""), serverChannel: channel.channelName, }, ]; if (channel.isPublic) { result.push({ clientRole: `${channel.channelName}_inbound`, clientChannel: "inbound", serverRole: withDefaultValue(form.serviceId, ""), serverChannel: channel.channelName, }); } return result; }); const topologyClientsChannels = (form.clientChannels || []).map( (channel) => ({ clientRole: withDefaultValue(form.serviceId, ""), clientChannel: channel.name, serverRole: "self", serverChannel: channel.name, }), ); const topology = [...topologyServerChannels, ...topologyClientsChannels]; const deploymentConfigParameters: DeploymentParameter[] = form.config.parameters .filter( (resource) => resource.type === "string" || resource.type === "number" || resource.type === "boolean" || resource.type === "bool" || resource.type === "file", ) .map((resource, index) => { if (resource.type === "string") { return { name: resource.name, value: resource.value, type: "string" }; } if (resource.type === "file") { return { name: "CONFIG_FILE_" + index, value: resource.value || "", type: "string", }; } if (resource.type === "boolean" || resource.type === "bool") { return { name: resource.name, value: resource.value.toString(), type: "bool", }; } if (resource.type === "number") { return { name: resource.name, value: resource.value, type: "number" }; } return { name: resource.name, value: resource.value, type: "string" }; }); const deploymentConfigResources: DeploymentResource[] = form.config.resources .filter( (resource) => resource.type === "secret" || resource.type === "volume", ) .map((resource) => { if (resource.type === "secret") { return { secret: { name: resource.name, resource: withDefaultValue(resource.value, ""), }, }; } if (resource.size) { const volType = (resource as any).kind || "nonreplicated"; switch (volType) { case "persistent": return { volume: { name: resource.name, volume: { size: (resource.size || 0) * 1000, unit: "M" as const, type: "persistent" as const, }, }, }; case "volatile": return { volume: { name: resource.name, volume: { size: (resource.size || 0) * 1000, unit: "M" as const, type: "volatile" as const, }, }, }; case "nonreplicated": default: return { volume: { name: resource.name, volume: { size: (resource.size || 0) * 1000, unit: "M" as const, type: "nonreplicated" as const, }, }, }; } } return { volume: { name: resource.name, resource: withDefaultValue(resource.value, ""), }, }; }); const deploymentConfigDomain: DeploymentResource[] = form.channels .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic) .map((channel) => ({ domain: { name: `${channel.channelName}_domain`, resource: withDefaultValue(channel.domain, ""), }, })); const hasDefaultCertChannelDeployment = form.channels.some( (ch) => ch.protocol === "HTTPS" && ch.isPublic && !ch.certificateResource, ); if (hasDefaultCertChannelDeployment) { deploymentConfigDomain.push({ certificate: { name: "main_inbound_servercert", resource: `wildcard-${form.tenantId}-domain-${getReferenceDomain()}`, }, }); } const customCertDeploymentResources: DeploymentResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.certificateResource, ) .map((ch) => ({ certificate: { name: `${ch.channelName}_cert`, resource: ch.certificateResource!, }, })); const customCaDeploymentResources: DeploymentResource[] = form.channels .filter( (ch) => ch.protocol === "HTTPS" && ch.isPublic && ch.withMtls && ch.caResource, ) .map((ch) => ({ ca: { name: `${ch.channelName}_ca`, resource: ch.caResource!, }, })); const deploymentConfigPort: DeploymentResource[] = form.channels .filter((channel) => channel.protocol === "TCP" && channel.isPublic) .map((channel) => ({ port: { name: `${channel.channelName}_port`, resource: channel.publicPort || "", }, })); 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[form.serviceId] = { scale_up: { cpu: parseInt(form.scaling?.cpu.up || "") || 0, memory: parseInt(form.scaling?.memory.up || "") || 0, }, scale_down: { cpu: parseInt(form.scaling?.cpu.down || "") || 0, memory: parseInt(form.scaling?.memory.down || "") || 0, }, hysteresis: parseInt(form.scaling?.histeresys || "") || 0, min_replicas: form.scaling?.instances.min || 0, max_replicas: form.scaling?.instances.max || 0, }; const autoscalingDefined = form.scaling; const hasMarketplacePackage = marketplaceItem?.package; const serviceSpec: ServiceSpecDSLWithLocalComponent = { type: "service" as const, subtype: marketplaceItem ? "" : "service_with_local_component", packageLocation: "deployment", artifactName: withDefaultValue( marketplaceItem?.artifact || form.serviceId + "_service", "", ), tenantId: withDefaultValue(form.tenantId, ""), accountId: withDefaultValue(form.accountId, ""), environmentId: withDefaultValue(form.environmentId, ""), deploymentId: "", moduleDomain: "kumori.examples", moduleId: withDefaultValue(marketplaceItem?.module || form.serviceId, ""), moduleVersion: [0, 0, 1] as [number, number, number], labels: {}, bundleTargetDir: "", bundleTargetKind: "", channels: { client: (form.clientChannels || []).map((ch) => ch.name), server: form.channels .filter((ch) => ch.channelName) .map((ch) => { if (ch.containerPort === 0) { return { name: ch.channelName }; } else { return { name: ch.channelName, port: ch.containerPort }; } }), }, config: { parameters, resources: [ ...serverConfigDomainResources, ...customCertResources, ...customCaResources, ...serviceConfigPortResources, ...serviceConfigResources, ...volatileVolumeResources, ], }, roles: [ { name: withDefaultValue( marketplaceItem?.deploymentData?.name || form.serviceId, "", ), artifact: { artifactKind: marketplaceItem?.type === "service" ? ("service" as const) : ("component" as const), moduleDomain: hasMarketplacePackage ? marketplaceItem?.domain || "kumori.systems" : "mod.local", moduleName: withDefaultValue( marketplaceItem?.module || form.serviceId, "", ), moduleVersion: marketplaceItem?.version ? (marketplaceItem.version.split(".").map(Number) as [ number, number, number, ]) : ([0, 0, 1] as [number, number, number]), artifactName: withDefaultValue( marketplaceItem?.artifact || form.serviceId + "_component", "", ), packageLocation: hasMarketplacePackage ? marketplaceItem?.package || "" : "deployment", config: { parameters: [...rolesParameters], resources: [...rolesResources], }, }, }, ...inboundRoles, ], topology, deploymentConfig: { parameters: [...deploymentConfigParameters], resources: [ ...deploymentConfigDomain, ...customCertDeploymentResources, ...customCaDeploymentResources, ...deploymentConfigPort, ...deploymentConfigResources, ], scale: { detail: { [withDefaultValue(form.serviceId, "")]: form.hsize || 1, }, }, meta: { deploymentMeta: { environment: form.environment || [], team: "development", scaling: autoscalingDefined ? scaling : {} }, }, }, }; if (!marketplaceItem) { const localComponent: ComponentSpecDSL = { type: "component", artifactName: withDefaultValue(form.serviceId + "_component", ""), packageLocation: "deployment", tenantId: withDefaultValue(form.tenantId, ""), accountId: withDefaultValue(form.accountId, ""), environmentId: withDefaultValue(form.environmentId, ""), deploymentId: "", moduleDomain: "kumori.examples", moduleId: withDefaultValue(form.serviceId, ""), moduleVersion: [0, 0, 1], labels: {}, bundleTargetDir: "", bundleTargetKind: "", channels: { client: (form.clientChannels || []).map((ch) => ch.name), server: form.channels .filter((ch) => ch.channelName) .map((ch) => { if (ch.containerPort === 0) { return { name: ch.channelName }; } else { return { name: ch.channelName, port: ch.containerPort }; } }), }, cpuRequirements: withDefaultValue(form.cpuRequirements, 0) * 1000, memoryRequirements: withDefaultValue(form.memoryRequirements, 0) * 1000, registryUrl: withDefaultValue(form.registryUrl, "docker.io").trim(), imageTag: withDefaultValue(form.imageTag, "").trim(), registryCredentialsSecret: form.registryCredentialsSecret ? form.config.resources.find( (r) => r.type === "secret" && r.name === form.registryCredentialsSecret, )?.name : form.config.resources.find( (r) => r.type === "secret" && r.key === "registryCredentials", )?.name, config: { parameters, resources: componentResources, }, environment: environment, filesystem: fileSystem ?? [], defaultExecutable: { cmd: withDefaultValue(form.defaultExecutable.cmd, undefined), entrypoint: withDefaultValue( form.defaultExecutable.entryPoint, undefined, ), }, }; serviceSpec.local_components = { [withDefaultValue(form.serviceId + "_component", "")]: localComponent, }; } return serviceSpec; } /** * Function to deploy a service. * @param service Service object with the data of the service. * @returns The body of the request to deploy the service. */ export async function deployServiceHelper( service: Service, marketplaceItem?: MarketplaceService, ): Promise { const serviceForm: ServiceSpecForm = transformServiceToForm(service); let CUEBundle; const serviceSpecDSL = await generateServiceSpecDSL( serviceForm, marketplaceItem, ); CUEBundle = buildServiceDeploymentModule(serviceSpecDSL); // if (marketplaceItem?.package) { // } else { // const serviceSpec = await generateServiceSpec(serviceForm, marketplaceItem); // CUEBundle = await buildServiceDeploymentModuleWithLocalComponent( // serviceSpec // ); // } const resolvedCUEBundle = await CUEBundle; //downloadZipDev(resolvedCUEBundle, true); if (service.download) { downloadZipDev(resolvedCUEBundle, true); return new FormData(); } else { const formData = new FormData(); formData.append("bundle", resolvedCUEBundle); formData.append( "meta", JSON.stringify({ targetAccount: withDefaultValue(serviceForm.accountId, ""), targetEnvironment: withDefaultValue(serviceForm.environmentId, ""), }), ); formData.append( "labels", JSON.stringify({ project: withDefaultValue(service.project, "") }), ); formData.append("comment", " "); return formData; } } function downloadZipDev(bundle: Blob, download: boolean) { if (!download) { return; } const url = URL.createObjectURL(bundle); const downloadLink: HTMLAnchorElement = document.createElement("a"); downloadLink.target = "_blank"; downloadLink.href = url; downloadLink.click(); }