0438192f8e
- Backend: added redeployApplication() to DeploymentsService that creates a new deployment record and re-runs the full build+deploy pipeline - Backend: added POST /deployments/applications/:appId/redeploy endpoint - Frontend: added Redeploy button on app detail page, visible after first deploy when no build is in progress - For git-based apps: pulls latest code from repo on each redeploy - For zip-based apps: rebuilds from last uploaded source code
171 lines
6.2 KiB
TypeScript
171 lines
6.2 KiB
TypeScript
import { Injectable, NotFoundException, Logger, Inject, forwardRef } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
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 { DeploymentStatus } from '../common/enums';
|
|
|
|
@Injectable()
|
|
export class DeploymentsService {
|
|
private readonly logger = new Logger(DeploymentsService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(Deployment)
|
|
private deploymentsRepository: Repository<Deployment>,
|
|
@Inject(forwardRef(() => ApplicationsService))
|
|
private applicationsService: ApplicationsService,
|
|
private kubernetesService: KubernetesService,
|
|
private buildService: BuildService,
|
|
) {}
|
|
|
|
async triggerDeployment(applicationId: string, userId: string): Promise<Deployment> {
|
|
const app = await this.applicationsService.findOne(applicationId, userId);
|
|
|
|
// Create deployment record
|
|
const deployment = this.deploymentsRepository.create({
|
|
applicationId: app.id,
|
|
triggeredBy: userId,
|
|
imageTag: `${app.name}:${Date.now()}`,
|
|
status: DeploymentStatus.PENDING,
|
|
version: `v${Date.now()}`,
|
|
});
|
|
const saved = await this.deploymentsRepository.save(deployment);
|
|
|
|
// Trigger async build & deploy pipeline
|
|
this.executePipeline(saved.id, app).catch((error) => {
|
|
this.logger.error(`Pipeline failed for deployment ${saved.id}:`, error);
|
|
});
|
|
|
|
return saved;
|
|
}
|
|
|
|
private async executePipeline(deploymentId: string, app: any): Promise<void> {
|
|
try {
|
|
// Step 1: Build image
|
|
await this.updateStatus(deploymentId, DeploymentStatus.BUILDING);
|
|
const imageUri = await this.buildService.buildImage(app);
|
|
|
|
// Step 2: Update app with new image tag
|
|
await this.applicationsService.updateImageTag(app.id, imageUri);
|
|
|
|
// Step 3: Deploy to Kubernetes
|
|
await this.updateStatus(deploymentId, DeploymentStatus.DEPLOYING);
|
|
const k8sResources = await this.kubernetesService.deployApplication(app, imageUri);
|
|
|
|
// Step 4: Mark success
|
|
await this.deploymentsRepository.update(deploymentId, {
|
|
status: DeploymentStatus.RUNNING,
|
|
k8sResources,
|
|
finishedAt: new Date(),
|
|
});
|
|
} catch (error: any) {
|
|
this.logger.error(`Deployment ${deploymentId} failed:`, error);
|
|
await this.deploymentsRepository.update(deploymentId, {
|
|
status: DeploymentStatus.FAILED,
|
|
errorMessage: error.message,
|
|
finishedAt: new Date(),
|
|
});
|
|
}
|
|
}
|
|
|
|
async updateStatus(id: string, status: DeploymentStatus): Promise<void> {
|
|
await this.deploymentsRepository.update(id, { status });
|
|
}
|
|
|
|
async findByApplication(applicationId: string): Promise<Deployment[]> {
|
|
return this.deploymentsRepository.find({
|
|
where: { applicationId },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
}
|
|
|
|
async findOne(id: string): Promise<Deployment> {
|
|
const deployment = await this.deploymentsRepository.findOne({
|
|
where: { id },
|
|
relations: ['application'],
|
|
});
|
|
if (!deployment) {
|
|
throw new NotFoundException('Deployment not found');
|
|
}
|
|
return deployment;
|
|
}
|
|
|
|
async getLogs(applicationId: string, userId: string): Promise<string> {
|
|
const app = await this.applicationsService.findOne(applicationId, userId);
|
|
return this.kubernetesService.getPodLogs(app);
|
|
}
|
|
|
|
async stopDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
|
const app = await this.applicationsService.findOne(applicationId, userId);
|
|
await this.kubernetesService.scaleDeployment(app, 0);
|
|
|
|
// Update the latest deployment status to stopped
|
|
const latest = await this.deploymentsRepository.findOne({
|
|
where: { applicationId },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
if (latest) {
|
|
latest.status = DeploymentStatus.STOPPED;
|
|
await this.deploymentsRepository.save(latest);
|
|
}
|
|
return latest;
|
|
}
|
|
|
|
async startDeployment(applicationId: string, userId: string): Promise<Deployment | null> {
|
|
const app = await this.applicationsService.findOne(applicationId, userId);
|
|
await this.kubernetesService.scaleDeployment(app, app.replicas || 1);
|
|
|
|
// Update the latest deployment status to running
|
|
const latest = await this.deploymentsRepository.findOne({
|
|
where: { applicationId },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
if (latest) {
|
|
latest.status = DeploymentStatus.RUNNING;
|
|
await this.deploymentsRepository.save(latest);
|
|
}
|
|
return latest;
|
|
}
|
|
|
|
async restartDeployment(applicationId: string, userId: string): Promise<void> {
|
|
const app = await this.applicationsService.findOne(applicationId, userId);
|
|
await this.kubernetesService.restartDeployment(app);
|
|
}
|
|
|
|
/**
|
|
* Redeploy: re-build from latest source (git pull or existing zip) and deploy new version.
|
|
* For git-based apps this pulls the latest code, for zip-based it rebuilds from last uploaded zip.
|
|
*/
|
|
async redeployApplication(applicationId: string, userId: string): Promise<Deployment> {
|
|
const app = await this.applicationsService.findOne(applicationId, userId);
|
|
|
|
if (!app.codePath && !app.gitUrl) {
|
|
throw new NotFoundException('No source code available. Upload code or set a git URL first.');
|
|
}
|
|
|
|
// Create new deployment record
|
|
const deployment = this.deploymentsRepository.create({
|
|
applicationId: app.id,
|
|
triggeredBy: userId,
|
|
imageTag: `${app.name}:${Date.now()}`,
|
|
status: DeploymentStatus.PENDING,
|
|
version: `v${Date.now()}`,
|
|
});
|
|
const saved = await this.deploymentsRepository.save(deployment);
|
|
|
|
// Trigger async build & deploy pipeline (same as initial deploy)
|
|
this.executePipeline(saved.id, app).catch((error) => {
|
|
this.logger.error(`Redeploy pipeline failed for deployment ${saved.id}:`, error);
|
|
});
|
|
|
|
this.logger.log(`Redeploy triggered for ${app.name} (${app.gitUrl ? 'git: ' + app.gitUrl : 'zip'})`);
|
|
return saved;
|
|
}
|
|
|
|
async deleteAllForApplication(applicationId: string): Promise<void> {
|
|
await this.deploymentsRepository.delete({ applicationId });
|
|
}
|
|
}
|