support launcher backend source and image builds
This commit is contained in:
@@ -33,6 +33,10 @@ export class CreateLauncherVersionDto {
|
||||
}
|
||||
|
||||
export class UpdateLauncherSourceDto {
|
||||
@IsOptional()
|
||||
@IsIn(['launcher-client', 'launcher-backend'])
|
||||
target?: 'launcher-client' | 'launcher-backend';
|
||||
|
||||
@IsString()
|
||||
repository!: string;
|
||||
|
||||
@@ -43,6 +47,14 @@ export class UpdateLauncherSourceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
commitSha?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
image?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dockerfile?: string;
|
||||
}
|
||||
|
||||
export class LinkLauncherVersionSourceDto {
|
||||
|
||||
@@ -23,9 +23,14 @@ import {
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const LAUNCHER_SOURCE_SETTING_KEYS = {
|
||||
repository: 'launcher.source.repository',
|
||||
branch: 'launcher.source.branch',
|
||||
commitSha: 'launcher.source.commit_sha',
|
||||
repository: 'launcher.client.source.repository',
|
||||
branch: 'launcher.client.source.branch',
|
||||
commitSha: 'launcher.client.source.commit_sha',
|
||||
backendRepository: 'launcher.backend.source.repository',
|
||||
backendBranch: 'launcher.backend.source.branch',
|
||||
backendCommitSha: 'launcher.backend.source.commit_sha',
|
||||
backendImage: 'launcher.backend.image',
|
||||
backendDockerfile: 'launcher.backend.dockerfile',
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
@@ -101,11 +106,51 @@ export class LauncherService {
|
||||
}
|
||||
|
||||
async updateSource(body: UpdateLauncherSourceDto) {
|
||||
const target = body.target ?? 'launcher-client';
|
||||
const repository = normalizeRepository(body.repository);
|
||||
const branch = body.branch?.trim() || 'release';
|
||||
const commitSha = body.commitSha?.trim() || '';
|
||||
const isBackend = target === 'launcher-backend';
|
||||
const image = body.image?.trim() || this.configService.get<string>('LAUNCHER_BACKEND_IMAGE', 'localhost:5000/studio-launcher-be');
|
||||
const dockerfile = body.dockerfile?.trim() || 'Dockerfile';
|
||||
|
||||
await this.launcherPrisma.$transaction([
|
||||
const writes = isBackend ? [
|
||||
this.upsertCommonSettingByKey(
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendRepository,
|
||||
'런처 백엔드 소스 저장소',
|
||||
'source',
|
||||
repository,
|
||||
'런처 백엔드 컨테이너 빌드에 사용할 저장소',
|
||||
),
|
||||
this.upsertCommonSettingByKey(
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendBranch,
|
||||
'런처 백엔드 소스 브랜치',
|
||||
'source',
|
||||
branch,
|
||||
'런처 백엔드 컨테이너 빌드 기준 브랜치',
|
||||
),
|
||||
this.upsertCommonSettingByKey(
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendCommitSha,
|
||||
'런처 백엔드 소스 커밋',
|
||||
'source',
|
||||
commitSha,
|
||||
'비워두면 브랜치 최신 커밋 기준',
|
||||
),
|
||||
this.upsertCommonSettingByKey(
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendImage,
|
||||
'런처 백엔드 이미지',
|
||||
'source',
|
||||
image,
|
||||
'런처 백엔드 컨테이너 이미지 이름',
|
||||
),
|
||||
this.upsertCommonSettingByKey(
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendDockerfile,
|
||||
'런처 백엔드 Dockerfile',
|
||||
'source',
|
||||
dockerfile,
|
||||
'런처 백엔드 빌드에 사용할 Dockerfile 경로',
|
||||
),
|
||||
] : [
|
||||
this.upsertCommonSettingByKey(
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.repository,
|
||||
'런처 소스 저장소',
|
||||
@@ -127,9 +172,19 @@ export class LauncherService {
|
||||
commitSha,
|
||||
'비워두면 브랜치 최신 커밋 기준',
|
||||
),
|
||||
]);
|
||||
];
|
||||
|
||||
return { source: { repository, branch, commitSha: commitSha || null } };
|
||||
await this.launcherPrisma.$transaction(writes);
|
||||
|
||||
return {
|
||||
source: {
|
||||
repository,
|
||||
branch,
|
||||
commitSha: commitSha || null,
|
||||
image: isBackend ? image : undefined,
|
||||
dockerfile: isBackend ? dockerfile : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getSourceSettings() {
|
||||
@@ -140,6 +195,11 @@ export class LauncherService {
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.repository,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.branch,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.commitSha,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendRepository,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendBranch,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendCommitSha,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendImage,
|
||||
LAUNCHER_SOURCE_SETTING_KEYS.backendDockerfile,
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -148,11 +208,28 @@ export class LauncherService {
|
||||
const repository = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.repository) || null;
|
||||
const branch = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.branch) || 'release';
|
||||
const commitSha = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.commitSha) || null;
|
||||
const backendRepository = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendRepository) || null;
|
||||
const backendBranch = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendBranch) || 'release';
|
||||
const backendCommitSha = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendCommitSha) || null;
|
||||
const backendImage = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendImage) || this.configService.get<string>('LAUNCHER_BACKEND_IMAGE', 'localhost:5000/studio-launcher-be');
|
||||
const backendDockerfile = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendDockerfile) || 'Dockerfile';
|
||||
|
||||
return {
|
||||
repository,
|
||||
branch,
|
||||
commitSha,
|
||||
client: {
|
||||
repository,
|
||||
branch,
|
||||
commitSha,
|
||||
},
|
||||
backend: {
|
||||
repository: backendRepository,
|
||||
branch: backendBranch,
|
||||
commitSha: backendCommitSha,
|
||||
image: backendImage,
|
||||
dockerfile: backendDockerfile,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -690,27 +767,37 @@ export class LauncherService {
|
||||
if (!body.version?.trim()) throw new BadRequestException('버전을 입력하세요.');
|
||||
if (!body.artifact?.trim()) throw new BadRequestException('산출물 파일명을 입력하세요.');
|
||||
const source = await this.getSourceSettings();
|
||||
const repository = body.repository ? normalizeRepository(body.repository) : source.repository;
|
||||
const branch = body.branch?.trim() || source.branch || 'release';
|
||||
const commitSha = body.commitSha?.trim() || source.commitSha || null;
|
||||
const plan = this.createLauncherArtifactPlan(body.version.trim(), branch, commitSha, body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim());
|
||||
if (!plan.uploadPath) throw new BadRequestException('업로드 경로를 입력하세요.');
|
||||
if (!plan.downloadUrl) throw new BadRequestException('다운로드 URL을 입력하세요.');
|
||||
const target = body.target?.trim() || 'launcher-client';
|
||||
const isBackend = target === 'launcher-backend';
|
||||
const repository = body.repository
|
||||
? normalizeRepository(body.repository)
|
||||
: isBackend
|
||||
? source.backend.repository
|
||||
: source.repository;
|
||||
const branch = body.branch?.trim() || (isBackend ? source.backend.branch : source.branch) || 'release';
|
||||
const commitSha = body.commitSha?.trim() || (isBackend ? source.backend.commitSha : source.commitSha) || null;
|
||||
const plan = isBackend
|
||||
? this.createLauncherBackendImagePlan(body.version.trim(), body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim())
|
||||
: this.createLauncherArtifactPlan(body.version.trim(), branch, commitSha, body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim());
|
||||
if (!repository) throw new BadRequestException('소스 저장소가 연결되지 않았습니다.');
|
||||
if (!plan.uploadPath) throw new BadRequestException(isBackend ? 'Dockerfile 경로를 입력하세요.' : '업로드 경로를 입력하세요.');
|
||||
if (!plan.downloadUrl) throw new BadRequestException(isBackend ? '이미지 태그를 입력하세요.' : '다운로드 URL을 입력하세요.');
|
||||
|
||||
const logs = [
|
||||
`[queued] launcher build created: ${body.artifact.trim()}`,
|
||||
`[target] ${target}`,
|
||||
repository ? `[plan] clone ${this.toCloneUrl(repository)}` : '[plan] source repository is not linked',
|
||||
`[plan] checkout branch ${branch}`,
|
||||
commitSha ? `[plan] checkout commit ${commitSha}` : '[plan] use branch latest commit',
|
||||
`[plan] build deb package`,
|
||||
`[plan] upload ${body.artifact.trim()} to ${plan.uploadPath}`,
|
||||
`[plan] publish download url ${plan.downloadUrl}`,
|
||||
isBackend ? `[plan] build and push image ${plan.downloadUrl}` : `[plan] build deb package`,
|
||||
isBackend ? `[plan] dockerfile ${plan.uploadPath}` : `[plan] upload ${body.artifact.trim()} to ${plan.uploadPath}`,
|
||||
isBackend ? `[plan] image tag ${plan.downloadUrl}` : `[plan] publish download url ${plan.downloadUrl}`,
|
||||
'[pending] waiting for launcher build runner',
|
||||
].join('\n');
|
||||
|
||||
const job = await this.prisma.launcherBuildJob.create({
|
||||
data: {
|
||||
target: body.target?.trim() || 'launcher-client',
|
||||
target,
|
||||
repository,
|
||||
branch,
|
||||
commitSha,
|
||||
@@ -783,6 +870,30 @@ export class LauncherService {
|
||||
await runCommand(`git checkout ${quote(job.commitSha)}`, workDir, append);
|
||||
}
|
||||
|
||||
if (job.target === 'launcher-backend') {
|
||||
const dockerfile = job.uploadPath || 'Dockerfile';
|
||||
const imageTag = job.downloadUrl || job.artifact;
|
||||
await append(`[backend] dockerfile ${dockerfile}`);
|
||||
await append(`[backend] image ${imageTag}`);
|
||||
await runCommand(
|
||||
`docker build -t ${quote(imageTag)} -f ${quote(dockerfile)} .`,
|
||||
workDir,
|
||||
append,
|
||||
);
|
||||
await runCommand(`docker push ${quote(imageTag)}`, workDir, append);
|
||||
await append('[done] launcher backend image build completed');
|
||||
|
||||
await this.prisma.launcherBuildJob.update({
|
||||
where: { id: jobId },
|
||||
data: {
|
||||
status: 'SUCCEEDED',
|
||||
finishedAt: new Date(),
|
||||
logs,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const buildCommand = process.env.LAUNCHER_BUILD_COMMAND || 'npm ci && npm run build:linux';
|
||||
await runCommand(buildCommand, workDir, append);
|
||||
|
||||
@@ -848,6 +959,23 @@ export class LauncherService {
|
||||
};
|
||||
}
|
||||
|
||||
private createLauncherBackendImagePlan(
|
||||
version: string,
|
||||
artifact: string,
|
||||
dockerfile?: string,
|
||||
imageTag?: string,
|
||||
) {
|
||||
const registry = this.configService.get<string>('BUILD_REGISTRY', 'localhost:5000').replace(/\/$/, '');
|
||||
const imageName = artifact.includes(':')
|
||||
? artifact
|
||||
: `${registry}/${artifact.replace(/^\/+/, '')}:${version}`;
|
||||
|
||||
return {
|
||||
uploadPath: dockerfile || 'Dockerfile',
|
||||
downloadUrl: imageTag || imageName,
|
||||
};
|
||||
}
|
||||
|
||||
private async uploadLauncherArtifactIfConfigured(
|
||||
job: {
|
||||
version: string;
|
||||
|
||||
Reference in New Issue
Block a user