fix: 첫 빌드 전 닫힌 서버도 패치 정책으로 오픈 요청을 받는다
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type { GatewayProfileRecord } from '../orchestrator/profileRepository.js';
|
||||
import { readProfileReleaseSource } from '../orchestrator/profileReleaseSource.js';
|
||||
import { resolveGitBranchCommitSha, resolveGitCommitSha } from '../scenario/scenarioCatalog.js';
|
||||
|
||||
// 이미 배포된 서버는 그 catalog를 보존하고, 첫 오픈 전 서버만 운영자가 정한 source를 사용한다.
|
||||
// 일반 사용자는 이 조회에도 branch/commit을 전달할 수 없다.
|
||||
export const resolveOpeningCatalogCommit = async (
|
||||
profile: Pick<GatewayProfileRecord, 'meta' | 'buildCommitSha'>
|
||||
): Promise<string> => {
|
||||
if (profile.buildCommitSha?.trim()) return profile.buildCommitSha.trim();
|
||||
const source = readProfileReleaseSource(profile);
|
||||
if (!source) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '서버의 배포 버전 또는 패치 정책이 필요합니다.' });
|
||||
}
|
||||
try {
|
||||
return source.mode === 'BRANCH'
|
||||
? await resolveGitBranchCommitSha(source.ref)
|
||||
: await resolveGitCommitSha(source.ref);
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message: '서버 패치 정책의 시나리오 버전을 확인하지 못했습니다.',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '../scenario/scenarioCatalog.js';
|
||||
import { canRequestOpening, openingSeasonKey, readOpenRequestPolicy, type OpenRequestPolicy } from './policy.js';
|
||||
import { z } from 'zod';
|
||||
import { resolveOpeningCatalogCommit } from './catalogSource.js';
|
||||
|
||||
export const zOpeningInstall = zOperationInstallOptions.required({ preopenAt: true, openAt: true }).strict();
|
||||
export type OpeningInstall = z.infer<typeof zOpeningInstall>;
|
||||
@@ -200,8 +201,11 @@ export const createOpenRequest = async (
|
||||
}
|
||||
if (old.filter((request) => activeStatus(request, profile, now) === 'PENDING').length >= 100)
|
||||
return fail('이 서버의 대기 요청이 너무 많습니다.', 'CONFLICT');
|
||||
if (!profile.buildCommitSha) return fail('서버의 시나리오 버전이 준비되지 않았습니다.');
|
||||
const scenario = (await listScenarioPreviews({ gitRef: profile.buildCommitSha })).find(
|
||||
const catalogCommit = await resolveOpeningCatalogCommit({
|
||||
meta: profile.meta as GatewayPrisma.JsonObject,
|
||||
buildCommitSha: profile.buildCommitSha ?? undefined,
|
||||
});
|
||||
const scenario = (await listScenarioPreviews({ gitRef: catalogCommit })).find(
|
||||
(entry) => entry.id === install.scenarioId
|
||||
);
|
||||
if (!scenario) return fail('현재 서버에 없는 시나리오입니다.');
|
||||
|
||||
@@ -12,6 +12,7 @@ import { toPublicUser } from './auth/userRepository.js';
|
||||
import type { UserOAuthInfo, UserRecord } from './auth/userRepository.js';
|
||||
import { adminRouter } from './adminRouter.js';
|
||||
import { openRequestsRouter } from './openRequests/router.js';
|
||||
import { resolveOpeningCatalogCommit } from './openRequests/catalogSource.js';
|
||||
import { accountRouter } from './account/router.js';
|
||||
import {
|
||||
hasActiveSpecialAccountGrant,
|
||||
@@ -217,16 +218,11 @@ export const appRouter = router({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
|
||||
}
|
||||
const profile = await ctx.profiles.getProfile(input.profileName);
|
||||
const activeBuildCommit = profile?.buildCommitSha?.trim();
|
||||
if (!profile || !activeBuildCommit) {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message: 'The profile has no active build commit.',
|
||||
});
|
||||
}
|
||||
if (!profile) throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
|
||||
const catalogCommit = await resolveOpeningCatalogCommit(profile);
|
||||
|
||||
try {
|
||||
return await listScenarioPreviews({ gitRef: activeBuildCommit });
|
||||
return await listScenarioPreviews({ gitRef: catalogCommit });
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
|
||||
@@ -209,6 +209,17 @@ suite('durable opening requests (isolated PostgreSQL and HTTP)', () => {
|
||||
expect((await listOpenRequests(context(), await user(2), profileName)).requests).toHaveLength(1);
|
||||
expect(JSON.stringify(await listOpenRequests(context(), await user(2), profileName))).not.toContain('password');
|
||||
});
|
||||
it('supports the first opening of a closed server using its configured source without an active build', async () => {
|
||||
await prisma.gatewayProfile.update({
|
||||
where: { profileName },
|
||||
data: { buildCommitSha: null, meta: { releaseSource: { mode: 'BRANCH', ref: 'main' } } },
|
||||
});
|
||||
expect(await (await caller(1)).lobby.scenarios({ profileName })).toHaveLength(1);
|
||||
expect(catalog.resolveGitBranchCommitSha).toHaveBeenCalledWith('main');
|
||||
const request = await create();
|
||||
await (await caller(3)).admin.openRequests.decide({ id: request.id, action: 'ACCEPT' });
|
||||
expect((await prisma.gatewayOperation.findFirstOrThrow({ where: { profileName } })).sourceMode).toBe('BRANCH');
|
||||
});
|
||||
it('uses the configured lifetime for distant proposals', async () => {
|
||||
await policy(null, { lifetimeDays: 2 });
|
||||
const input = install();
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
않는다. 정책 저장은 `admin.profiles.settings` 권한이 필요하다. 기존 superuser
|
||||
판정과 profile scope 검사를 그대로 사용하며 운영 mutation은 감사 원장에 남긴다.
|
||||
|
||||
배포 이력이 있는 서버의 시나리오는 active build에서 읽는다. 아직 첫 build가 없는
|
||||
닫힌 서버는 운영자가 설정한 releaseSource에서 시나리오를 읽어 첫 오픈도 요청할 수
|
||||
있다. active build와 patch 정책이 모두 없으면 운영 설정 준비를 요구한다.
|
||||
|
||||
## 서버 정책
|
||||
|
||||
정책은 `GatewayProfile.meta.openRequestPolicy`에 저장한다. 요청 화면의 권한자용
|
||||
|
||||
Reference in New Issue
Block a user