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; } }