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 <cursoragent@cursor.com>
This commit is contained in:
@@ -240,15 +240,15 @@ export class DeploymentsService {
|
|||||||
|
|
||||||
async stopDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
async stopDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
||||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
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({
|
const latest = await this.deploymentsRepository.findOne({
|
||||||
where: { applicationId },
|
where: { applicationId },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
});
|
});
|
||||||
if (latest) {
|
if (latest) {
|
||||||
latest.status = DeploymentStatus.STOPPED;
|
latest.status = DeploymentStatus.STOPPED;
|
||||||
|
latest.finishedAt = latest.finishedAt || new Date();
|
||||||
await this.deploymentsRepository.save(latest);
|
await this.deploymentsRepository.save(latest);
|
||||||
}
|
}
|
||||||
return latest;
|
return latest;
|
||||||
@@ -256,9 +256,8 @@ export class DeploymentsService {
|
|||||||
|
|
||||||
async startDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
async startDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
||||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
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({
|
const latest = await this.deploymentsRepository.findOne({
|
||||||
where: { applicationId },
|
where: { applicationId },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
|
|||||||
@@ -1353,96 +1353,33 @@ export class KubernetesService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** All K8s Deployments that belong to an application stack. */
|
||||||
* Suspend an application by scaling deployment to 0 replicas.
|
private getApplicationWorkloadDeployments(app: Application): { name: string; runningReplicas: number }[] {
|
||||||
* This keeps all resources (PVC, Service, Ingress) but stops the pods.
|
const workloads: { name: string; runningReplicas: number }[] = [
|
||||||
* Also scales database deployment to 0 if exists.
|
{ name: app.name, runningReplicas: app.replicas || 1 },
|
||||||
*/
|
];
|
||||||
async suspendApplication(app: Application): Promise<void> {
|
|
||||||
const { appsApi } = await this.getK8sClient(app.clusterId);
|
|
||||||
const namespace = `user-${app.userId.split('-')[0]}`;
|
|
||||||
|
|
||||||
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) {
|
if (app.databaseType && app.databaseType !== DatabaseType.NONE) {
|
||||||
try {
|
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<void> {
|
||||||
await appsApi.patchNamespacedDeployment(
|
await appsApi.patchNamespacedDeployment(
|
||||||
`${app.name}-db`,
|
deploymentName,
|
||||||
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`);
|
|
||||||
} 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resume a suspended application by scaling deployment back to original replicas.
|
|
||||||
* Also scales database deployment back to 1 if exists.
|
|
||||||
*/
|
|
||||||
async resumeApplication(app: Application): Promise<void> {
|
|
||||||
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) {
|
|
||||||
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`);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.response?.statusCode !== 404) {
|
|
||||||
this.logger.warn(`Failed to scale ${app.name}-db: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale main deployment back
|
|
||||||
try {
|
|
||||||
await appsApi.patchNamespacedDeployment(
|
|
||||||
app.name,
|
|
||||||
namespace,
|
namespace,
|
||||||
{ spec: { replicas } },
|
{ spec: { replicas } },
|
||||||
undefined,
|
undefined,
|
||||||
@@ -1452,9 +1389,61 @@ export class KubernetesService implements OnModuleInit {
|
|||||||
undefined,
|
undefined,
|
||||||
{ headers: { 'Content-Type': 'application/merge-patch+json' } },
|
{ headers: { 'Content-Type': 'application/merge-patch+json' } },
|
||||||
);
|
);
|
||||||
this.logger.log(`Scaled ${app.name} to ${replicas} replicas`);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspend an application by scaling all stack deployments to 0 replicas.
|
||||||
|
* Keeps PVCs, Services, and Ingress in place.
|
||||||
|
*/
|
||||||
|
async suspendApplication(app: Application): Promise<void> {
|
||||||
|
const { appsApi } = await this.getK8sClient(app.clusterId);
|
||||||
|
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||||
|
|
||||||
|
this.logger.log(`Suspending application ${app.name} in namespace ${namespace}`);
|
||||||
|
|
||||||
|
for (const workload of this.getApplicationWorkloadDeployments(app)) {
|
||||||
|
try {
|
||||||
|
await this.patchDeploymentReplicas(appsApi, namespace, workload.name, 0);
|
||||||
|
this.logger.log(`Scaled ${workload.name} to 0 replicas`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.warn(`Failed to scale ${app.name}: ${e.message}`);
|
if (e?.response?.statusCode !== 404) {
|
||||||
|
this.logger.warn(`Failed to scale ${workload.name}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume a suspended application by scaling all stack deployments back up.
|
||||||
|
*/
|
||||||
|
async resumeApplication(app: Application): Promise<void> {
|
||||||
|
const { appsApi } = await this.getK8sClient(app.clusterId);
|
||||||
|
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||||
|
|
||||||
|
this.logger.log(`Resuming application ${app.name} in namespace ${namespace}`);
|
||||||
|
|
||||||
|
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 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 ${workload.name}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!main) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 ${main.name}: ${e.message}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user