This commit is contained in:
@@ -71,6 +71,11 @@ export class LinkLauncherVersionSourceDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class DeployLauncherBackendDto {
|
||||||
|
@IsString()
|
||||||
|
image!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateLauncherBuildJobDto {
|
export class CreateLauncherBuildJobDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
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';
|
import { LauncherService } from './launcher.service';
|
||||||
|
|
||||||
@Controller('launcher')
|
@Controller('launcher')
|
||||||
@@ -38,6 +38,16 @@ export class LauncherController {
|
|||||||
return this.launcherService.updateBuildEnv(body);
|
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')
|
@Get('builds')
|
||||||
listBuildJobs() {
|
listBuildJobs() {
|
||||||
return this.launcherService.listBuildJobs();
|
return this.launcherService.listBuildJobs();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { exec } from 'node:child_process';
|
import { exec, execFile } from 'node:child_process';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
import { copyFile, mkdir, readFile, rm, stat } from 'node:fs/promises';
|
import { copyFile, mkdir, readFile, rm, stat } from 'node:fs/promises';
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CreateLauncherBuildJobDto,
|
CreateLauncherBuildJobDto,
|
||||||
CreateLauncherCommonSettingDto,
|
CreateLauncherCommonSettingDto,
|
||||||
CreateLauncherVersionDto,
|
CreateLauncherVersionDto,
|
||||||
|
DeployLauncherBackendDto,
|
||||||
UpdateLauncherSourceDto,
|
UpdateLauncherSourceDto,
|
||||||
LinkLauncherVersionSourceDto,
|
LinkLauncherVersionSourceDto,
|
||||||
UpdateLauncherAppLinkDto,
|
UpdateLauncherAppLinkDto,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
} from './dto';
|
} from './dto';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
const LAUNCHER_BUILD_ENV_PREFIX = 'launcher.client.build.env.';
|
const LAUNCHER_BUILD_ENV_PREFIX = 'launcher.client.build.env.';
|
||||||
const DEFAULT_LAUNCHER_CLIENT_BUILD_ENV = [
|
const DEFAULT_LAUNCHER_CLIENT_BUILD_ENV = [
|
||||||
{
|
{
|
||||||
@@ -344,6 +346,86 @@ export class LauncherService {
|
|||||||
return this.listBuildEnv();
|
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() {
|
listBuildJobs() {
|
||||||
return this.prisma.launcherBuildJob.findMany({
|
return this.prisma.launcherBuildJob.findMany({
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
@@ -1218,6 +1300,28 @@ export class LauncherService {
|
|||||||
return this.r2Client;
|
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) {
|
private async readPackageVersion(workDir: string) {
|
||||||
try {
|
try {
|
||||||
const packageJson = await readFile(`${workDir}/package.json`, 'utf8');
|
const packageJson = await readFile(`${workDir}/package.json`, 'utf8');
|
||||||
@@ -1361,6 +1465,11 @@ function mergeBuildEnv(
|
|||||||
return nextEnv;
|
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) {
|
function trimOptional(value?: string) {
|
||||||
return value === undefined ? null : value.trim();
|
return value === undefined ? null : value.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user