From 195b3f5babb2a7518623f3753f51c60cdda56185 Mon Sep 17 00:00:00 2001 From: keyhan Date: Fri, 15 May 2026 00:57:31 +0330 Subject: [PATCH] fix(deploy): stop scales all app workloads and sets status stopped Use suspendApplication on stop so app, database, Redis, and RabbitMQ deployments scale to zero. Start uses resumeApplication to bring the full stack back. Deployment status is updated to stopped/running. Co-authored-by: Cursor --- .../src/deployments/deployments.service.ts | 7 +- backend/src/kubernetes/kubernetes.service.ts | 127 ++++++++---------- 2 files changed, 61 insertions(+), 73 deletions(-) diff --git a/backend/src/deployments/deployments.service.ts b/backend/src/deployments/deployments.service.ts index 1ff48bf..df5abf7 100644 --- a/backend/src/deployments/deployments.service.ts +++ b/backend/src/deployments/deployments.service.ts @@ -240,15 +240,15 @@ export class DeploymentsService { async stopDeployment(applicationId: string, userId: string): Promise { const app = await this.applicationsService.findOne(applicationId, userId); - await this.kubernetesService.scaleDeployment(app, 0); + await this.kubernetesService.suspendApplication(app); - // Update the latest deployment status to stopped const latest = await this.deploymentsRepository.findOne({ where: { applicationId }, order: { createdAt: 'DESC' }, }); if (latest) { latest.status = DeploymentStatus.STOPPED; + latest.finishedAt = latest.finishedAt || new Date(); await this.deploymentsRepository.save(latest); } return latest; @@ -256,9 +256,8 @@ export class DeploymentsService { async startDeployment(applicationId: string, userId: string): Promise { const app = await this.applicationsService.findOne(applicationId, userId); - await this.kubernetesService.scaleDeployment(app, app.replicas || 1); + await this.kubernetesService.resumeApplication(app); - // Update the latest deployment status to running const latest = await this.deploymentsRepository.findOne({ where: { applicationId }, order: { createdAt: 'DESC' }, diff --git a/backend/src/kubernetes/kubernetes.service.ts b/backend/src/kubernetes/kubernetes.service.ts index f775e58..114e021 100644 --- a/backend/src/kubernetes/kubernetes.service.ts +++ b/backend/src/kubernetes/kubernetes.service.ts @@ -1353,10 +1353,47 @@ export class KubernetesService implements OnModuleInit { ); } + /** All K8s Deployments that belong to an application stack. */ + private getApplicationWorkloadDeployments(app: Application): { name: string; runningReplicas: number }[] { + const workloads: { name: string; runningReplicas: number }[] = [ + { name: app.name, runningReplicas: app.replicas || 1 }, + ]; + + if (app.databaseType && app.databaseType !== DatabaseType.NONE) { + workloads.push({ name: `${app.name}-db`, runningReplicas: 1 }); + } + if (app.enableRedis) { + workloads.push({ name: `${app.name}-redis`, runningReplicas: 1 }); + } + if (app.enableRabbitmq) { + workloads.push({ name: `${app.name}-rabbitmq`, runningReplicas: 1 }); + } + + return workloads; + } + + private async patchDeploymentReplicas( + appsApi: k8s.AppsV1Api, + namespace: string, + deploymentName: string, + replicas: number, + ): Promise { + await appsApi.patchNamespacedDeployment( + deploymentName, + namespace, + { spec: { replicas } }, + undefined, + undefined, + undefined, + undefined, + undefined, + { headers: { 'Content-Type': 'application/merge-patch+json' } }, + ); + } + /** - * Suspend an application by scaling deployment to 0 replicas. - * This keeps all resources (PVC, Service, Ingress) but stops the pods. - * Also scales database deployment to 0 if exists. + * Suspend an application by scaling all stack deployments to 0 replicas. + * Keeps PVCs, Services, and Ingress in place. */ async suspendApplication(app: Application): Promise { const { appsApi } = await this.getK8sClient(app.clusterId); @@ -1364,97 +1401,49 @@ export class KubernetesService implements OnModuleInit { this.logger.log(`Suspending application ${app.name} in namespace ${namespace}`); - // Scale main deployment to 0 - try { - await appsApi.patchNamespacedDeployment( - app.name, - namespace, - { spec: { replicas: 0 } }, - undefined, - undefined, - undefined, - undefined, - undefined, - { headers: { 'Content-Type': 'application/merge-patch+json' } }, - ); - this.logger.log(`Scaled ${app.name} to 0 replicas`); - } catch (e: any) { - this.logger.warn(`Failed to scale ${app.name}: ${e.message}`); - } - - // Scale database deployment to 0 if exists - if (app.databaseType && app.databaseType !== DatabaseType.NONE) { + for (const workload of this.getApplicationWorkloadDeployments(app)) { try { - await appsApi.patchNamespacedDeployment( - `${app.name}-db`, - namespace, - { spec: { replicas: 0 } }, - undefined, - undefined, - undefined, - undefined, - undefined, - { headers: { 'Content-Type': 'application/merge-patch+json' } }, - ); - this.logger.log(`Scaled ${app.name}-db to 0 replicas`); + await this.patchDeploymentReplicas(appsApi, namespace, workload.name, 0); + this.logger.log(`Scaled ${workload.name} to 0 replicas`); } catch (e: any) { - // Database might not exist, that's ok if (e?.response?.statusCode !== 404) { - this.logger.warn(`Failed to scale ${app.name}-db: ${e.message}`); + this.logger.warn(`Failed to scale ${workload.name}: ${e.message}`); } } } } /** - * Resume a suspended application by scaling deployment back to original replicas. - * Also scales database deployment back to 1 if exists. + * Resume a suspended application by scaling all stack deployments back up. */ async resumeApplication(app: Application): Promise { const { appsApi } = await this.getK8sClient(app.clusterId); const namespace = `user-${app.userId.split('-')[0]}`; - const replicas = app.replicas || 1; this.logger.log(`Resuming application ${app.name} in namespace ${namespace}`); - // Scale database deployment back first (so it's ready when app starts) - if (app.databaseType && app.databaseType !== DatabaseType.NONE) { + const workloads = this.getApplicationWorkloadDeployments(app); + const dependencies = workloads.filter((w) => w.name !== app.name); + const main = workloads.find((w) => w.name === app.name); + + for (const workload of dependencies) { try { - await appsApi.patchNamespacedDeployment( - `${app.name}-db`, - namespace, - { spec: { replicas: 1 } }, - undefined, - undefined, - undefined, - undefined, - undefined, - { headers: { 'Content-Type': 'application/merge-patch+json' } }, - ); - this.logger.log(`Scaled ${app.name}-db to 1 replica`); + await this.patchDeploymentReplicas(appsApi, namespace, workload.name, workload.runningReplicas); + this.logger.log(`Scaled ${workload.name} to ${workload.runningReplicas} replica(s)`); } catch (e: any) { if (e?.response?.statusCode !== 404) { - this.logger.warn(`Failed to scale ${app.name}-db: ${e.message}`); + this.logger.warn(`Failed to scale ${workload.name}: ${e.message}`); } } } - // Scale main deployment back + if (!main) return; + try { - await appsApi.patchNamespacedDeployment( - app.name, - namespace, - { spec: { replicas } }, - undefined, - undefined, - undefined, - undefined, - undefined, - { headers: { 'Content-Type': 'application/merge-patch+json' } }, - ); - this.logger.log(`Scaled ${app.name} to ${replicas} replicas`); + await this.patchDeploymentReplicas(appsApi, namespace, main.name, main.runningReplicas); + this.logger.log(`Scaled ${main.name} to ${main.runningReplicas} replica(s)`); } catch (e: any) { - this.logger.warn(`Failed to scale ${app.name}: ${e.message}`); + this.logger.warn(`Failed to scale ${main.name}: ${e.message}`); throw e; } }