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, @Inject(forwardRef(() => ApplicationsService)) private applicationsService: ApplicationsService, private kubernetesService: KubernetesService, private buildService: BuildService, ) {} async triggerDeployment(applicationId: string, userId: string): Promise { 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 { 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 { await this.deploymentsRepository.update(id, { status }); } async findByApplication(applicationId: string): Promise { return this.deploymentsRepository.find({ where: { applicationId }, order: { createdAt: 'DESC' }, }); } async findOne(id: string): Promise { 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 { const app = await this.applicationsService.findOne(applicationId, userId); return this.kubernetesService.getPodLogs(app); } async stopDeployment(applicationId: string, userId: string): Promise { 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 { 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 { 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 { 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 { await this.deploymentsRepository.delete({ applicationId }); } }