This commit is contained in:
@@ -71,6 +71,11 @@ export class LinkLauncherVersionSourceDto {
|
||||
}
|
||||
|
||||
|
||||
export class DeployLauncherBackendDto {
|
||||
@IsString()
|
||||
image!: string;
|
||||
}
|
||||
|
||||
export class CreateLauncherBuildJobDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
||||
|
||||
import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, CreateLauncherCommonSettingDto, CreateLauncherVersionDto, UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, MoveLauncherAppLinkDto, UpdateLauncherAppLinkDto, UpdateLauncherBuildEnvDto, UpdateLauncherCommonSettingDto, UpdateLauncherUserAppPermissionsDto } from './dto';
|
||||
import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, CreateLauncherCommonSettingDto, CreateLauncherVersionDto, UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, MoveLauncherAppLinkDto, UpdateLauncherAppLinkDto, UpdateLauncherBuildEnvDto, UpdateLauncherCommonSettingDto, UpdateLauncherUserAppPermissionsDto, DeployLauncherBackendDto } from './dto';
|
||||
import { LauncherService } from './launcher.service';
|
||||
|
||||
@Controller('launcher')
|
||||
@@ -38,6 +38,16 @@ export class LauncherController {
|
||||
return this.launcherService.updateBuildEnv(body);
|
||||
}
|
||||
|
||||
@Get('backend-deployment')
|
||||
getBackendDeployment() {
|
||||
return this.launcherService.getBackendDeployment();
|
||||
}
|
||||
|
||||
@Post('backend-deployment')
|
||||
deployBackend(@Body() body: DeployLauncherBackendDto) {
|
||||
return this.launcherService.deployBackend(body);
|
||||
}
|
||||
|
||||
@Get('builds')
|
||||
listBuildJobs() {
|
||||
return this.launcherService.listBuildJobs();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { exec } from 'node:child_process';
|
||||
import { exec, execFile } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { copyFile, mkdir, readFile, rm, stat } from 'node:fs/promises';
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CreateLauncherBuildJobDto,
|
||||
CreateLauncherCommonSettingDto,
|
||||
CreateLauncherVersionDto,
|
||||
DeployLauncherBackendDto,
|
||||
UpdateLauncherSourceDto,
|
||||
LinkLauncherVersionSourceDto,
|
||||
UpdateLauncherAppLinkDto,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
} from './dto';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
const LAUNCHER_BUILD_ENV_PREFIX = 'launcher.client.build.env.';
|
||||
const DEFAULT_LAUNCHER_CLIENT_BUILD_ENV = [
|
||||
{
|
||||
@@ -344,6 +346,86 @@ export class LauncherService {
|
||||
return this.listBuildEnv();
|
||||
}
|
||||
|
||||
|
||||
async getBackendDeployment() {
|
||||
const namespace = this.configService.get<string>('LAUNCHER_BACKEND_NAMESPACE', 'studio-system');
|
||||
const deployment = this.configService.get<string>('LAUNCHER_BACKEND_DEPLOYMENT', 'studio-launcher-be');
|
||||
const container = this.configService.get<string>('LAUNCHER_BACKEND_CONTAINER', deployment);
|
||||
const source = await this.getSourceSettings();
|
||||
const latestBuild = await this.prisma.launcherBuildJob.findFirst({
|
||||
where: { target: 'launcher-backend', status: 'SUCCEEDED' },
|
||||
orderBy: { finishedAt: 'desc' },
|
||||
});
|
||||
|
||||
let currentImage: string | null = null;
|
||||
let available = false;
|
||||
try {
|
||||
currentImage = (await this.kubectl([
|
||||
'get',
|
||||
'deployment',
|
||||
deployment,
|
||||
'-n',
|
||||
namespace,
|
||||
'-o',
|
||||
`jsonpath={.spec.template.spec.containers[?(@.name=="${container}")].image}`,
|
||||
])).trim() || null;
|
||||
available = true;
|
||||
} catch {
|
||||
currentImage = null;
|
||||
}
|
||||
|
||||
return {
|
||||
namespace,
|
||||
deployment,
|
||||
container,
|
||||
currentImage,
|
||||
available,
|
||||
source: source.backend,
|
||||
latestBuild,
|
||||
};
|
||||
}
|
||||
|
||||
async deployBackend(body: DeployLauncherBackendDto) {
|
||||
const image = body.image?.trim();
|
||||
if (!image) throw new BadRequestException('배포할 이미지가 없습니다.');
|
||||
|
||||
const namespace = this.configService.get<string>('LAUNCHER_BACKEND_NAMESPACE', 'studio-system');
|
||||
const deployment = this.configService.get<string>('LAUNCHER_BACKEND_DEPLOYMENT', 'studio-launcher-be');
|
||||
const container = this.configService.get<string>('LAUNCHER_BACKEND_CONTAINER', deployment);
|
||||
const logs: string[] = [];
|
||||
|
||||
logs.push(`[running] launcher backend deploy`);
|
||||
logs.push(`[target] ${namespace}/deployment/${deployment}`);
|
||||
logs.push(`[set-image] ${container}=${image}`);
|
||||
appendOutput(logs, await this.kubectl([
|
||||
'set',
|
||||
'image',
|
||||
`deployment/${deployment}`,
|
||||
`${container}=${image}`,
|
||||
'-n',
|
||||
namespace,
|
||||
]));
|
||||
logs.push(`[rollout] deployment/${deployment}`);
|
||||
appendOutput(logs, await this.kubectl([
|
||||
'rollout',
|
||||
'status',
|
||||
`deployment/${deployment}`,
|
||||
'-n',
|
||||
namespace,
|
||||
'--timeout=180s',
|
||||
]));
|
||||
logs.push('[done] launcher backend deployed');
|
||||
|
||||
return {
|
||||
deployed: true,
|
||||
namespace,
|
||||
deployment,
|
||||
container,
|
||||
image,
|
||||
logs: logs.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
listBuildJobs() {
|
||||
return this.prisma.launcherBuildJob.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -1218,6 +1300,28 @@ export class LauncherService {
|
||||
return this.r2Client;
|
||||
}
|
||||
|
||||
|
||||
private async kubectl(args: string[]) {
|
||||
const kubectlBin = this.configService.get<string>('KUBECTL_BIN', 'kubectl');
|
||||
const env = await this.kubectlEnv();
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(kubectlBin, args, {
|
||||
env,
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
});
|
||||
return [stdout, stderr].filter(Boolean).join('\n').trim();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'kubectl failed';
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async kubectlEnv() {
|
||||
const kubeconfig = this.configService.get<string>('KUBECONFIG');
|
||||
if (!kubeconfig) return process.env;
|
||||
return { ...process.env, KUBECONFIG: kubeconfig };
|
||||
}
|
||||
|
||||
private async readPackageVersion(workDir: string) {
|
||||
try {
|
||||
const packageJson = await readFile(`${workDir}/package.json`, 'utf8');
|
||||
@@ -1361,6 +1465,11 @@ function mergeBuildEnv(
|
||||
return nextEnv;
|
||||
}
|
||||
|
||||
function appendOutput(logs: string[], output?: string) {
|
||||
if (!output) return;
|
||||
logs.push(...output.split('\n').filter(Boolean).map((line) => `[out] ${line}`));
|
||||
}
|
||||
|
||||
function trimOptional(value?: string) {
|
||||
return value === undefined ? null : value.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user