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";
|
ALTER TABLE IF EXISTS "launcher_common_settings" RENAME TO "launcher_settings";
|
||||||
|
|
||||||
COMMENT ON TABLE "launcher_settings" IS '런처 소스, 빌드 환경 등 내부 설정';
|
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"
|
CREATE INDEX IF NOT EXISTS "admin_roles_is_active_sort_order_idx"
|
||||||
ON "admin_roles"("is_active", "sort_order");
|
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
|
VALUES
|
||||||
('OWNER', 'OWNER', '전체 관리자 권한', 1, true),
|
(gen_random_uuid(), 'OWNER', 'OWNER', '전체 관리자 권한', 1, true, CURRENT_TIMESTAMP),
|
||||||
('ADMIN', 'ADMIN', '관리자 권한', 2, true),
|
(gen_random_uuid(), 'ADMIN', 'ADMIN', '관리자 권한', 2, true, CURRENT_TIMESTAMP),
|
||||||
('VIEWER', 'VIEWER', '조회 권한', 3, true)
|
(gen_random_uuid(), 'VIEWER', 'VIEWER', '조회 권한', 3, true, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT ("code") DO NOTHING;
|
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?
|
logs String?
|
||||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||||
finishedAt DateTime? @map("finished_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)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
|
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([componentId, createdAt])
|
@@index([componentId, createdAt])
|
||||||
|
@@index([releaseCandidateHiddenAt])
|
||||||
@@map("build_jobs")
|
@@map("build_jobs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,8 +77,9 @@ export class AppsController {
|
|||||||
@Query('limit') limit?: string,
|
@Query('limit') limit?: string,
|
||||||
@Query('since') since?: string,
|
@Query('since') since?: string,
|
||||||
@Query('status') status?: 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')
|
@Post('builds')
|
||||||
@@ -91,6 +92,11 @@ export class AppsController {
|
|||||||
return this.appsService.cancelBuildJob(buildJobId);
|
return this.appsService.cancelBuildJob(buildJobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('builds/:buildJobId/hide-release-candidate')
|
||||||
|
hideReleaseCandidate(@Param('buildJobId') buildJobId: string) {
|
||||||
|
return this.appsService.hideReleaseCandidate(buildJobId);
|
||||||
|
}
|
||||||
|
|
||||||
@Delete('builds/:buildJobId')
|
@Delete('builds/:buildJobId')
|
||||||
deleteBuildJob(@Param('buildJobId') buildJobId: string) {
|
deleteBuildJob(@Param('buildJobId') buildJobId: string) {
|
||||||
return this.appsService.deleteBuildJob(buildJobId);
|
return this.appsService.deleteBuildJob(buildJobId);
|
||||||
@@ -132,6 +138,11 @@ export class AppsController {
|
|||||||
return this.appsService.createRelease(body);
|
return this.appsService.createRelease(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('releases/:releaseId')
|
||||||
|
deleteRelease(@Param('releaseId') releaseId: string) {
|
||||||
|
return this.appsService.deleteRelease(releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('deployments')
|
@Get('deployments')
|
||||||
listDeployments(
|
listDeployments(
|
||||||
@Query('appKey') appKey?: string,
|
@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) {
|
async listMigrationJobs(appId?: string, page?: string, limit?: string, since?: string) {
|
||||||
const pagination = parsePagination(page, limit);
|
const pagination = parsePagination(page, limit);
|
||||||
const sinceDate = parseSinceDate(since);
|
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 pagination = parsePagination(page, limit);
|
||||||
const sinceDate = parseSinceDate(since);
|
const sinceDate = parseSinceDate(since);
|
||||||
|
const excludeHiddenReleaseCandidates = hideReleaseCandidates === 'true';
|
||||||
const where: Prisma.BuildJobWhereInput = {
|
const where: Prisma.BuildJobWhereInput = {
|
||||||
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
||||||
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
||||||
...(status ? { status: normalizeJobStatus(status) } : {}),
|
...(status ? { status: normalizeJobStatus(status) } : {}),
|
||||||
|
...(excludeHiddenReleaseCandidates ? { releaseCandidateHiddenAt: null } : {}),
|
||||||
};
|
};
|
||||||
const include = {
|
const include = {
|
||||||
component: {
|
component: {
|
||||||
@@ -1347,6 +1379,7 @@ export class AppsService {
|
|||||||
where: {
|
where: {
|
||||||
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
||||||
...(status ? { status: normalizeJobStatus(status) } : {}),
|
...(status ? { status: normalizeJobStatus(status) } : {}),
|
||||||
|
...(excludeHiddenReleaseCandidates ? { releaseCandidateHiddenAt: null } : {}),
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include,
|
include,
|
||||||
@@ -1941,6 +1974,44 @@ export class AppsService {
|
|||||||
return { deleted: true };
|
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[]) {
|
private async kubectl(args: string[]) {
|
||||||
const kubectlBin = this.configService.get<string>('KUBECTL_BIN', 'kubectl');
|
const kubectlBin = this.configService.get<string>('KUBECTL_BIN', 'kubectl');
|
||||||
const env = await this.kubectlEnv();
|
const env = await this.kubectlEnv();
|
||||||
|
|||||||
Reference in New Issue
Block a user