// @yakcloud/bff-kit — handlers/binding // 앱 ↔ 서비스 바인딩 생성. 같은 클러스터 강제(유일 격리점) + 별칭 중복 방지 → createBinding + 재배포. // Repo 는 DI. Response(accepted) 반환. yakconsole services/[id]/bindings/route.ts 하강. import { ApiError, accepted } from "../respond"; import { requireOwnership, type SessionUser } from "../session"; import type { BindingRepo } from "./types"; export interface CreateBindingArgs { serviceId: string; deploymentId: string; alias?: string | null; } // 소유·같은클러스터·READY·중복 검증 후 바인딩 생성 → 앱 재배포 큐잉 + 감사. export async function createServiceBinding( repo: BindingRepo, user: SessionUser, args: CreateBindingArgs, ): Promise { const svc = await repo.getService(args.serviceId); if (!svc || svc.deletedAt) throw new ApiError("NOT_FOUND"); requireOwnership(await repo.getCluster(svc.clusterId), user); const dep = await repo.getDeployment(args.deploymentId); if (!dep) throw new ApiError("NOT_FOUND", "앱을 찾을 수 없습니다."); // LOCKED#1: 같은 클러스터의 앱에만 연결(크로스클러스터 차단) — 유일한 격리 강제점. if (dep.clusterId !== svc.clusterId) { throw new ApiError("VALIDATION_FAILED", "같은 클러스터의 앱에만 연결할 수 있습니다."); } if (svc.status !== "READY") { throw new ApiError("VALIDATION_FAILED", "서비스가 준비(READY)된 후에 연결할 수 있습니다."); } const alias = args.alias ?? svc.name; const existing = await repo.listBindingsByDeployment(dep.id); if (existing.some((b) => b.serviceId === svc.id)) { throw new ApiError("NAME_TAKEN", "이미 이 앱에 연결된 서비스입니다."); } if (existing.some((b) => b.alias === alias)) { throw new ApiError("NAME_TAKEN", "이미 사용 중인 별칭입니다."); } const binding = await repo.createBinding({ serviceId: svc.id, deploymentId: dep.id, alias, injectedKeys: [], // 실제 주입 키(BIND_ENV_VARS 파생)는 배포 시 백엔드가 도출. }); // 바인딩 반영 = 재배포(deployment_manifest 가 binds 를 다시 엮음). await repo.updateDeployment(dep.id, { status: "QUEUED", statusMessage: null }); await repo.enqueueJob({ kind: "app.deploy", payload: { deploymentId: dep.id } }); await repo.addAudit({ userId: user.id, action: "service.bind", targetType: "service", targetId: svc.id, meta: { deploymentId: dep.id, app: dep.name, alias }, }); return accepted({ id: binding.id, alias, serviceId: svc.id, deploymentId: dep.id }); }