improve admin release and deployment safeguards
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
ALTER TABLE IF EXISTS "launcher_common_settings" RENAME TO "launcher_settings";
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('public.launcher_settings') IS NOT NULL THEN
|
||||
COMMENT ON TABLE "launcher_settings" IS '런처 소스, 빌드 환경 등 내부 설정';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@@ -24,9 +24,9 @@ CREATE UNIQUE INDEX IF NOT EXISTS "admin_roles_code_key"
|
||||
CREATE INDEX IF NOT EXISTS "admin_roles_is_active_sort_order_idx"
|
||||
ON "admin_roles"("is_active", "sort_order");
|
||||
|
||||
INSERT INTO "admin_roles" ("code", "name", "description", "sort_order", "is_active")
|
||||
INSERT INTO "admin_roles" ("id", "code", "name", "description", "sort_order", "is_active", "updated_at")
|
||||
VALUES
|
||||
('OWNER', 'OWNER', '전체 관리자 권한', 1, true),
|
||||
('ADMIN', 'ADMIN', '관리자 권한', 2, true),
|
||||
('VIEWER', 'VIEWER', '조회 권한', 3, true)
|
||||
(gen_random_uuid(), 'OWNER', 'OWNER', '전체 관리자 권한', 1, true, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), 'ADMIN', 'ADMIN', '관리자 권한', 2, true, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), 'VIEWER', 'VIEWER', '조회 권한', 3, true, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT ("code") DO NOTHING;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "build_jobs"
|
||||
ADD COLUMN IF NOT EXISTS "release_candidate_hidden_at" TIMESTAMPTZ(3);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "build_jobs_release_candidate_hidden_at_idx"
|
||||
ON "build_jobs"("release_candidate_hidden_at");
|
||||
@@ -220,10 +220,12 @@ model BuildJob {
|
||||
logs String?
|
||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||||
releaseCandidateHiddenAt DateTime? @map("release_candidate_hidden_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([componentId, createdAt])
|
||||
@@index([releaseCandidateHiddenAt])
|
||||
@@map("build_jobs")
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,9 @@ export class AppsController {
|
||||
@Query('limit') limit?: string,
|
||||
@Query('since') since?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('hideReleaseCandidates') hideReleaseCandidates?: string,
|
||||
) {
|
||||
return this.appsService.listBuildJobs(appKey, page, limit, since, status);
|
||||
return this.appsService.listBuildJobs(appKey, page, limit, since, status, hideReleaseCandidates);
|
||||
}
|
||||
|
||||
@Post('builds')
|
||||
@@ -91,6 +92,11 @@ export class AppsController {
|
||||
return this.appsService.cancelBuildJob(buildJobId);
|
||||
}
|
||||
|
||||
@Patch('builds/:buildJobId/hide-release-candidate')
|
||||
hideReleaseCandidate(@Param('buildJobId') buildJobId: string) {
|
||||
return this.appsService.hideReleaseCandidate(buildJobId);
|
||||
}
|
||||
|
||||
@Delete('builds/:buildJobId')
|
||||
deleteBuildJob(@Param('buildJobId') buildJobId: string) {
|
||||
return this.appsService.deleteBuildJob(buildJobId);
|
||||
@@ -132,6 +138,11 @@ export class AppsController {
|
||||
return this.appsService.createRelease(body);
|
||||
}
|
||||
|
||||
@Delete('releases/:releaseId')
|
||||
deleteRelease(@Param('releaseId') releaseId: string) {
|
||||
return this.appsService.deleteRelease(releaseId);
|
||||
}
|
||||
|
||||
@Get('deployments')
|
||||
listDeployments(
|
||||
@Query('appKey') appKey?: string,
|
||||
|
||||
@@ -1139,6 +1139,36 @@ export class AppsService {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRelease(releaseId: string) {
|
||||
const release = await this.prisma.appRelease.findUnique({
|
||||
where: { id: releaseId },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
deployments: {
|
||||
where: { status: 'SUCCEEDED' },
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!release) {
|
||||
throw new NotFoundException('릴리즈를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if (release.status === 'PENDING' || release.status === 'RUNNING') {
|
||||
throw new BadRequestException('진행 중인 릴리즈는 삭제할 수 없습니다.');
|
||||
}
|
||||
|
||||
if (release.deployments.length > 0) {
|
||||
throw new BadRequestException('이미 성공 배포된 릴리즈는 삭제할 수 없습니다.');
|
||||
}
|
||||
|
||||
await this.prisma.appRelease.delete({ where: { id: releaseId } });
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
async listMigrationJobs(appId?: string, page?: string, limit?: string, since?: string) {
|
||||
const pagination = parsePagination(page, limit);
|
||||
const sinceDate = parseSinceDate(since);
|
||||
@@ -1326,13 +1356,15 @@ export class AppsService {
|
||||
}
|
||||
}
|
||||
|
||||
async listBuildJobs(appKey?: string, page?: string, limit?: string, since?: string, status?: string) {
|
||||
async listBuildJobs(appKey?: string, page?: string, limit?: string, since?: string, status?: string, hideReleaseCandidates?: string) {
|
||||
const pagination = parsePagination(page, limit);
|
||||
const sinceDate = parseSinceDate(since);
|
||||
const excludeHiddenReleaseCandidates = hideReleaseCandidates === 'true';
|
||||
const where: Prisma.BuildJobWhereInput = {
|
||||
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
||||
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
||||
...(status ? { status: normalizeJobStatus(status) } : {}),
|
||||
...(excludeHiddenReleaseCandidates ? { releaseCandidateHiddenAt: null } : {}),
|
||||
};
|
||||
const include = {
|
||||
component: {
|
||||
@@ -1347,6 +1379,7 @@ export class AppsService {
|
||||
where: {
|
||||
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
||||
...(status ? { status: normalizeJobStatus(status) } : {}),
|
||||
...(excludeHiddenReleaseCandidates ? { releaseCandidateHiddenAt: null } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include,
|
||||
@@ -1941,6 +1974,44 @@ export class AppsService {
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
async hideReleaseCandidate(buildJobId: string) {
|
||||
const buildJob = await this.prisma.buildJob.findUnique({
|
||||
where: { id: buildJobId },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
releaseCandidateHiddenAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!buildJob) {
|
||||
throw new NotFoundException('빌드 작업을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if (buildJob.status === 'PENDING' || buildJob.status === 'RUNNING') {
|
||||
throw new BadRequestException('진행 중인 빌드는 릴리즈 후보에서 숨길 수 없습니다.');
|
||||
}
|
||||
|
||||
const usedReleases = await this.prisma.appRelease.findMany({
|
||||
select: { imageTags: true },
|
||||
});
|
||||
if (getUsedReleaseBuildIds(usedReleases).includes(buildJob.id)) {
|
||||
throw new BadRequestException('이미 릴리즈에 사용된 빌드는 후보에서 숨길 수 없습니다.');
|
||||
}
|
||||
|
||||
const updated = await this.prisma.buildJob.update({
|
||||
where: { id: buildJob.id },
|
||||
data: { releaseCandidateHiddenAt: buildJob.releaseCandidateHiddenAt ?? new Date() },
|
||||
include: {
|
||||
component: {
|
||||
include: { app: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { buildJob: updated };
|
||||
}
|
||||
|
||||
private async kubectl(args: string[]) {
|
||||
const kubectlBin = this.configService.get<string>('KUBECTL_BIN', 'kubectl');
|
||||
const env = await this.kubectlEnv();
|
||||
|
||||
Reference in New Issue
Block a user