fix: reliable source upload, build cancel, WordPress port 80 default
- Replace port-forward/netcat PVC upload with kubectl cp for integrity - Add build cancellation API and session cleanup; deploy catches cancel - Default port 80 for WordPress, PHP, and Laravel on create - Build progress modal with cancel; Helm/K8s adjustments for deployments - Update build and kubernetes specs Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { Injectable, NotFoundException, Logger, Inject, forwardRef } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, BadRequestException, Logger, Inject, forwardRef } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as fs from 'fs';
|
||||
import { Deployment } from './entities/deployment.entity';
|
||||
import { ApplicationsService } from '../applications/applications.service';
|
||||
import { KubernetesService } from '../kubernetes/kubernetes.service';
|
||||
import { BuildService } from '../build/build.service';
|
||||
import { BuildService, BuildProgress, BuildCancelledError } from '../build/build.service';
|
||||
import { DeploymentStatus } from '../common/enums';
|
||||
|
||||
@Injectable()
|
||||
@@ -46,7 +46,7 @@ export class DeploymentsService {
|
||||
try {
|
||||
// Step 1: Build image
|
||||
await this.updateStatus(deploymentId, DeploymentStatus.BUILDING);
|
||||
const { imageUri, buildLog } = await this.buildService.buildImage(app);
|
||||
const { imageUri, buildLog } = await this.buildService.buildImage(app, deploymentId);
|
||||
|
||||
// Save build log
|
||||
await this.deploymentsRepository.update(deploymentId, { buildLog });
|
||||
@@ -56,15 +56,23 @@ export class DeploymentsService {
|
||||
|
||||
// Step 3: Deploy to Kubernetes
|
||||
await this.updateStatus(deploymentId, DeploymentStatus.DEPLOYING);
|
||||
const k8sResources = await this.kubernetesService.deployApplication(app, imageUri);
|
||||
this.buildService.setProgress(deploymentId, {
|
||||
phase: 'deploying',
|
||||
percent: 92,
|
||||
message: 'Deploying to Kubernetes...',
|
||||
});
|
||||
|
||||
// If a DB dump will be restored, deploy with 0 replicas first so WordPress
|
||||
// does not initialize empty tables before the dump is imported.
|
||||
const hasDbDump = app.dbDumpPath && fs.existsSync(app.dbDumpPath);
|
||||
const deployApp = hasDbDump ? { ...app, replicas: 0 } : app;
|
||||
const k8sResources = await this.kubernetesService.deployApplication(deployApp, imageUri);
|
||||
|
||||
// Step 3.5: Restore DB dump if one was uploaded (must happen after deploy creates the namespace + DB)
|
||||
if (app.dbDumpPath && fs.existsSync(app.dbDumpPath)) {
|
||||
if (hasDbDump) {
|
||||
this.logger.log(`Restoring DB dump for ${app.name} from ${app.dbDumpPath}`);
|
||||
try {
|
||||
// Wait for the database pod to be Ready before restoring
|
||||
await this.kubernetesService.waitForDatabaseReady(app, 120_000);
|
||||
// Re-fetch app to ensure we have latest data
|
||||
const freshApp = await this.applicationsService.findOne(app.id);
|
||||
const result = await this.kubernetesService.restoreDatabaseDump(freshApp, freshApp.dbDumpPath!);
|
||||
if (result.success) {
|
||||
@@ -74,18 +82,43 @@ export class DeploymentsService {
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`DB dump restore error for ${app.name}: ${e.message}`);
|
||||
// Don't fail the deployment — DB restore is a best-effort step
|
||||
}
|
||||
// Scale WordPress app up after restore (or even if restore failed)
|
||||
try {
|
||||
await this.kubernetesService.scaleDeployment(app, app.replicas || 1);
|
||||
this.logger.log(`Scaled ${app.name} to ${app.replicas || 1} replica(s) after DB restore`);
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`Failed to scale up ${app.name} after DB restore: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Mark success
|
||||
this.buildService.setProgress(deploymentId, {
|
||||
phase: 'done',
|
||||
percent: 100,
|
||||
message: 'Deployment complete',
|
||||
});
|
||||
await this.deploymentsRepository.update(deploymentId, {
|
||||
status: DeploymentStatus.RUNNING,
|
||||
k8sResources,
|
||||
finishedAt: new Date(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error instanceof BuildCancelledError || error?.name === 'BuildCancelledError') {
|
||||
this.logger.log(`Deployment ${deploymentId} cancelled by user`);
|
||||
await this.deploymentsRepository.update(deploymentId, {
|
||||
status: DeploymentStatus.FAILED,
|
||||
errorMessage: 'Cancelled by user',
|
||||
finishedAt: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.logger.error(`Deployment ${deploymentId} failed:`, error);
|
||||
this.buildService.setProgress(deploymentId, {
|
||||
phase: 'failed',
|
||||
percent: 0,
|
||||
message: error.message || 'Deployment failed',
|
||||
});
|
||||
|
||||
// Save build log if available (attached by build service on failure)
|
||||
const buildLog = error.buildLog || null;
|
||||
@@ -146,6 +179,65 @@ export class DeploymentsService {
|
||||
};
|
||||
}
|
||||
|
||||
async getBuildProgress(applicationId: string, userId: string): Promise<BuildProgress | null> {
|
||||
await this.applicationsService.findOne(applicationId, userId);
|
||||
|
||||
const latest = await this.deploymentsRepository.findOne({
|
||||
where: { applicationId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (!latest) return null;
|
||||
|
||||
const progress = this.buildService.getProgress(latest.id);
|
||||
if (progress) return progress;
|
||||
|
||||
// No in-memory progress — infer from deployment status
|
||||
if (latest.status === DeploymentStatus.RUNNING) {
|
||||
return { phase: 'done', percent: 100, message: 'Deployment complete' };
|
||||
}
|
||||
if (latest.status === DeploymentStatus.FAILED) {
|
||||
return { phase: 'failed', percent: 0, message: latest.errorMessage || 'Deployment failed' };
|
||||
}
|
||||
if (latest.status === DeploymentStatus.BUILDING) {
|
||||
return { phase: 'building', percent: 0, message: 'Building...' };
|
||||
}
|
||||
if (latest.status === DeploymentStatus.DEPLOYING) {
|
||||
return { phase: 'deploying', percent: 90, message: 'Deploying...' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async cancelDeployment(applicationId: string, userId: string): Promise<Deployment> {
|
||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
||||
|
||||
const latest = await this.deploymentsRepository.findOne({
|
||||
where: { applicationId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (!latest) {
|
||||
throw new NotFoundException('No deployment found');
|
||||
}
|
||||
|
||||
const inProgress = [
|
||||
DeploymentStatus.PENDING,
|
||||
DeploymentStatus.BUILDING,
|
||||
DeploymentStatus.DEPLOYING,
|
||||
];
|
||||
if (!inProgress.includes(latest.status as DeploymentStatus)) {
|
||||
throw new BadRequestException('No deployment in progress to cancel');
|
||||
}
|
||||
|
||||
await this.buildService.cancelBuild(latest.id);
|
||||
await this.buildService.cleanupBuildResourcesForApp(app);
|
||||
|
||||
latest.status = DeploymentStatus.FAILED;
|
||||
latest.errorMessage = 'Cancelled by user';
|
||||
latest.finishedAt = new Date();
|
||||
return this.deploymentsRepository.save(latest);
|
||||
}
|
||||
|
||||
async stopDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
||||
await this.kubernetesService.scaleDeployment(app, 0);
|
||||
|
||||
Reference in New Issue
Block a user