feat: reconcile platform bindings

This commit is contained in:
2026-04-28 12:22:27 +00:00
parent 6ba5e36f4f
commit b221e9fe43
3 changed files with 281 additions and 15 deletions
+2 -2
View File
@@ -79,8 +79,8 @@
"@push.rocks/smartstream": "^3.4.0", "@push.rocks/smartstream": "^3.4.0",
"@push.rocks/smartstring": "^4.1.0", "@push.rocks/smartstring": "^4.1.0",
"@push.rocks/taskbuffer": "^8.0.2", "@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/api": "^5.3.1", "@serve.zone/api": "^5.3.2",
"@serve.zone/interfaces": "^5.4.4", "@serve.zone/interfaces": "^5.4.5",
"@tsclass/tsclass": "^9.5.0", "@tsclass/tsclass": "^9.5.0",
"@types/node": "25.6.0" "@types/node": "25.6.0"
}, },
+11 -11
View File
@@ -69,11 +69,11 @@ importers:
specifier: ^8.0.2 specifier: ^8.0.2
version: 8.0.2 version: 8.0.2
'@serve.zone/api': '@serve.zone/api':
specifier: ^5.3.1 specifier: ^5.3.2
version: 5.3.1(@push.rocks/smartserve@2.0.3) version: 5.3.2(@push.rocks/smartserve@2.0.3)
'@serve.zone/interfaces': '@serve.zone/interfaces':
specifier: ^5.4.4 specifier: ^5.4.5
version: 5.4.4 version: 5.4.5
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.5.0 specifier: ^9.5.0
version: 9.5.0 version: 9.5.0
@@ -1516,11 +1516,11 @@ packages:
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@serve.zone/api@5.3.1': '@serve.zone/api@5.3.2':
resolution: {integrity: sha512-P6f3VWr2ljM8dwEtWYBROSZVtcW1HMc5oiorOCcvDeWY6roJbZobK6UFDlcdop02TxGEneJD+jVKoCwBoBLJVw==} resolution: {integrity: sha512-ETQ4KSNfhDP7O1WxXXLcMn/A+jZtDfd7FjuQ0k3n8tnXG9hExh8ZmqvMwVj8eT2CnXO+xQVlbAgT0HLMLnxCfA==}
'@serve.zone/interfaces@5.4.4': '@serve.zone/interfaces@5.4.5':
resolution: {integrity: sha512-0mcLvZBGHOFKG8PkpMieHo19lbPLJIpDIgKUSbuqfqOzSYymS8BLYftCr8Kw7ddl61kICZoy/m09U+TtxZbxBg==} resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==}
'@sindresorhus/is@5.6.0': '@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -6929,7 +6929,7 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {} '@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: dependencies:
'@api.global/typedrequest': 3.1.10 '@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
@@ -6938,7 +6938,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstream': 3.4.0 '@push.rocks/smartstream': 3.4.0
'@serve.zone/interfaces': 5.4.4 '@serve.zone/interfaces': 5.4.5
'@tsclass/tsclass': 9.5.0 '@tsclass/tsclass': 9.5.0
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
@@ -6949,7 +6949,7 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@serve.zone/interfaces@5.4.4': '@serve.zone/interfaces@5.4.5':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
+268 -2
View File
@@ -1,22 +1,288 @@
import type { Coreflow } from './coreflow.classes.coreflow.js'; import type { Coreflow } from './coreflow.classes.coreflow.js';
import * as plugins from './coreflow.plugins.js';
import { logger } from './coreflow.logging.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 { export class PlatformManager {
public coreflowRef: Coreflow; public coreflowRef: Coreflow;
private configSubscription?: { unsubscribe: () => void };
private currentDesiredState?: TPlatformDesiredState;
constructor(coreflowRefArg: Coreflow) { constructor(coreflowRefArg: Coreflow) {
this.coreflowRef = coreflowRefArg; this.coreflowRef = coreflowRefArg;
} }
public async start() { 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'); logger.log('info', 'Platform manager started');
} }
public async stop() { public async stop() {
this.configSubscription?.unsubscribe();
logger.log('info', 'Platform manager stopped'); logger.log('info', 'Platform manager stopped');
} }
public async reconcilePlatformServices() { public async reconcilePlatformServices(desiredStateArg?: Partial<TPlatformDesiredState>) {
logger.log('info', 'Platform service reconciliation is not implemented yet'); 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<TPlatformDesiredState> = {},
): Promise<TPlatformDesiredState> {
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);
} }
} }