import { Resource, Tenant } from "@kumori/aurora-interfaces"; import { decodeRegistryAuth } from "../utils/utils"; type ResourceType = "domain" | "port" | "secret" | "certificate" | "ca" | "volume"; interface HandleResourceEventParams { kind: ResourceType; eventData: any; parentParts: { [entity: string]: string }; tenantsMap: Map; secretsMap: Map; hasRequestingServices: boolean; } interface HandleResourceEventResult { resource: Resource; tenantId: string; tenantFound: boolean; updatedTenant: Tenant | null; pendingResource: Resource | null; publishUpdate: boolean; existingResource: Resource | null; secretsToStore: Array<{ key: string; value: any }>; } /** * Build a domain resource */ const buildDomainResource = ( eventData: any, tenantId: string, hasRequestingServices: boolean ): Resource => { return { type: "domain", name: eventData.id?.name, value: eventData.spec.domain, status: hasRequestingServices ? "used" : "available", tenant: tenantId, }; }; /** * Build a port resource */ const buildPortResource = ( eventData: any, tenantId: string, hasRequestingServices: boolean ): Resource => { return { type: "port", name: eventData.id?.name, value: eventData.spec.port, status: hasRequestingServices ? "used" : "available", tenant: tenantId, }; }; /** * Build a secret resource and extract credentials if applicable */ const buildSecretResource = ( eventData: any, tenantId: string, secretsMap: Map, hasRequestingServices: boolean ): { resource: Resource; secretsToStore: Array<{ key: string; value: any }> } => { const secretName = eventData.id?.name; const secretKey = `${tenantId}/${secretName}`; const secretsToStore: Array<{ key: string; value: any }> = []; secretsToStore.push({ key: secretKey, value: eventData.spec.secret }); if (eventData.spec?.secret) { try { const secretContent = eventData.spec.secret.trim(); if (secretContent.startsWith("{") || secretContent.startsWith("[")) { const parsedSecret = JSON.parse(secretContent); if (parsedSecret?.auths && typeof parsedSecret.auths === "object") { const registryUrl = Object.keys(parsedSecret.auths)[0]; if (registryUrl && parsedSecret.auths[registryUrl]?.auth) { const authString = parsedSecret.auths[registryUrl].auth; const credentials = decodeRegistryAuth(authString); const credentialsKey = `${secretKey}_credentials`; secretsToStore.push({ key: credentialsKey, value: { secretUsername: credentials.username, secretPassword: credentials.password, registryUrl, }, }); } } } } catch (error) { if ( eventData.spec.secret.trim().startsWith("{") || eventData.spec.secret.trim().startsWith("[") ) { console.error("Error parsing secret JSON:", { secretName, error: error, secretContent: eventData.spec.secret, }); } } } const resource: Resource = { type: "secret", name: secretName, value: eventData.spec.secret, status: hasRequestingServices ? "used" : "available", tenant: tenantId, }; return { resource, secretsToStore }; }; /** * Build a certificate resource */ const buildCertificateResource = ( eventData: any, tenantId: string, hasRequestingServices: boolean ): Resource => { return { type: "certificate", name: eventData.id?.name, value: eventData.spec.certificate.cert, key: eventData.spec.certificate.key, domain: eventData.spec.certificate.domain, status: hasRequestingServices ? "used" : "available", tenant: tenantId, certName: eventData.meta?.labels?.certNameWUI || undefined, certKeyName: eventData.meta?.labels?.certKeyWUI || undefined, }; }; /** * Build a CA resource */ const buildCAResource = ( eventData: any, tenantId: string, hasRequestingServices: boolean ): Resource => { return { type: "ca", name: eventData.id?.name, value: eventData.spec.ca, status: hasRequestingServices ? "used" : "available", tenant: tenantId, certName: eventData.meta?.labels?.certNameWUI || undefined, }; }; /** * Build a volume resource */ const buildVolumeResource = ( eventData: any, tenantId: string ): Resource => { return { type: "volume", name: eventData.id?.name, value: eventData.spec.volume.size, kind: eventData.spec.volume.kind === "shared" ? "volatile" : eventData.spec.volume.kind, maxItems: eventData.spec.volume.maxitems, status: "available", tenant: tenantId, }; }; /** * Handles resource events (domain, port, secret, certificate, ca, volume) * from WebSocket messages */ export const handleResourceEvent = ({ kind, eventData, parentParts, tenantsMap, secretsMap, hasRequestingServices, }: HandleResourceEventParams): HandleResourceEventResult => { const tenantId = parentParts.tenant; let resource: Resource; let secretsToStore: Array<{ key: string; value: any }> = []; switch (kind) { case "domain": resource = buildDomainResource(eventData, tenantId, hasRequestingServices); break; case "port": resource = buildPortResource(eventData, tenantId, hasRequestingServices); break; case "secret": const secretResult = buildSecretResource( eventData, tenantId, secretsMap, hasRequestingServices ); resource = secretResult.resource; secretsToStore = secretResult.secretsToStore; break; case "certificate": resource = buildCertificateResource(eventData, tenantId, hasRequestingServices); break; case "ca": resource = buildCAResource(eventData, tenantId, hasRequestingServices); break; case "volume": resource = buildVolumeResource(eventData, tenantId); break; default: throw new Error(`Unknown resource kind: ${kind}`); } const tenant = tenantsMap.get(tenantId); let tenantFound = false; let updatedTenant: Tenant | null = null; let pendingResource: Resource | null = null; let publishUpdate = false; let existingResource: Resource | null = null; if (tenant) { tenantFound = true; const existingIndex = tenant.resources.findIndex( (res) => res.name === resource.name && res.type === resource.type ); if (existingIndex !== -1) { existingResource = tenant.resources[existingIndex]; publishUpdate = true; tenant.resources[existingIndex] = existingResource.deleting ? { ...resource, deleting: true } : resource; } else { tenant.resources.push(resource); } updatedTenant = tenant; } else { pendingResource = resource; console.warn( `Recurso huérfano (${kind}) para tenant ${tenantId}, lo guardo en pendingDomains.` ); } return { resource, tenantId, tenantFound, updatedTenant, pendingResource, publishUpdate, existingResource, secretsToStore, }; }; /** * Handle domain event */ export const handleDomainEvent = ( eventData: any, parentParts: { [entity: string]: string }, tenantsMap: Map, hasRequestingServices: boolean ): HandleResourceEventResult => { return handleResourceEvent({ kind: "domain", eventData, parentParts, tenantsMap, secretsMap: new Map(), hasRequestingServices, }); }; /** * Handle port event */ export const handlePortEvent = ( eventData: any, parentParts: { [entity: string]: string }, tenantsMap: Map, hasRequestingServices: boolean ): HandleResourceEventResult => { return handleResourceEvent({ kind: "port", eventData, parentParts, tenantsMap, secretsMap: new Map(), hasRequestingServices }); }; /** * Handle secret event */ export const handleSecretEvent = ( eventData: any, parentParts: { [entity: string]: string }, tenantsMap: Map, secretsMap: Map, hasRequestingServices: boolean = false, ): HandleResourceEventResult => { return handleResourceEvent({ kind: "secret", eventData, parentParts, tenantsMap, secretsMap, hasRequestingServices }); }; /** * Handle certificate event */ export const handleCertificateEvent = ( eventData: any, parentParts: { [entity: string]: string }, tenantsMap: Map, hasRequestingServices: boolean = false, ): HandleResourceEventResult => { return handleResourceEvent({ kind: "certificate", eventData, parentParts, tenantsMap, secretsMap: new Map(), hasRequestingServices }); }; /** * Handle CA event */ export const handleCAEvent = ( eventData: any, parentParts: { [entity: string]: string }, tenantsMap: Map, hasRequestingServices: boolean = false, ): HandleResourceEventResult => { return handleResourceEvent({ kind: "ca", eventData, parentParts, tenantsMap, secretsMap: new Map(), hasRequestingServices }); }; /** * Handle volume event */ export const handleVolumeEvent = ( eventData: any, parentParts: { [entity: string]: string }, tenantsMap: Map ): HandleResourceEventResult => { return handleResourceEvent({ kind: "volume", eventData, parentParts, tenantsMap, secretsMap: new Map(), hasRequestingServices: false }); }; export const setResourceDeleting = ( tenantsMap: Map, tenantName: string, resourceName: string, resourceType: string, ) => { const tenant = tenantsMap.get(tenantName); if (!tenant) return; const index = tenant.resources.findIndex( (res: Resource) => res.name === resourceName && res.type === resourceType, ); if (index === -1) return; tenant.resources[index] = { ...tenant.resources[index], deleting: true }; tenantsMap.set(tenantName, tenant); }; export const processResourceResult = ( result: any, tenantsMap: Map, pendingDomains: any[], eventHelper: any, ) => { if (result.tenantFound && result.updatedTenant) { if (result.publishUpdate && result.existingResource) { eventHelper.resource.publish.updated(result.existingResource); } tenantsMap.set(result.tenantId, result.updatedTenant); } else if (result.pendingResource) { pendingDomains.push(result.pendingResource); } };