diff --git a/ts/classes.cloudly.ts b/ts/classes.cloudly.ts index 003d53e..a9215f2 100644 --- a/ts/classes.cloudly.ts +++ b/ts/classes.cloudly.ts @@ -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(); } } diff --git a/ts/manager.deployment/classes.deployment.ts b/ts/manager.deployment/classes.deployment.ts new file mode 100644 index 0000000..0ac7251 --- /dev/null +++ b/ts/manager.deployment/classes.deployment.ts @@ -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 + ): Promise { + 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 { + 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, + }; + } +} \ No newline at end of file diff --git a/ts/manager.deployment/classes.deploymentmanager.ts b/ts/manager.deployment/classes.deploymentmanager.ts new file mode 100644 index 0000000..6928eae --- /dev/null +++ b/ts/manager.deployment/classes.deploymentmanager.ts @@ -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( + '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( + '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( + '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( + '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( + '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( + '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( + '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( + '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( + '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 { + return await this.CDeployment.getInstances({}); + } + + /** + * Get deployments for a specific service + */ + public async getDeploymentsForService(serviceId: string): Promise { + return await this.CDeployment.getInstances({ + serviceId, + }); + } + + /** + * Get deployments for a specific node + */ + public async getDeploymentsForNode(nodeId: string): Promise { + return await this.CDeployment.getInstances({ + nodeId, + }); + } + + /** + * Create a new deployment + */ + public async createDeployment( + serviceId: string, + nodeId: string, + version: string = 'latest' + ): Promise { + 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'); + } +} \ No newline at end of file diff --git a/ts/manager.service/classes.servicemanager.ts b/ts/manager.service/classes.servicemanager.ts index eef327e..6bb5001 100644 --- a/ts/manager.service/classes.servicemanager.ts +++ b/ts/manager.service/classes.servicemanager.ts @@ -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( @@ -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'); + } } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 0dad3ab..cf3b315 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -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; }); diff --git a/ts_web/elements/cloudly-view-clusters.ts b/ts_web/elements/cloudly-view-clusters.ts index 9d40f51..5d9d368 100644 --- a/ts_web/elements/cloudly-view-clusters.ts +++ b/ts_web/elements/cloudly-view-clusters.ts @@ -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(); diff --git a/ts_web/elements/cloudly-view-deployments.ts b/ts_web/elements/cloudly-view-deployments.ts index 307863b..6cf1cf5 100644 --- a/ts_web/elements/cloudly-view-deployments.ts +++ b/ts_web/elements/cloudly-view-deployments.ts @@ -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(); diff --git a/ts_web/elements/cloudly-view-images.ts b/ts_web/elements/cloudly-view-images.ts index e4fb8a3..df284ef 100644 --- a/ts_web/elements/cloudly-view-images.ts +++ b/ts_web/elements/cloudly-view-images.ts @@ -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(); diff --git a/ts_web/elements/cloudly-view-secretbundles.ts b/ts_web/elements/cloudly-view-secretbundles.ts index 07a5a0d..c9356a3 100644 --- a/ts_web/elements/cloudly-view-secretbundles.ts +++ b/ts_web/elements/cloudly-view-secretbundles.ts @@ -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 { { return { name: itemArg.data.name, diff --git a/ts_web/elements/cloudly-view-secretgroups.ts b/ts_web/elements/cloudly-view-secretgroups.ts index 45dc035..08bd43c 100644 --- a/ts_web/elements/cloudly-view-secretgroups.ts +++ b/ts_web/elements/cloudly-view-secretgroups.ts @@ -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 { { return { name: secretGroup.data.name, diff --git a/ts_web/elements/cloudly-view-services.ts b/ts_web/elements/cloudly-view-services.ts index 41cf455..c7ce1bd 100644 --- a/ts_web/elements/cloudly-view-services.ts +++ b/ts_web/elements/cloudly-view-services.ts @@ -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();