feat: add platform desired state manager

This commit is contained in:
2026-04-28 12:18:12 +00:00
parent 84d3e8f52f
commit 3080075811
8 changed files with 422 additions and 18 deletions
+2 -2
View File
@@ -73,8 +73,8 @@
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^3.4.0",
"@push.rocks/webjwt": "^1.0.9",
"@serve.zone/api": "^5.3.1",
"@serve.zone/interfaces": "^5.4.3",
"@serve.zone/api": "^5.3.2",
"@serve.zone/interfaces": "^5.4.5",
"@tsclass/tsclass": "^9.2.0"
},
"files": [
+13 -15
View File
@@ -135,11 +135,11 @@ importers:
specifier: ^1.0.9
version: 1.0.9
'@serve.zone/api':
specifier: ^5.3.1
version: 5.3.1
specifier: ^5.3.2
version: 5.3.2
'@serve.zone/interfaces':
specifier: ^5.4.3
version: 5.4.3
specifier: ^5.4.5
version: 5.4.5
'@tsclass/tsclass':
specifier: ^9.2.0
version: 9.2.0
@@ -1950,11 +1950,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.3':
resolution: {integrity: sha512-9ijFhHoC7GYyyAUJbBoDYmcoCmIXTFPiD6fI3x68SWiC0xA+2LG0nOe14D32c1QN9X/3i2Ac5/1sUibfjHsIGg==}
'@serve.zone/interfaces@5.4.5':
resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==}
'@shikijs/engine-oniguruma@3.12.2':
resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==}
@@ -7150,10 +7150,8 @@ snapshots:
transitivePeerDependencies:
- '@nuxt/kit'
- '@swc/helpers'
- bufferutil
- react
- supports-color
- utf-8-validate
- vue
'@happy-dom/global-registrator@15.11.7':
@@ -9034,7 +9032,7 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@serve.zone/api@5.3.1':
'@serve.zone/api@5.3.2':
dependencies:
'@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19
@@ -9043,14 +9041,14 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstream': 3.2.5
'@serve.zone/interfaces': 5.4.3
'@tsclass/tsclass': 9.2.0
'@serve.zone/interfaces': 5.4.5
'@tsclass/tsclass': 9.5.0
'@serve.zone/interfaces@5.4.3':
'@serve.zone/interfaces@5.4.5':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.2.0
'@tsclass/tsclass': 9.5.0
'@shikijs/engine-oniguruma@3.12.2':
dependencies:
+54
View File
@@ -84,6 +84,60 @@ tap.test('should get an identity', async () => {
}
});
tap.test('should expose platform desired state', async () => {
const capabilitiesResponse = await testClient.platform.getPlatformCapabilities();
expect(capabilitiesResponse.capabilities.find((capability) => capability.id === 'database')).toBeTruthy();
const desiredState = await testClient.platform.getPlatformDesiredState();
expect(desiredState.capabilities).toBeTruthy();
expect(desiredState.providerConfigs).toBeTruthy();
expect(desiredState.bindings).toBeTruthy();
});
let platformProviderConfigId: string;
let platformBindingId: string;
tap.test('should upsert platform provider config and binding', async () => {
const providerConfigResponse = await testClient.platform.upsertPlatformProviderConfig({
id: '',
capability: 'database',
providerType: 'docker',
name: 'Local Docker Database',
enabled: true,
});
platformProviderConfigId = providerConfigResponse.providerConfig.id;
expect(platformProviderConfigId).toBeTruthy();
const bindingResponse = await testClient.platform.upsertPlatformBinding({
id: '',
serviceId: 'test-service',
capability: 'database',
desiredState: 'enabled',
status: 'requested',
providerConfigId: platformProviderConfigId,
});
platformBindingId = bindingResponse.binding.id;
expect(platformBindingId).toBeTruthy();
const statusResponse = await testClient.platform.updatePlatformBindingStatus({
bindingId: platformBindingId,
status: 'ready',
endpoints: [
{
name: 'primary',
capability: 'database',
protocol: 'mongodb',
internalUrl: 'mongodb://platform-database:27017/test-service',
},
],
});
expect(statusResponse.binding.status).toEqual('ready');
const bindingsResponse = await testClient.platform.getPlatformBindings({
serviceId: 'test-service',
});
expect(bindingsResponse.bindings.find((binding) => binding.id === platformBindingId)).toBeTruthy();
});
let image: any;
tap.test('should create and upload an image', async () => {
console.log('🔵 Test: Creating and uploading image...');
+5
View File
@@ -30,6 +30,7 @@ import { DomainManager } from './manager.domain/classes.domainmanager.js';
import { logger } from './logger.js';
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
import { CloudlyPlatformManager } from './manager.platform/classes.platformmanager.js';
/**
* Cloudly class can be used to instantiate a cloudly server.
@@ -59,6 +60,7 @@ export class Cloudly {
public authManager: CloudlyAuthManager;
public secretManager: CloudlySecretManager;
public settingsManager: CloudlySettingsManager;
public platformManager: CloudlyPlatformManager;
public clusterManager: ClusterManager;
public coreflowManager: CloudlyCoreflowManager;
public externalApiManager: ExternalApiManager;
@@ -92,6 +94,7 @@ export class Cloudly {
// managers
this.authManager = new CloudlyAuthManager(this);
this.settingsManager = new CloudlySettingsManager(this);
this.platformManager = new CloudlyPlatformManager(this);
this.clusterManager = new ClusterManager(this);
this.coreflowManager = new CloudlyCoreflowManager(this);
this.externalApiManager = new ExternalApiManager(this);
@@ -127,6 +130,7 @@ export class Cloudly {
await this.nodeManager.start();
await this.baremetalManager.start();
await this.serviceManager.start();
await this.platformManager.start();
await this.deploymentManager.start();
await this.taskManager.init();
@@ -150,6 +154,7 @@ export class Cloudly {
await this.mongodbConnector.stop();
await this.secretManager.stop();
await this.serviceManager.stop();
await this.platformManager.stop();
await this.deploymentManager.stop();
await this.taskManager.stop();
await this.externalRegistryManager.stop();
+5 -1
View File
@@ -72,10 +72,14 @@ export class CloudlyCoreflowManager {
console.log('trying to get clusterConfigSet');
console.log(dataArg);
const cluster = await this.cloudlyRef.clusterManager.getClusterBy_Identity(identity);
const services = await this.cloudlyRef.serviceManager.CService.getInstances({});
const platformDesiredState = await this.cloudlyRef.platformManager.getPlatformDesiredState();
console.log('got cluster config and sending it back to coreflow');
return {
configData: await cluster.createSavableObject(),
services: [],
services: await Promise.all(services.map((service) => service.createSavableObject())),
platformProviderConfigs: platformDesiredState.providerConfigs,
platformBindings: platformDesiredState.bindings,
};
}
)
@@ -0,0 +1,68 @@
import * as plugins from '../plugins.js';
import type { CloudlyPlatformManager } from './classes.platformmanager.js';
@plugins.smartdata.managed()
export class PlatformBinding extends plugins.smartdata.SmartDataDbDoc<
PlatformBinding,
plugins.servezoneInterfaces.platform.IPlatformBinding,
CloudlyPlatformManager
> {
public static async upsertBinding(
bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding,
) {
const existingBinding =
bindingArg.id &&
(await this.getInstance({
id: bindingArg.id,
}));
const binding = existingBinding || new PlatformBinding();
const timestamp = Date.now();
Object.assign(binding, {
...bindingArg,
id: bindingArg.id || (await this.getNewId()),
status: bindingArg.status || 'requested',
desiredState: bindingArg.desiredState || 'enabled',
createdAt: bindingArg.createdAt || existingBinding?.createdAt || timestamp,
updatedAt: timestamp,
});
await binding.save();
return binding;
}
@plugins.smartdata.unI()
public id!: string;
@plugins.smartdata.svDb()
public serviceId!: string;
@plugins.smartdata.svDb()
public capability!: plugins.servezoneInterfaces.platform.TPlatformCapability;
@plugins.smartdata.svDb()
public desiredState!: plugins.servezoneInterfaces.platform.TPlatformDesiredState;
@plugins.smartdata.svDb()
public status!: plugins.servezoneInterfaces.platform.TPlatformBindingStatus;
@plugins.smartdata.svDb()
public providerConfigId?: string;
@plugins.smartdata.svDb()
public config?: { [key: string]: plugins.servezoneInterfaces.platform.TPlatformConfigValue };
@plugins.smartdata.svDb()
public endpoints?: plugins.servezoneInterfaces.platform.IPlatformServiceEndpoint[];
@plugins.smartdata.svDb()
public credentials?: plugins.servezoneInterfaces.platform.IPlatformCredentialRef[];
@plugins.smartdata.svDb()
public createdAt?: number;
@plugins.smartdata.svDb()
public updatedAt?: number;
@plugins.smartdata.svDb()
public errorText?: string;
}
@@ -0,0 +1,228 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import { PlatformBinding } from './classes.platformbinding.js';
import { PlatformProviderConfig } from './classes.platformproviderconfig.js';
export class CloudlyPlatformManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
public capabilities: plugins.servezoneInterfaces.platform.IPlatformCapability[] = [
{ id: 'email', title: 'Email', accessMode: 'rpc', defaultProviderType: 'cloudly' },
{ id: 'sms', title: 'SMS', accessMode: 'rpc', defaultProviderType: 'cloudly' },
{ id: 'pushnotification', title: 'Push Notifications', accessMode: 'rpc', defaultProviderType: 'cloudly' },
{ id: 'letter', title: 'Letters', accessMode: 'rpc', defaultProviderType: 'cloudly' },
{ id: 'ai', title: 'AI', accessMode: 'rpc', defaultProviderType: 'cloudly' },
{ id: 'database', title: 'Database', accessMode: 'binding', defaultProviderType: 'docker' },
{ id: 'objectstorage', title: 'Object Storage', accessMode: 'binding', defaultProviderType: 's3' },
{ id: 'logging', title: 'Logging', accessMode: 'sidecar', defaultProviderType: 'corelog' },
{ id: 'backup', title: 'Backup', accessMode: 'internal', defaultProviderType: 'corebackup' },
{ id: 'sip', title: 'SIP', accessMode: 'rpc', defaultProviderType: 'cloudly' },
];
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CPlatformProviderConfig = plugins.smartdata.setDefaultManagerForDoc(
this,
PlatformProviderConfig,
);
public CPlatformBinding = plugins.smartdata.setDefaultManagerForDoc(this, PlatformBinding);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_GetPlatformDesiredState>(
'getPlatformDesiredState',
async (requestData) => {
await this.passValidIdentity(requestData);
return await this.getPlatformDesiredState();
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_GetPlatformCapabilities>(
'getPlatformCapabilities',
async (requestData) => {
await this.passValidIdentity(requestData);
return {
capabilities: this.capabilities,
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_GetPlatformProviderConfigs>(
'getPlatformProviderConfigs',
async (requestData) => {
await this.passValidIdentity(requestData);
const query = requestData.capability ? { capability: requestData.capability } : {};
return {
providerConfigs: await this.getProviderConfigs(query),
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_UpsertPlatformProviderConfig>(
'upsertPlatformProviderConfig',
async (requestData) => {
await this.passAdminIdentity(requestData);
const providerConfig = await PlatformProviderConfig.upsertProviderConfig(
requestData.providerConfig,
);
return {
providerConfig: await providerConfig.createSavableObject(),
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_DeletePlatformProviderConfigById>(
'deletePlatformProviderConfigById',
async (requestData) => {
await this.passAdminIdentity(requestData);
const providerConfig = await PlatformProviderConfig.getInstance({
id: requestData.providerConfigId,
});
if (providerConfig) {
await providerConfig.delete();
}
return {
success: true,
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_GetPlatformBindings>(
'getPlatformBindings',
async (requestData) => {
await this.passValidIdentity(requestData);
return {
bindings: await this.getBindings({
...(requestData.serviceId ? { serviceId: requestData.serviceId } : {}),
...(requestData.capability ? { capability: requestData.capability } : {}),
}),
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_UpsertPlatformBinding>(
'upsertPlatformBinding',
async (requestData) => {
await this.passAdminIdentity(requestData);
const binding = await PlatformBinding.upsertBinding(requestData.binding);
return {
binding: await binding.createSavableObject(),
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_UpdatePlatformBindingStatus>(
'updatePlatformBindingStatus',
async (requestData) => {
await this.passAdminOrClusterIdentity(requestData);
const binding = await PlatformBinding.getInstance({
id: requestData.bindingId,
});
if (!binding) {
throw new plugins.typedrequest.TypedResponseError(
`Platform binding ${requestData.bindingId} not found`,
);
}
binding.status = requestData.status;
binding.updatedAt = Date.now();
if (requestData.endpoints) {
binding.endpoints = requestData.endpoints;
}
if (requestData.credentials) {
binding.credentials = requestData.credentials;
}
if (requestData.errorText !== undefined) {
binding.errorText = requestData.errorText;
}
await binding.save();
return {
binding: await binding.createSavableObject(),
};
},
),
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.platform.IReq_Any_Cloudly_DeletePlatformBindingById>(
'deletePlatformBindingById',
async (requestData) => {
await this.passAdminIdentity(requestData);
const binding = await PlatformBinding.getInstance({
id: requestData.bindingId,
});
if (binding) {
await binding.delete();
}
return {
success: true,
};
},
),
);
}
public async start() {}
public async stop() {}
public async getPlatformDesiredState() {
return {
capabilities: this.capabilities,
providerConfigs: await this.getProviderConfigs(),
bindings: await this.getBindings(),
};
}
public async getProviderConfigs(queryArg: Record<string, unknown> = {}) {
const providerConfigs = await this.CPlatformProviderConfig.getInstances(queryArg);
return await Promise.all(
providerConfigs.map((providerConfig) => providerConfig.createSavableObject()),
);
}
public async getBindings(queryArg: Record<string, unknown> = {}) {
const bindings = await this.CPlatformBinding.getInstances(queryArg);
return await Promise.all(bindings.map((binding) => binding.createSavableObject()));
}
private async passValidIdentity(requestData: { identity: plugins.servezoneInterfaces.data.IIdentity }) {
await plugins.smartguard.passGuardsOrReject(requestData, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
}
private async passAdminIdentity(requestData: { identity: plugins.servezoneInterfaces.data.IIdentity }) {
await plugins.smartguard.passGuardsOrReject(requestData, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
}
private async passAdminOrClusterIdentity(requestData: {
identity: plugins.servezoneInterfaces.data.IIdentity;
}) {
await this.passValidIdentity(requestData);
if (requestData.identity.role !== 'admin' && requestData.identity.role !== 'cluster') {
throw new plugins.typedrequest.TypedResponseError('identity must be admin or cluster');
}
}
}
@@ -0,0 +1,47 @@
import * as plugins from '../plugins.js';
import type { CloudlyPlatformManager } from './classes.platformmanager.js';
@plugins.smartdata.managed()
export class PlatformProviderConfig extends plugins.smartdata.SmartDataDbDoc<
PlatformProviderConfig,
plugins.servezoneInterfaces.platform.IPlatformProviderConfig,
CloudlyPlatformManager
> {
public static async upsertProviderConfig(
providerConfigArg: plugins.servezoneInterfaces.platform.IPlatformProviderConfig,
) {
const providerConfig =
(providerConfigArg.id &&
(await this.getInstance({
id: providerConfigArg.id,
}))) || new PlatformProviderConfig();
Object.assign(providerConfig, {
...providerConfigArg,
id: providerConfigArg.id || (await this.getNewId()),
});
await providerConfig.save();
return providerConfig;
}
@plugins.smartdata.unI()
public id!: string;
@plugins.smartdata.svDb()
public capability!: plugins.servezoneInterfaces.platform.TPlatformCapability;
@plugins.smartdata.svDb()
public providerType!: string;
@plugins.smartdata.svDb()
public name!: string;
@plugins.smartdata.svDb()
public enabled!: boolean;
@plugins.smartdata.svDb()
public config?: { [key: string]: plugins.servezoneInterfaces.platform.TPlatformConfigValue };
@plugins.smartdata.svDb()
public secretBundleId?: string;
}