From b221e9fe4306957f3d924eaa16c2c291d115b80a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 28 Apr 2026 12:22:27 +0000 Subject: [PATCH] feat: reconcile platform bindings --- package.json | 4 +- pnpm-lock.yaml | 22 +- ts/coreflow.classes.platformmanager.ts | 270 ++++++++++++++++++++++++- 3 files changed, 281 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 2a7a9a0..d816b8f 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "@push.rocks/smartstream": "^3.4.0", "@push.rocks/smartstring": "^4.1.0", "@push.rocks/taskbuffer": "^8.0.2", - "@serve.zone/api": "^5.3.1", - "@serve.zone/interfaces": "^5.4.4", + "@serve.zone/api": "^5.3.2", + "@serve.zone/interfaces": "^5.4.5", "@tsclass/tsclass": "^9.5.0", "@types/node": "25.6.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2327934..28ed0f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,11 +69,11 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@serve.zone/api': - specifier: ^5.3.1 - version: 5.3.1(@push.rocks/smartserve@2.0.3) + specifier: ^5.3.2 + version: 5.3.2(@push.rocks/smartserve@2.0.3) '@serve.zone/interfaces': - specifier: ^5.4.4 - version: 5.4.4 + specifier: ^5.4.5 + version: 5.4.5 '@tsclass/tsclass': specifier: ^9.5.0 version: 9.5.0 @@ -1516,11 +1516,11 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@serve.zone/api@5.3.1': - resolution: {integrity: sha512-P6f3VWr2ljM8dwEtWYBROSZVtcW1HMc5oiorOCcvDeWY6roJbZobK6UFDlcdop02TxGEneJD+jVKoCwBoBLJVw==} + '@serve.zone/api@5.3.2': + resolution: {integrity: sha512-ETQ4KSNfhDP7O1WxXXLcMn/A+jZtDfd7FjuQ0k3n8tnXG9hExh8ZmqvMwVj8eT2CnXO+xQVlbAgT0HLMLnxCfA==} - '@serve.zone/interfaces@5.4.4': - resolution: {integrity: sha512-0mcLvZBGHOFKG8PkpMieHo19lbPLJIpDIgKUSbuqfqOzSYymS8BLYftCr8Kw7ddl61kICZoy/m09U+TtxZbxBg==} + '@serve.zone/interfaces@5.4.5': + resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -6929,7 +6929,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@serve.zone/api@5.3.1(@push.rocks/smartserve@2.0.3)': + '@serve.zone/api@5.3.2(@push.rocks/smartserve@2.0.3)': dependencies: '@api.global/typedrequest': 3.1.10 '@api.global/typedrequest-interfaces': 3.0.19 @@ -6938,7 +6938,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 '@push.rocks/smartstream': 3.4.0 - '@serve.zone/interfaces': 5.4.4 + '@serve.zone/interfaces': 5.4.5 '@tsclass/tsclass': 9.5.0 transitivePeerDependencies: - '@nuxt/kit' @@ -6949,7 +6949,7 @@ snapshots: - utf-8-validate - vue - '@serve.zone/interfaces@5.4.4': + '@serve.zone/interfaces@5.4.5': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/smartlog-interfaces': 3.0.2 diff --git a/ts/coreflow.classes.platformmanager.ts b/ts/coreflow.classes.platformmanager.ts index 9d1b1dc..4756bbc 100644 --- a/ts/coreflow.classes.platformmanager.ts +++ b/ts/coreflow.classes.platformmanager.ts @@ -1,22 +1,288 @@ 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() { - logger.log('info', 'Platform service reconciliation is not implemented yet'); + 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); } }