feat(deployment): Implement Deployment and DeploymentManager classes with CRUD operations and service integration

This commit is contained in:
2025-09-08 12:46:23 +00:00
parent 4e38d2ff43
commit ce047d1bb0
11 changed files with 462 additions and 41 deletions

View File

@@ -23,6 +23,8 @@ import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalma
import { ExternalApiManager } from './manager.status/statusmanager.js';
import { ExternalRegistryManager } from './manager.externalregistry/index.js';
import { ImageManager } from './manager.image/classes.imagemanager.js';
import { ServiceManager } from './manager.service/classes.servicemanager.js';
import { DeploymentManager } from './manager.deployment/classes.deploymentmanager.js';
import { logger } from './logger.js';
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
@@ -60,6 +62,8 @@ export class Cloudly {
public externalApiManager: ExternalApiManager;
public externalRegistryManager: ExternalRegistryManager;
public imageManager: ImageManager;
public serviceManager: ServiceManager;
public deploymentManager: DeploymentManager;
public taskManager: CloudlyTaskmanager;
public nodeManager: CloudlyNodeManager;
public baremetalManager: CloudlyBaremetalManager;
@@ -89,6 +93,8 @@ export class Cloudly {
this.externalApiManager = new ExternalApiManager(this);
this.externalRegistryManager = new ExternalRegistryManager(this);
this.imageManager = new ImageManager(this);
this.serviceManager = new ServiceManager(this);
this.deploymentManager = new DeploymentManager(this);
this.taskManager = new CloudlyTaskmanager(this);
this.secretManager = new CloudlySecretManager(this);
this.nodeManager = new CloudlyNodeManager(this);
@@ -114,6 +120,8 @@ export class Cloudly {
await this.secretManager.start();
await this.nodeManager.start();
await this.baremetalManager.start();
await this.serviceManager.start();
await this.deploymentManager.start();
await this.cloudflareConnector.init();
await this.letsencryptConnector.init();
@@ -133,5 +141,7 @@ export class Cloudly {
await this.letsencryptConnector.stop();
await this.mongodbConnector.stop();
await this.secretManager.stop();
await this.serviceManager.stop();
await this.deploymentManager.stop();
}
}

View File

@@ -0,0 +1,97 @@
import * as plugins from '../plugins.js';
export class Deployment extends plugins.smartdata.SmartDataDbDoc<
Deployment,
plugins.servezoneInterfaces.data.IDeployment
> {
@plugins.smartdata.unI()
public id: string = plugins.smartunique.uniSimple('deployment');
@plugins.smartdata.svDb()
public serviceId: string;
@plugins.smartdata.svDb()
public nodeId: string;
@plugins.smartdata.svDb()
public containerId?: string;
@plugins.smartdata.svDb()
public usedImageId: string;
@plugins.smartdata.svDb()
public version: string;
@plugins.smartdata.svDb()
public deployedAt: number;
@plugins.smartdata.svDb()
public deploymentLog: string[] = [];
@plugins.smartdata.svDb()
public status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
@plugins.smartdata.svDb()
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
@plugins.smartdata.svDb()
public resourceUsage?: {
cpuUsagePercent: number;
memoryUsedMB: number;
lastUpdated: number;
};
public static async createDeployment(
deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>
): Promise<Deployment> {
const deployment = new Deployment();
if (deploymentData.serviceId) deployment.serviceId = deploymentData.serviceId;
if (deploymentData.nodeId) deployment.nodeId = deploymentData.nodeId;
if (deploymentData.containerId) deployment.containerId = deploymentData.containerId;
if (deploymentData.usedImageId) deployment.usedImageId = deploymentData.usedImageId;
if (deploymentData.version) deployment.version = deploymentData.version;
if (deploymentData.deployedAt) deployment.deployedAt = deploymentData.deployedAt;
if (deploymentData.deploymentLog) deployment.deploymentLog = deploymentData.deploymentLog;
if (deploymentData.status) deployment.status = deploymentData.status;
if (deploymentData.healthStatus) deployment.healthStatus = deploymentData.healthStatus;
if (deploymentData.resourceUsage) deployment.resourceUsage = deploymentData.resourceUsage;
await deployment.save();
return deployment;
}
public async updateHealthStatus(healthStatus: 'healthy' | 'unhealthy' | 'unknown') {
this.healthStatus = healthStatus;
await this.save();
}
public async updateResourceUsage(cpuUsagePercent: number, memoryUsedMB: number) {
this.resourceUsage = {
cpuUsagePercent,
memoryUsedMB,
lastUpdated: Date.now(),
};
await this.save();
}
public async addLogEntry(entry: string) {
this.deploymentLog.push(entry);
await this.save();
}
public async createSavableObject(): Promise<plugins.servezoneInterfaces.data.IDeployment> {
return {
id: this.id,
serviceId: this.serviceId,
nodeId: this.nodeId,
containerId: this.containerId,
usedImageId: this.usedImageId,
version: this.version,
deployedAt: this.deployedAt,
deploymentLog: this.deploymentLog,
status: this.status,
healthStatus: this.healthStatus,
resourceUsage: this.resourceUsage,
};
}
}

View File

@@ -0,0 +1,303 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import { Deployment } from './classes.deployment.js';
export class DeploymentManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CDeployment = plugins.smartdata.setDefaultManagerForDoc(this, Deployment);
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
// Connect typedrouter to main router
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
// Get all deployments
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(
'getDeployments',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployments = await this.CDeployment.getInstances({});
return {
deployments: await Promise.all(
deployments.map((deployment) => deployment.createSavableObject())
),
};
}
)
);
// Get deployment by ID
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(
'getDeploymentById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
return {
deployment: await deployment.createSavableObject(),
};
}
)
);
// Get deployments by service
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentsByService>(
'getDeploymentsByService',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployments = await this.CDeployment.getInstances({
serviceId: reqArg.serviceId,
});
return {
deployments: await Promise.all(
deployments.map((deployment) => deployment.createSavableObject())
),
};
}
)
);
// Get deployments by node
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentsByNode>(
'getDeploymentsByNode',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployments = await this.CDeployment.getInstances({
nodeId: reqArg.nodeId,
});
return {
deployments: await Promise.all(
deployments.map((deployment) => deployment.createSavableObject())
),
};
}
)
);
// Create deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(
'createDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await Deployment.createDeployment(reqArg.deploymentData);
return {
deployment: await deployment.createSavableObject(),
};
}
)
);
// Update deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(
'updateDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
// Update fields
if (reqArg.deploymentData.status !== undefined) {
deployment.status = reqArg.deploymentData.status;
}
if (reqArg.deploymentData.healthStatus !== undefined) {
deployment.healthStatus = reqArg.deploymentData.healthStatus;
}
if (reqArg.deploymentData.containerId !== undefined) {
deployment.containerId = reqArg.deploymentData.containerId;
}
if (reqArg.deploymentData.resourceUsage !== undefined) {
deployment.resourceUsage = reqArg.deploymentData.resourceUsage;
}
await deployment.save();
return {
deployment: await deployment.createSavableObject(),
};
}
)
);
// Delete deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(
'deleteDeploymentById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
await deployment.delete();
return {
success: true,
};
}
)
);
// Restart deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(
'restartDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
// TODO: Implement actual restart logic with Docker/container runtime
deployment.status = 'starting';
await deployment.save();
return {
success: true,
deployment: await deployment.createSavableObject(),
};
}
)
);
// Scale deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(
'scaleDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
// TODO: Implement scaling logic
// This would create/delete deployment instances based on replicas count
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
return {
success: true,
deployment: await deployment.createSavableObject(),
};
}
)
);
}
/**
* Get all deployments
*/
public async getAllDeployments(): Promise<Deployment[]> {
return await this.CDeployment.getInstances({});
}
/**
* Get deployments for a specific service
*/
public async getDeploymentsForService(serviceId: string): Promise<Deployment[]> {
return await this.CDeployment.getInstances({
serviceId,
});
}
/**
* Get deployments for a specific node
*/
public async getDeploymentsForNode(nodeId: string): Promise<Deployment[]> {
return await this.CDeployment.getInstances({
nodeId,
});
}
/**
* Create a new deployment
*/
public async createDeployment(
serviceId: string,
nodeId: string,
version: string = 'latest'
): Promise<Deployment> {
return await Deployment.createDeployment({
serviceId,
nodeId,
version,
status: 'scheduled',
deployedAt: Date.now(),
deploymentLog: [`Deployment created at ${new Date().toISOString()}`],
});
}
public async start() {
// DeploymentManager is ready - handlers are already registered in constructor
console.log('DeploymentManager started');
}
public async stop() {
// Cleanup if needed
console.log('DeploymentManager stopped');
}
}

View File

@@ -14,6 +14,8 @@ export class ServiceManager {
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
@@ -97,4 +99,14 @@ export class ServiceManager {
)
);
}
public async start() {
// ServiceManager is ready - handlers are already registered in constructor
console.log('ServiceManager started');
}
public async stop() {
// Cleanup if needed
console.log('ServiceManager stopped');
}
}

View File

@@ -142,13 +142,21 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
'/typedrequest',
'getServices'
);
const responseServices = await trGetServices.fire({
identity: loginStatePart.getState().identity,
});
currentState = {
...currentState,
services: responseServices.services,
};
try {
const responseServices = await trGetServices.fire({
identity: loginStatePart.getState().identity,
});
currentState = {
...currentState,
services: responseServices?.services || [],
};
} catch (error) {
console.error('Failed to fetch services:', error);
currentState = {
...currentState,
services: [],
};
}
// Deployments
const trGetDeployments =
@@ -156,13 +164,21 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
'/typedrequest',
'getDeployments'
);
const responseDeployments = await trGetDeployments.fire({
identity: loginStatePart.getState().identity,
});
currentState = {
...currentState,
deployments: responseDeployments.deployments,
};
try {
const responseDeployments = await trGetDeployments.fire({
identity: loginStatePart.getState().identity,
});
currentState = {
...currentState,
deployments: responseDeployments?.deployments || [],
};
} catch (error) {
console.error('Failed to fetch deployments:', error);
currentState = {
...currentState,
deployments: [],
};
}
return currentState;
});

View File

@@ -15,10 +15,7 @@ import * as appstate from '../appstate.js';
@customElement('cloudly-view-clusters')
export class CloudlyViewClusters extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
private data: appstate.IDataState = {};
constructor() {
super();

View File

@@ -15,10 +15,7 @@ import * as appstate from '../appstate.js';
@customElement('cloudly-view-deployments')
export class CloudlyViewDeployments extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
private data: appstate.IDataState = {};
constructor() {
super();

View File

@@ -8,10 +8,7 @@ import * as appstate from '../appstate.js';
@customElement('cloudly-view-images')
export class CloudlyViewImages extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
private data: appstate.IDataState = {};
constructor() {
super();

View File

@@ -15,10 +15,7 @@ import * as appstate from '../appstate.js';
@customElement('cloudly-view-secretbundles')
export class CloudlyViewSecretBundles extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
private data: appstate.IDataState = {};
constructor() {
super();
@@ -44,7 +41,7 @@ export class CloudlyViewSecretBundles extends DeesElement {
<dees-table
.heading1=${'SecretBundles'}
.heading2=${'decoded in client'}
.data=${this.data.secretBundles}
.data=${this.data.secretBundles || []}
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
return {
name: itemArg.data.name,

View File

@@ -8,18 +8,16 @@ import * as appstate from '../appstate.js';
@customElement('cloudly-view-secretsgroups')
export class CloudlyViewSecretGroups extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
private data: appstate.IDataState = {};
constructor() {
super();
appstate.dataState
const subscription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subscription);
}
public static styles = [
@@ -36,7 +34,7 @@ export class CloudlyViewSecretGroups extends DeesElement {
<dees-table
heading1="SecretGroups"
heading2="decoded in client"
.data=${this.data.secretGroups}
.data=${this.data.secretGroups || []}
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
return {
name: secretGroup.data.name,

View File

@@ -15,10 +15,7 @@ import * as appstate from '../appstate.js';
@customElement('cloudly-view-services')
export class CloudlyViewServices extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
private data: appstate.IDataState = {};
constructor() {
super();