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[]; }; type TCoreStoreProvisionResponse = { serviceId: string; serviceName?: string; resources: Array<{ capability: 'database' | 'objectstorage'; provider: 'smartdb' | 'smartstorage'; resourceName: string; env: Record; }>; env: Record; }; 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`); } public async provisionBindingsForService( serviceArg: plugins.servezoneInterfaces.data.IService, ): Promise> { const desiredState = this.currentDesiredState || (await this.getDesiredState()); this.currentDesiredState = desiredState; const bindings = desiredState.bindings.filter((bindingArg) => { return ( bindingArg.desiredState !== 'disabled' && this.bindingMatchesService(bindingArg, serviceArg) && this.isCoreStoreCapability(bindingArg.capability) ); }); const env: Record = {}; for (const binding of bindings) { const providerConfig = this.getProviderConfig(binding, desiredState.providerConfigs); const provisionedEnv = await this.provisionCoreStoreBinding(binding, serviceArg, providerConfig); Object.assign(env, provisionedEnv); } return env; } 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 (this.isCoreStoreCapability(bindingArg.capability)) { try { await this.provisionCoreStoreBinding(bindingArg, service, providerConfig); } catch (error) { await this.failBinding(bindingArg, `CoreStore provisioning failed: ${(error as Error).message}`); } 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 bindingMatchesService( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, serviceArg: plugins.servezoneInterfaces.data.IService, ) { return bindingArg.serviceId === serviceArg.id || bindingArg.serviceId === serviceArg.data.name; } private isCoreStoreCapability( capabilityArg: plugins.servezoneInterfaces.platform.TPlatformCapability, ): capabilityArg is 'database' | 'objectstorage' { return capabilityArg === 'database' || capabilityArg === 'objectstorage'; } private getCoreStoreControlUrl( providerConfigArg?: plugins.servezoneInterfaces.platform.IPlatformProviderConfig, ) { const configuredUrl = this.getStringConfigValue(providerConfigArg?.config || {}, 'controlUrl'); return configuredUrl || process.env.CORESTORE_CONTROL_URL || 'http://corestore:3000'; } private getCoreStoreApiToken( providerConfigArg?: plugins.servezoneInterfaces.platform.IPlatformProviderConfig, ) { return this.getStringConfigValue(providerConfigArg?.config || {}, 'apiToken') || process.env.CORESTORE_API_TOKEN; } private async provisionCoreStoreBinding( bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding, serviceArg: plugins.servezoneInterfaces.data.IService, providerConfigArg?: plugins.servezoneInterfaces.platform.IPlatformProviderConfig, ): Promise> { if (!this.isCoreStoreCapability(bindingArg.capability)) { throw new Error(`CoreStore cannot provision ${bindingArg.capability}`); } const capability = bindingArg.capability; const controlUrl = this.getCoreStoreControlUrl(providerConfigArg); const response = await this.postCoreStore( `${controlUrl.replace(/\/+$/, '')}/resources/provision`, { serviceId: serviceArg.id, serviceName: serviceArg.data.name, capabilities: [capability], }, providerConfigArg, ); const resource = response.resources.find((resourceArg) => resourceArg.capability === capability); if (!resource) { throw new Error(`CoreStore did not return a ${capability} resource`); } await this.updateBindingStatus(bindingArg, { status: 'ready', endpoints: [this.getCoreStoreEndpoint(capability, resource.env)], credentials: [{ env: resource.env }], errorText: '', }); return resource.env; } private getCoreStoreEndpoint( capabilityArg: 'database' | 'objectstorage', envArg: Record, ): plugins.servezoneInterfaces.platform.IPlatformServiceEndpoint { if (capabilityArg === 'database') { return { name: 'corestore-smartdb', capability: 'database', protocol: 'mongodb', internalUrl: envArg.MONGODB_URI, networkAlias: envArg.MONGODB_HOST || 'corestore', port: Number(envArg.MONGODB_PORT || '27017'), }; } return { name: 'corestore-smartstorage', capability: 'objectstorage', protocol: 's3', internalUrl: envArg.AWS_ENDPOINT_URL || envArg.S3_ENDPOINT, networkAlias: envArg.S3_ENDPOINT_HOST || 'corestore', port: Number(envArg.S3_PORT || '9000'), }; } private async postCoreStore( urlArg: string, bodyArg: unknown, providerConfigArg?: plugins.servezoneInterfaces.platform.IPlatformProviderConfig, ): Promise { const token = this.getCoreStoreApiToken(providerConfigArg); const response = await fetch(urlArg, { method: 'POST', headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(bodyArg), }); const responseText = await response.text(); if (!response.ok) { throw new Error(`CoreStore request failed ${response.status}: ${responseText}`); } return responseText ? JSON.parse(responseText) as T : ({} as T); } 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); } }