feat: reconcile platform bindings
This commit is contained in:
+2
-2
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+11
-11
@@ -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
|
||||
|
||||
@@ -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<TPlatformDesiredState>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user