init
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { DeploymentsService } from './deployments.service';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
|
||||
@ApiTags('Deployments')
|
||||
@ApiBearerAuth()
|
||||
@Controller('deployments')
|
||||
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||
export class DeploymentsController {
|
||||
constructor(private readonly deploymentsService: DeploymentsService) {}
|
||||
|
||||
@Post('applications/:appId/deploy')
|
||||
@ApiOperation({ summary: 'Trigger a new deployment' })
|
||||
async triggerDeployment(@Param('appId') appId: string, @Request() req: any) {
|
||||
return this.deploymentsService.triggerDeployment(appId, req.user.id);
|
||||
}
|
||||
|
||||
@Get('applications/:appId')
|
||||
@ApiOperation({ summary: 'List deployments for an application' })
|
||||
async findByApplication(@Param('appId') appId: string) {
|
||||
return this.deploymentsService.findByApplication(appId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get deployment details' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.deploymentsService.findOne(id);
|
||||
}
|
||||
|
||||
@Get('applications/:appId/logs')
|
||||
@ApiOperation({ summary: 'Get application logs' })
|
||||
async getLogs(@Param('appId') appId: string, @Request() req: any) {
|
||||
return { logs: await this.deploymentsService.getLogs(appId, req.user.id) };
|
||||
}
|
||||
|
||||
@Post('applications/:appId/stop')
|
||||
@ApiOperation({ summary: 'Stop an application' })
|
||||
async stop(@Param('appId') appId: string, @Request() req: any) {
|
||||
const deployment = await this.deploymentsService.stopDeployment(appId, req.user.id);
|
||||
return { message: 'Application stopped', deployment };
|
||||
}
|
||||
|
||||
@Post('applications/:appId/start')
|
||||
@ApiOperation({ summary: 'Start a stopped application' })
|
||||
async start(@Param('appId') appId: string, @Request() req: any) {
|
||||
const deployment = await this.deploymentsService.startDeployment(appId, req.user.id);
|
||||
return { message: 'Application started', deployment };
|
||||
}
|
||||
|
||||
@Post('applications/:appId/restart')
|
||||
@ApiOperation({ summary: 'Restart an application' })
|
||||
async restart(@Param('appId') appId: string, @Request() req: any) {
|
||||
await this.deploymentsService.restartDeployment(appId, req.user.id);
|
||||
return { message: 'Application restarted' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DeploymentsService } from './deployments.service';
|
||||
import { DeploymentsController } from './deployments.controller';
|
||||
import { Deployment } from './entities/deployment.entity';
|
||||
import { ApplicationsModule } from '../applications/applications.module';
|
||||
import { KubernetesModule } from '../kubernetes/kubernetes.module';
|
||||
import { BuildModule } from '../build/build.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Deployment]),
|
||||
forwardRef(() => ApplicationsModule),
|
||||
KubernetesModule,
|
||||
BuildModule,
|
||||
],
|
||||
controllers: [DeploymentsController],
|
||||
providers: [DeploymentsService],
|
||||
exports: [DeploymentsService],
|
||||
})
|
||||
export class DeploymentsModule {}
|
||||
@@ -0,0 +1,140 @@
|
||||
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);
|
||||
}
|
||||
|
||||
async deleteAllForApplication(applicationId: string): Promise<void> {
|
||||
await this.deploymentsRepository.delete({ applicationId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { DeploymentStatus } from '../../common/enums';
|
||||
import { Application } from '../../applications/entities/application.entity';
|
||||
|
||||
@Entity('deployments')
|
||||
export class Deployment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'enum', enum: DeploymentStatus, default: DeploymentStatus.PENDING })
|
||||
status: DeploymentStatus;
|
||||
|
||||
@Column()
|
||||
imageTag: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
version: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
k8sResources: Record<string, any>; // Snapshot of generated K8s manifests
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
buildLog: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
deployLog: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
errorMessage: string;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Application, (app: Application) => app.deployments, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'applicationId' })
|
||||
application: Application;
|
||||
|
||||
@Column()
|
||||
applicationId: string;
|
||||
|
||||
@Column()
|
||||
triggeredBy: string; // userId who triggered the deployment
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
finishedAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user