import type { Coreflow } from './coreflow.classes.coreflow.js'; import * as plugins from './coreflow.plugins.js'; import { logger } from './coreflow.logging.js'; type TPlatformDesiredState = { capabilities: plugins.servezoneInterfaces.platform.IPlatformCapability[]; providerConfigs: plugins.servezoneInterfaces.platform.IPlatformProviderConfig[]; bindings: plugins.servezoneInterfaces.platform.IPlatformBinding[]; services?: plugins.servezoneInterfaces.data.IService[]; }; export class PlatformManager { public coreflowRef: Coreflow; private configSubscription?: { unsubscribe: () => void }; private currentDesiredState?: TPlatformDesiredState; constructor(coreflowRefArg: Coreflow) { this.coreflowRef = coreflowRefArg; } public async start() { await this.reconcilePlatformServices(); this.configSubscription = this.coreflowRef.cloudlyConnector.cloudlyApiClient.configUpdateSubject.subscribe( async (configUpdateArg) => { try { await this.reconcilePlatformServices({ providerConfigs: configUpdateArg.platformProviderConfigs || [], bindings: configUpdateArg.platformBindings || [], services: configUpdateArg.services || [], }); } catch (error) { logger.log('error', `Platform service reconciliation failed: ${(error as Error).message}`); } }, ); logger.log('info', 'Platform manager started'); } public async stop() { this.configSubscription?.unsubscribe(); logger.log('info', 'Platform manager stopped'); } public async reconcilePlatformServices(desiredStateArg?: Partial) { const desiredState = await this.getDesiredState(desiredStateArg); this.currentDesiredState = desiredState; for (const binding of desiredState.bindings) { await this.reconcileBinding(binding, desiredState); } logger.log('info', `Platform service reconciliation completed for ${desiredState.bindings.length} bindings`); } private async getDesiredState( desiredStateArg: Partial = {}, ): Promise { const platformDesiredState = desiredStateArg.capabilities && desiredStateArg.providerConfigs && desiredStateArg.bindings ? { capabilities: desiredStateArg.capabilities, providerConfigs: desiredStateArg.providerConfigs, bindings: desiredStateArg.bindings, } : await this.coreflowRef.cloudlyConnector.cloudlyApiClient.platform.getPlatformDesiredState(); const services = desiredStateArg.services || ((await this.coreflowRef.cloudlyConnector.cloudlyApiClient.services.getServices()) as unknown as plugins.servezoneInterfaces.data.IService[]); return { capabilities: platformDesiredState.capabilities, providerConfigs: platformDesiredState.providerConfigs, bindings: platformDesiredState.bindings, services, }; } private async reconcileBinding( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, desiredStateArg: TPlatformDesiredState, ) { const service = desiredStateArg.services?.find( (serviceArg) => serviceArg.id === bindingArg.serviceId || serviceArg.data.name === bindingArg.serviceId, ); const capability = desiredStateArg.capabilities.find( (capabilityArg) => capabilityArg.id === bindingArg.capability, ); const providerConfig = this.getProviderConfig(bindingArg, desiredStateArg.providerConfigs); if (bindingArg.desiredState === 'disabled') { await this.updateBindingStatus(bindingArg, { status: 'disabled', endpoints: [], credentials: [], }); return; } if (!capability) { await this.failBinding(bindingArg, `Unknown platform capability ${bindingArg.capability}`); return; } if (!service) { await this.failBinding(bindingArg, `Service ${bindingArg.serviceId} not found for platform binding`); return; } if (!providerConfig) { await this.failBinding(bindingArg, `No enabled provider config found for ${bindingArg.capability}`); return; } if (!providerConfig.enabled) { await this.failBinding(bindingArg, `Provider config ${providerConfig.id} is disabled`); return; } const endpoints = this.getEndpointsForBinding(bindingArg, providerConfig, capability); const credentials = this.getCredentialsForBinding(bindingArg, providerConfig); await this.updateBindingStatus(bindingArg, { status: 'ready', endpoints, credentials, }); } private getProviderConfig( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, providerConfigsArg: plugins.servezoneInterfaces.platform.IPlatformProviderConfig[], ) { if (bindingArg.providerConfigId) { return providerConfigsArg.find((providerConfigArg) => providerConfigArg.id === bindingArg.providerConfigId); } return providerConfigsArg.find( (providerConfigArg) => providerConfigArg.capability === bindingArg.capability && providerConfigArg.enabled, ); } private getEndpointsForBinding( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, providerConfigArg: plugins.servezoneInterfaces.platform.IPlatformProviderConfig, capabilityArg: plugins.servezoneInterfaces.platform.IPlatformCapability, ): plugins.servezoneInterfaces.platform.IPlatformServiceEndpoint[] { const config = { ...(providerConfigArg.config || {}), ...(bindingArg.config || {}), }; const internalUrl = this.getStringConfigValue(config, 'internalUrl'); const externalUrl = this.getStringConfigValue(config, 'externalUrl'); const networkAlias = this.getStringConfigValue(config, 'networkAlias'); const port = this.getNumberConfigValue(config, 'port'); if (!internalUrl && !externalUrl && !networkAlias && !port && capabilityArg.accessMode !== 'rpc') { return bindingArg.endpoints || []; } const protocol = this.getEndpointProtocol(config, bindingArg.capability); return [ { name: this.getStringConfigValue(config, 'endpointName') || providerConfigArg.name, capability: bindingArg.capability, protocol, ...(internalUrl ? { internalUrl } : {}), ...(externalUrl ? { externalUrl } : {}), ...(networkAlias ? { networkAlias } : {}), ...(port ? { port } : {}), }, ]; } private getCredentialsForBinding( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, providerConfigArg: plugins.servezoneInterfaces.platform.IPlatformProviderConfig, ): plugins.servezoneInterfaces.platform.IPlatformCredentialRef[] { if (bindingArg.credentials?.length) { return bindingArg.credentials; } if (!providerConfigArg.secretBundleId) { return []; } return [ { secretBundleId: providerConfigArg.secretBundleId, }, ]; } private async failBinding( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, errorTextArg: string, ) { await this.updateBindingStatus(bindingArg, { status: 'failed', errorText: errorTextArg, }); } private async updateBindingStatus( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, updateArg: Omit< plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_UpdatePlatformBindingStatus['request'], 'identity' | 'bindingId' >, ) { if (this.bindingStatusIsCurrent(bindingArg, updateArg)) { return; } await this.coreflowRef.cloudlyConnector.cloudlyApiClient.platform.updatePlatformBindingStatus({ bindingId: bindingArg.id, ...updateArg, }); } private getStringConfigValue( configArg: { [key: string]: plugins.servezoneInterfaces.platform.TPlatformConfigValue }, keyArg: string, ) { const value = configArg[keyArg]; return typeof value === 'string' ? value : undefined; } private bindingStatusIsCurrent( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, updateArg: Omit< plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_UpdatePlatformBindingStatus['request'], 'identity' | 'bindingId' >, ) { const sameStatus = bindingArg.status === updateArg.status; const sameEndpoints = updateArg.endpoints === undefined || JSON.stringify(bindingArg.endpoints || []) === JSON.stringify(updateArg.endpoints || []); const sameCredentials = updateArg.credentials === undefined || JSON.stringify(bindingArg.credentials || []) === JSON.stringify(updateArg.credentials || []); const sameErrorText = updateArg.errorText === undefined || (bindingArg as { errorText?: string }).errorText === updateArg.errorText; return sameStatus && sameEndpoints && sameCredentials && sameErrorText; } private getNumberConfigValue( configArg: { [key: string]: plugins.servezoneInterfaces.platform.TPlatformConfigValue }, keyArg: string, ) { const value = configArg[keyArg]; return typeof value === 'number' ? value : undefined; } private getEndpointProtocol( configArg: { [key: string]: plugins.servezoneInterfaces.platform.TPlatformConfigValue }, capabilityArg: plugins.servezoneInterfaces.platform.TPlatformCapability, ): plugins.servezoneInterfaces.platform.TPlatformEndpointProtocol { const configuredProtocol = this.getStringConfigValue(configArg, 'protocol'); if (configuredProtocol && this.isEndpointProtocol(configuredProtocol)) { return configuredProtocol; } switch (capabilityArg) { case 'database': return 'mongodb'; case 'objectstorage': return 's3'; case 'email': return 'smtp'; case 'sip': return 'sip'; default: return 'typedrequest'; } } private isEndpointProtocol( protocolArg: string, ): protocolArg is plugins.servezoneInterfaces.platform.TPlatformEndpointProtocol { return [ 'typedrequest', 'http', 'tcp', 'udp', 'smtp', 's3', 'postgres', 'mongodb', 'sip', ].includes(protocolArg); } }