feat(ds-sdk): B3 추출 — DTO·Zod·bind-env 9종·ApiCode (리프)
yakconsole → @yakcloud/ds-sdk 추출(11 모듈): - enums.ts: 상태/타입 리터럴 유니온 SSOT(값 배열 + type) - dto/: cluster·service·deployment·account·domain (비밀필드 제외, publicService 계약 동형) - bind-env.ts: BIND_ENV_VARS 9종(MONGODB..ORACLE) + SERVICE_DEFAULT_PORT + bindEnvPrefix/bindEnvKeys - schemas.ts: zod(생성/검증 body + DTO), api-codes.ts: ApiCode 유니온 + CODE_STATUS - zod ^4.4.3(소스 정합), 리프 규칙 준수(react/prisma/next/서버 import 0) 검증: 적대적 Verify=SHIP, bind-env 9종 Python _bind_env_for parity 완전 일치, 누출 0, tsc --noEmit 통과(strict+noUncheckedIndexedAccess+verbatimModuleSyntax). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -25,6 +25,6 @@
|
||||
"test": "echo \"(no tests yet)\""
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.0"
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"test": "echo \"(no tests yet)\""
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.0",
|
||||
"zod": "^4.4.3",
|
||||
"@yakcloud/ds-sdk": "workspace:*",
|
||||
"@yakcloud/auth-adapter": "workspace:*"
|
||||
},
|
||||
|
||||
@ -25,6 +25,6 @@
|
||||
"test": "echo \"(no tests yet)\""
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.0"
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
20
packages/ds-sdk/src/api-codes.ts
Normal file
20
packages/ds-sdk/src/api-codes.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// ApiCode 리터럴 유니온 + CODE_STATUS 맵(코드→HTTP 상태). yakconsole api/respond.ts L4-19 하강.
|
||||
// 순수 계약만 — DEFAULT_MSG(한국어 문구)·ApiError 클래스·ok/fail/handle/parseBody(서버 전용)는 제외.
|
||||
|
||||
// ── error codes → HTTP status (design.md §4.1) ──
|
||||
export const CODE_STATUS = {
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
VALIDATION_FAILED: 422,
|
||||
NAME_TAKEN: 409,
|
||||
CONFLICT: 409,
|
||||
NAME_RESERVED: 422,
|
||||
QUOTA_EXCEEDED: 403,
|
||||
CAPACITY_UNAVAILABLE: 503,
|
||||
RANCHER_UPSTREAM_ERROR: 502,
|
||||
RATE_LIMITED: 429,
|
||||
NOT_IMPLEMENTED: 501,
|
||||
INTERNAL: 500,
|
||||
} as const;
|
||||
export type ApiCode = keyof typeof CODE_STATUS;
|
||||
129
packages/ds-sdk/src/bind-env.ts
Normal file
129
packages/ds-sdk/src/bind-env.ts
Normal file
@ -0,0 +1,129 @@
|
||||
// bind-env 9종 계약 (BIND_ENV_VARS) — 앱 컨테이너에 주입되는 <PREFIX>_* 환경변수 명세.
|
||||
// 백엔드 _bind_env_for(infra/api/yakcloud_api.py) 미러. 실제 URL 조립의 진실은 서버이며
|
||||
// 여기는 접미사 집합 + 문서/UI 힌트(desc/example)만 노출한다(값 조립은 리프 패키지 대상 아님).
|
||||
//
|
||||
// PREFIX 도출: p = alias.upper().replace('-','_'). 아래 헬퍼 bindEnvKeys(alias,type) 가 동일 규칙으로
|
||||
// 실제 env 키 배열을 만든다. 소스를 연결하면 이 키들이 앱 파드 env 로 주입되어
|
||||
// 코드에서 process.env.<PREFIX>_URL 등으로 접속한다.
|
||||
import type { ServiceTypeDTO } from "./enums";
|
||||
|
||||
export type BindEnvVar = { suffix: string; desc: string; example: string };
|
||||
|
||||
export const BIND_ENV_VARS: Record<ServiceTypeDTO, readonly BindEnvVar[]> = {
|
||||
// mongodb://{quote(user)}:{quote(pw)}@{host}:{port}/{db}?authSource={db} — 기본포트 27017
|
||||
MONGODB: [
|
||||
{ suffix: "URL", desc: "연결 문자열", example: "mongodb://user:pass@host:27017/appdb?authSource=appdb" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "mongo.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "27017" },
|
||||
{ suffix: "USERNAME", desc: "사용자", example: "app" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호", example: "(자동 발급)" },
|
||||
{ suffix: "DB", desc: "데이터베이스", example: "appdb" },
|
||||
],
|
||||
// redis://:{quote(pw)}@{host}:{port}/{db} — USERNAME 은 항상 ""(requirepass 만) · 기본포트 6379
|
||||
REDIS: [
|
||||
{ suffix: "URL", desc: "연결 문자열", example: "redis://:pass@host:6379/0" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "redis.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "6379" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호(requirepass)", example: "(자동 발급)" },
|
||||
{ suffix: "DB", desc: "DB 번호", example: "0" },
|
||||
{ suffix: "USERNAME", desc: "사용자(Redis 는 빈값)", example: "" },
|
||||
],
|
||||
// {scheme}://{netloc} — USE_SSL(_YC_SSL||443), 표준포트 생략, REGION 하드코드 us-east-1
|
||||
// USERNAME==ACCESS_KEY, PASSWORD==SECRET_KEY, URL==ENDPOINT · 기본포트 9000
|
||||
MINIO: [
|
||||
{ suffix: "ENDPOINT", desc: "S3 엔드포인트", example: "https://s3.yakenator.io" },
|
||||
{ suffix: "URL", desc: "엔드포인트(ENDPOINT 와 동일)", example: "https://s3.yakenator.io" },
|
||||
{ suffix: "ACCESS_KEY", desc: "액세스 키", example: "(자동 발급)" },
|
||||
{ suffix: "SECRET_KEY", desc: "시크릿 키", example: "(자동 발급)" },
|
||||
{ suffix: "USERNAME", desc: "액세스 키 별칭", example: "(ACCESS_KEY 와 동일)" },
|
||||
{ suffix: "PASSWORD", desc: "시크릿 키 별칭", example: "(SECRET_KEY 와 동일)" },
|
||||
{ suffix: "BUCKET", desc: "버킷", example: "app-bucket" },
|
||||
{ suffix: "REGION", desc: "리전", example: "us-east-1" },
|
||||
{ suffix: "USE_SSL", desc: "TLS 사용 여부", example: "true" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "s3.yakenator.io" },
|
||||
{ suffix: "PORT", desc: "포트", example: "443" },
|
||||
],
|
||||
// mysql://{quote(user)}:{quote(pw)}@{host}:{port}/{db} — 기본포트 3306
|
||||
MYSQL: [
|
||||
{ suffix: "URL", desc: "연결 문자열", example: "mysql://user:pass@host:3306/appdb" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "mysql.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "3306" },
|
||||
{ suffix: "USERNAME", desc: "사용자", example: "app" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호", example: "(자동 발급)" },
|
||||
{ suffix: "DB", desc: "데이터베이스", example: "appdb" },
|
||||
],
|
||||
// postgresql://{quote(user)}:{quote(pw)}@{host}:{port}/{db} — 기본포트 5432
|
||||
POSTGRESQL: [
|
||||
{ suffix: "URL", desc: "연결 문자열", example: "postgresql://user:pass@host:5432/appdb" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "postgres.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "5432" },
|
||||
{ suffix: "USERNAME", desc: "사용자", example: "app" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호", example: "(자동 발급)" },
|
||||
{ suffix: "DB", desc: "데이터베이스", example: "appdb" },
|
||||
],
|
||||
// mysql://… (MySQL 와이어 호환이라 postgresql 아님) — 기본포트 3306
|
||||
MARIADB: [
|
||||
{ suffix: "URL", desc: "연결 문자열(MySQL 와이어 호환)", example: "mysql://user:pass@host:3306/appdb" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "mariadb.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "3306" },
|
||||
{ suffix: "USERNAME", desc: "사용자", example: "app" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호", example: "(자동 발급)" },
|
||||
{ suffix: "DB", desc: "데이터베이스", example: "appdb" },
|
||||
],
|
||||
// amqp://{quote(user)}:{quote(pw)}@{host}:{port}/{quote(vhost)} — MGMT_URL=http://{host}:15672 · 기본포트 5672
|
||||
RABBITMQ: [
|
||||
{ suffix: "URL", desc: "AMQP 연결 문자열", example: "amqp://user:pass@host:5672/vhost" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "rabbitmq.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "5672" },
|
||||
{ suffix: "USERNAME", desc: "사용자", example: "app" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호", example: "(자동 발급)" },
|
||||
{ suffix: "VHOST", desc: "가상 호스트", example: "/" },
|
||||
{ suffix: "MGMT_URL", desc: "관리 콘솔 URL", example: "http://host:15672" },
|
||||
],
|
||||
// URL={endpoint}/solr/{core}; ENDPOINT=http://{host}:{port} — 기본 무인증(USER/PASS="") · 기본포트 8983
|
||||
SOLR: [
|
||||
{ suffix: "URL", desc: "코어 URL", example: "http://host:8983/solr/appcore" },
|
||||
{ suffix: "ENDPOINT", desc: "엔드포인트", example: "http://host:8983" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "solr.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "8983" },
|
||||
{ suffix: "CORE", desc: "코어", example: "appcore" },
|
||||
{ suffix: "USERNAME", desc: "사용자(기본 무인증)", example: "" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호(기본 무인증)", example: "" },
|
||||
],
|
||||
// URL=oracle://…; JDBC_URL=jdbc:oracle:thin:@//{host}:{port}/{service}; DSN={host}:{port}/{service}
|
||||
// JDBC/DSN 은 자격증명 미포함 · 기본포트 1521
|
||||
ORACLE: [
|
||||
{ suffix: "URL", desc: "연결 문자열", example: "oracle://user:pass@host:1521/service" },
|
||||
{ suffix: "JDBC_URL", desc: "JDBC(thin) URL", example: "jdbc:oracle:thin:@//host:1521/service" },
|
||||
{ suffix: "DSN", desc: "간단 DSN", example: "host:1521/service" },
|
||||
{ suffix: "HOST", desc: "호스트", example: "oracle.yakcloud-services.svc.cluster.local" },
|
||||
{ suffix: "PORT", desc: "포트", example: "1521" },
|
||||
{ suffix: "USERNAME", desc: "사용자", example: "app" },
|
||||
{ suffix: "PASSWORD", desc: "비밀번호", example: "(자동 발급)" },
|
||||
{ suffix: "SERVICE", desc: "서비스명", example: "service" },
|
||||
],
|
||||
};
|
||||
|
||||
// 각 서비스 타입의 기본 포트 — 백엔드 _SVC_PORT 미러(실제 port 는 conn.port/_YC_PORT).
|
||||
export const SERVICE_DEFAULT_PORT: Record<ServiceTypeDTO, number> = {
|
||||
MONGODB: 27017,
|
||||
REDIS: 6379,
|
||||
MINIO: 9000,
|
||||
MYSQL: 3306,
|
||||
POSTGRESQL: 5432,
|
||||
MARIADB: 3306,
|
||||
RABBITMQ: 5672,
|
||||
SOLR: 8983,
|
||||
ORACLE: 1521,
|
||||
};
|
||||
|
||||
// alias → PREFIX 도출(백엔드와 동일 규칙): 대문자화 후 '-' → '_'.
|
||||
export function bindEnvPrefix(alias: string): string {
|
||||
return alias.toUpperCase().replace(/-/g, "_");
|
||||
}
|
||||
|
||||
// alias + type → 실제 주입되는 env 키 목록(<PREFIX>_<SUFFIX>).
|
||||
export function bindEnvKeys(alias: string, type: ServiceTypeDTO): string[] {
|
||||
const prefix = bindEnvPrefix(alias);
|
||||
return BIND_ENV_VARS[type].map((v) => `${prefix}_${v.suffix}`);
|
||||
}
|
||||
125
packages/ds-sdk/src/dto/account.ts
Normal file
125
packages/ds-sdk/src/dto/account.ts
Normal file
@ -0,0 +1,125 @@
|
||||
// 사용자/쿼터/사용량 DTO. 비밀 없음. 관리자 조인 확장 DTO 포함.
|
||||
import type { ClusterStatus, QuotaField, QuotaRequestStatusDTO, UserRole, UserStatus } from "../enums";
|
||||
|
||||
export interface UserDTO {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
locale: string;
|
||||
emailOptIn: boolean;
|
||||
plan: string; // 현재 활성 플랜 코드(free/standard/…)
|
||||
requestedPlan: string | null; // 승인 대기 중인 상향 플랜
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface QuotaDTO {
|
||||
maxClusters: number;
|
||||
maxNodesPerCluster: number;
|
||||
maxDomains: number;
|
||||
maxServices: number;
|
||||
}
|
||||
|
||||
export interface MeDTO {
|
||||
user: UserDTO;
|
||||
quota: QuotaDTO | null;
|
||||
usage: { clusters: number; nodes: number; domains: number };
|
||||
}
|
||||
|
||||
export interface QuotaRequestDTO {
|
||||
id: string;
|
||||
userId: string;
|
||||
field: QuotaField;
|
||||
currentValue: number;
|
||||
requestedValue: number;
|
||||
reason: string | null;
|
||||
status: QuotaRequestStatusDTO;
|
||||
decidedById: string | null;
|
||||
decidedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface QuotaRequestAdminDTO extends QuotaRequestDTO {
|
||||
user: { email: string; displayName: string };
|
||||
}
|
||||
|
||||
export interface PlanRequestAdminDTO {
|
||||
userId: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
currentPlan: string;
|
||||
requestedPlan: string;
|
||||
}
|
||||
|
||||
// GET /usage/summary — 계정 사용량 (카운트 + 할당 리소스 + 쿼터 + 클러스터별 분해)
|
||||
export interface UsageClusterDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
status: ClusterStatus;
|
||||
nodes: number;
|
||||
vcpu: number;
|
||||
memGb: number;
|
||||
diskGb: number;
|
||||
}
|
||||
|
||||
// 대시보드 추세 스냅샷 1점 — 예약(allocated) + 실측(used) 시계열.
|
||||
export interface UsageHistoryPoint {
|
||||
ts: string;
|
||||
clusters: number;
|
||||
nodes: number;
|
||||
domains: number;
|
||||
vcpu: number;
|
||||
memGb: number;
|
||||
diskGb: number;
|
||||
usedVcpu: number | null;
|
||||
usedMemGb: number | null;
|
||||
usedDiskGb: number | null;
|
||||
}
|
||||
|
||||
export interface UsageSummaryDTO {
|
||||
counts: { clusters: number; nodes: number; domains: number };
|
||||
allocated: { vcpu: number; memGb: number; diskGb: number };
|
||||
used: { vcpu: number; memGb: number; diskGb: number } | null;
|
||||
quota: QuotaDTO | null;
|
||||
clusters: UsageClusterDTO[];
|
||||
history: UsageHistoryPoint[];
|
||||
}
|
||||
|
||||
// GET /clusters/:id/usage — 노드 상태/롤 + 실측 CPU/메모리
|
||||
export interface NodeMetricDTO {
|
||||
name: string;
|
||||
roles: string[];
|
||||
ready: boolean;
|
||||
joined: boolean; // K8s 클러스터에 조인 완료 여부 (false=프로비저닝 중)
|
||||
maas_status: string | null; // MAAS 상태 (Commissioning/Deploying/Deployed…)
|
||||
phase: string; // 사용자용 단계 문구 (커미셔닝 중·OS 설치·클러스터 조인 중·Ready…)
|
||||
cpu: string | null; // "669m"
|
||||
cpu_pct: string | null; // "16"
|
||||
mem: string | null; // "3378Mi"
|
||||
mem_pct: string | null; // "42"
|
||||
disk: string | null; // "16.3Gi"
|
||||
disk_pct: string | null; // "45"
|
||||
cpu_cap?: number | null; // 총 코어 수(스펙)
|
||||
mem_cap?: number | null; // 총 메모리 GiB(스펙)
|
||||
disk_cap?: number | null; // 총 디스크 GiB(스펙)
|
||||
cpu_req?: number | null; // 예약(requests) 합 — 코어
|
||||
cpu_req_pct?: number | null; // 예약 ÷ allocatable (%)
|
||||
mem_req?: number | null; // 예약(requests) 합 — GiB
|
||||
mem_req_pct?: number | null; // 예약 ÷ allocatable (%)
|
||||
cpu_alloc?: number | null; // allocatable 코어(예약 분모)
|
||||
mem_alloc?: number | null; // allocatable GiB(예약 분모)
|
||||
pending?: boolean; // 예약 슬롯(아직 MAAS 레코드 없음) — "노드 준비중" 플레이스홀더
|
||||
}
|
||||
|
||||
export interface ClusterUsageDTO {
|
||||
available: boolean; // 컨트롤플레인 조회 성공 여부
|
||||
metricsAvailable: boolean; // kubectl top(metrics API) 사용 가능 여부
|
||||
nodes: NodeMetricDTO[];
|
||||
// 오토스케일러가 있으면 노드 수를 그것이 소유 → 수동 조정 비활성.
|
||||
autoscale: { enabled: boolean; min: number | null; max: number | null };
|
||||
provisioning: boolean; // 스케일링 진행 중(미조인 노드 존재) → 설정 변경 잠금
|
||||
// 사용량 추이 서버 롤링 버퍼(총사용%) — 새로고침 시 차트 시드용(비면 클라 누적만).
|
||||
history?: { t: number; cpu: number | null; mem: number | null; disk: number | null }[];
|
||||
}
|
||||
51
packages/ds-sdk/src/dto/cluster.ts
Normal file
51
packages/ds-sdk/src/dto/cluster.ts
Normal file
@ -0,0 +1,51 @@
|
||||
// 클러스터 관련 DTO — /api/v1 JSON 응답 형태(DateTime 은 ISO 문자열). 비밀 없음.
|
||||
// yakconsole types.ts L133-177 미러. metricsJson 은 문자열 페이로드로 유지.
|
||||
import type { ClusterStatus } from "../enums";
|
||||
|
||||
export interface ClusterDTO {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
templateCode: string;
|
||||
desiredNodes: number;
|
||||
rancherClusterId: string | null;
|
||||
status: ClusterStatus;
|
||||
statusMessage: string | null;
|
||||
defaultHostname: string;
|
||||
ingressVip: string | null;
|
||||
// 마지막 적용 오토스케일 범위(설정 다이얼로그 초기값 — metrics 실패 시 폴백)
|
||||
autoscaleMin: number | null;
|
||||
autoscaleMax: number | null;
|
||||
byAdmin: boolean; // 운영자 프로비저닝(쿼터 제외) — 목록 배지 표시용
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
// 마지막 노드 사용 스냅샷(history 포함) — 목록 행 미니 그래프용
|
||||
metricsJson: string | null;
|
||||
}
|
||||
|
||||
export interface ClusterEventDTO {
|
||||
id: string;
|
||||
clusterId: string;
|
||||
phase: string;
|
||||
message: string;
|
||||
level: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type ClusterDetailDTO = ClusterDTO & { events: ClusterEventDTO[] };
|
||||
|
||||
export interface TemplateDTO {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
vcpuPerNode: number;
|
||||
memGbPerNode: number;
|
||||
diskGbPerNode: number;
|
||||
minNodes: number;
|
||||
maxNodes: number;
|
||||
active: boolean;
|
||||
sortOrder: number;
|
||||
available: boolean;
|
||||
}
|
||||
36
packages/ds-sdk/src/dto/deployment.ts
Normal file
36
packages/ds-sdk/src/dto/deployment.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// 배포/환경변수 DTO. EnvVarDTO.value 는 secret 이면 서버가 마스킹한 값(빈 문자열)을
|
||||
// 담는 계약 — SDK 는 형태만 정의. generateManifest 는 포털 소관(여기 미포함).
|
||||
import type { DeploymentStatus } from "../enums";
|
||||
|
||||
export interface EnvVarDTO {
|
||||
key: string;
|
||||
value: string; // secret 이면 마스킹(빈 문자열)
|
||||
secret: boolean;
|
||||
}
|
||||
|
||||
export interface DeploymentDTO {
|
||||
id: string;
|
||||
clusterId: string;
|
||||
name: string;
|
||||
gitRepo: string | null; // 빌드 배포(Phase 3). image 배포 시 null
|
||||
gitBranch: string;
|
||||
gitCommit: string | null;
|
||||
image: string | null; // 컨테이너 이미지 (Phase 1 배포원)
|
||||
status: DeploymentStatus;
|
||||
statusMessage: string | null;
|
||||
// K8s 스펙
|
||||
env: EnvVarDTO[];
|
||||
port: number | null;
|
||||
cpuRequest: string;
|
||||
memRequest: string;
|
||||
healthPath: string | null;
|
||||
exposeHost: string | null;
|
||||
path: string;
|
||||
pathType: string;
|
||||
rewritePrefix: boolean;
|
||||
replicasDesired: number;
|
||||
replicasReady: number;
|
||||
lastDeployedAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
13
packages/ds-sdk/src/dto/domain.ts
Normal file
13
packages/ds-sdk/src/dto/domain.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// 도메인 DTO — fqdn·status·certStatus·검증 타임스탬프.
|
||||
import type { CertStatus, DomainStatus } from "../enums";
|
||||
|
||||
export interface DomainDTO {
|
||||
id: string;
|
||||
clusterId: string;
|
||||
fqdn: string;
|
||||
status: DomainStatus;
|
||||
certStatus: CertStatus;
|
||||
verifyExpiresAt: string;
|
||||
verifiedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
18
packages/ds-sdk/src/dto/index.ts
Normal file
18
packages/ds-sdk/src/dto/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// dto/* 배럴 재수출.
|
||||
export type { ClusterDTO, ClusterEventDTO, ClusterDetailDTO, TemplateDTO } from "./cluster";
|
||||
export type { ServiceDTO, BindingDTO } from "./service";
|
||||
export type { EnvVarDTO, DeploymentDTO } from "./deployment";
|
||||
export type {
|
||||
UserDTO,
|
||||
QuotaDTO,
|
||||
MeDTO,
|
||||
QuotaRequestDTO,
|
||||
QuotaRequestAdminDTO,
|
||||
PlanRequestAdminDTO,
|
||||
UsageClusterDTO,
|
||||
UsageHistoryPoint,
|
||||
UsageSummaryDTO,
|
||||
NodeMetricDTO,
|
||||
ClusterUsageDTO,
|
||||
} from "./account";
|
||||
export type { DomainDTO } from "./domain";
|
||||
31
packages/ds-sdk/src/dto/service.ts
Normal file
31
packages/ds-sdk/src/dto/service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 데이터 소스(외부/관리형 서비스) DTO + 바인딩 DTO.
|
||||
// connInfo 는 host/port/db/bucket 만(비밀 미포함, serialize.publicService 계약과 동형).
|
||||
// connSecretRef 등 비밀 필드는 절대 미포함.
|
||||
import type { ServiceStatusDTO, ServiceModeDTO, ServiceTypeDTO } from "../enums";
|
||||
|
||||
export interface ServiceDTO {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
clusterId: string;
|
||||
clusterName?: string; // 목록 응답에서 조인 제공
|
||||
name: string;
|
||||
type: ServiceTypeDTO;
|
||||
mode: ServiceModeDTO;
|
||||
status: ServiceStatusDTO;
|
||||
statusMessage: string | null;
|
||||
size: string; // small | medium | large
|
||||
storageGb: number; // persist=false 면 0
|
||||
persist: boolean;
|
||||
// 비밀 제외 접속 메타(serialize.publicService 계약). host/port/db/bucket 만.
|
||||
connInfo: { host?: string; port?: number; db?: number | string; bucket?: string } | null;
|
||||
bindings?: { app: string; alias: string }[]; // 단일 조회 응답에서 제공
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// GET /api/v1/deployments/:id/bindings — 앱에 연결된 서비스(배포 카드 BindPanel)
|
||||
export interface BindingDTO {
|
||||
id: string;
|
||||
alias: string;
|
||||
service: { id: string; name: string; type: ServiceTypeDTO; status: ServiceStatusDTO };
|
||||
}
|
||||
86
packages/ds-sdk/src/enums.ts
Normal file
86
packages/ds-sdk/src/enums.ts
Normal file
@ -0,0 +1,86 @@
|
||||
// 코어 상태/타입 리터럴 유니온 — DTO·스키마가 공유하는 단일 원천(SSOT).
|
||||
// yakconsole types.ts 에서 각 DTO 에 인라인돼 있던 union 을 여기로 승격.
|
||||
// zod 무의존(순수 type). 값 배열은 schemas.ts 의 z.enum 이 참조해 드리프트 방지.
|
||||
|
||||
// ── 값 배열(런타임 상수) — schemas.ts z.enum 이 재사용 ──
|
||||
export const CLUSTER_STATUS_VALUES = [
|
||||
"REQUESTED",
|
||||
"PROVISIONING",
|
||||
"BOOTSTRAPPING",
|
||||
"INSTALLING_ADDONS",
|
||||
"ACTIVE",
|
||||
"UPDATING",
|
||||
"DELETING",
|
||||
"ERROR",
|
||||
"DELETED",
|
||||
] as const;
|
||||
export type ClusterStatus = (typeof CLUSTER_STATUS_VALUES)[number];
|
||||
|
||||
export const DEPLOYMENT_STATUS_VALUES = [
|
||||
"QUEUED",
|
||||
"BUILDING",
|
||||
"DEPLOYING",
|
||||
"RUNNING",
|
||||
"FAILED",
|
||||
"STOPPED",
|
||||
] as const;
|
||||
export type DeploymentStatus = (typeof DEPLOYMENT_STATUS_VALUES)[number];
|
||||
|
||||
// 데이터 소스(관리형/외부) 서비스 9종 — SOLR·ORACLE 포함.
|
||||
export const SERVICE_TYPE_VALUES = [
|
||||
"MONGODB",
|
||||
"REDIS",
|
||||
"MINIO",
|
||||
"MYSQL",
|
||||
"POSTGRESQL",
|
||||
"MARIADB",
|
||||
"RABBITMQ",
|
||||
"SOLR",
|
||||
"ORACLE",
|
||||
] as const;
|
||||
export type ServiceTypeDTO = (typeof SERVICE_TYPE_VALUES)[number];
|
||||
|
||||
export const SERVICE_STATUS_VALUES = [
|
||||
"REQUESTED",
|
||||
"PROVISIONING",
|
||||
"READY",
|
||||
"ERROR",
|
||||
"DELETING",
|
||||
"DELETED",
|
||||
] as const;
|
||||
export type ServiceStatusDTO = (typeof SERVICE_STATUS_VALUES)[number];
|
||||
|
||||
// 데이터 소스 방식: LOCAL=클러스터 내 관리형 · REMOTE=외부 서버 연결.
|
||||
export const SERVICE_MODE_VALUES = ["LOCAL", "REMOTE"] as const;
|
||||
export type ServiceModeDTO = (typeof SERVICE_MODE_VALUES)[number];
|
||||
|
||||
export const DOMAIN_STATUS_VALUES = [
|
||||
"PENDING_DNS",
|
||||
"VERIFYING",
|
||||
"VERIFIED",
|
||||
"EDGE_SYNCING",
|
||||
"ACTIVE",
|
||||
"ERROR",
|
||||
"REMOVED",
|
||||
] as const;
|
||||
export type DomainStatus = (typeof DOMAIN_STATUS_VALUES)[number];
|
||||
|
||||
export const CERT_STATUS_VALUES = ["NONE", "ISSUING", "ISSUED", "RENEWING", "FAILED"] as const;
|
||||
export type CertStatus = (typeof CERT_STATUS_VALUES)[number];
|
||||
|
||||
export const QUOTA_REQUEST_STATUS_VALUES = ["PENDING", "APPROVED", "REJECTED"] as const;
|
||||
export type QuotaRequestStatusDTO = (typeof QUOTA_REQUEST_STATUS_VALUES)[number];
|
||||
|
||||
export const QUOTA_FIELD_VALUES = [
|
||||
"maxClusters",
|
||||
"maxNodesPerCluster",
|
||||
"maxDomains",
|
||||
"maxServices",
|
||||
] as const;
|
||||
export type QuotaField = (typeof QUOTA_FIELD_VALUES)[number];
|
||||
|
||||
export const USER_ROLE_VALUES = ["USER", "ADMIN"] as const;
|
||||
export type UserRole = (typeof USER_ROLE_VALUES)[number];
|
||||
|
||||
export const USER_STATUS_VALUES = ["ACTIVE", "SUSPENDED"] as const;
|
||||
export type UserStatus = (typeof USER_STATUS_VALUES)[number];
|
||||
@ -1,4 +1,45 @@
|
||||
// @yakcloud/ds-sdk
|
||||
// 데이터소스 계약(리프): DTO·Zod 스키마·BIND_ENV SSOT·ApiCode 리터럴. ⛔ Prisma/react/서버 금지.
|
||||
// B1 스켈레톤 — 실제 추출은 후속(scaffold-input.md 판정표 참조).
|
||||
export const __package = "@yakcloud/ds-sdk";
|
||||
// @yakcloud/ds-sdk — 데이터소스 계약(리프): DTO·Zod 스키마·BIND_ENV SSOT·ApiCode 리터럴.
|
||||
// ⛔ Prisma/react/next/서버 금지. type-only 소비자(ui/api-client)의 단일 진입점.
|
||||
// verbatimModuleSyntax: type 은 export type 으로, 값(zod 스키마·상수·헬퍼)은 export 로 분리.
|
||||
|
||||
// ── enums (값 배열 + 리터럴 유니온) ──
|
||||
export {
|
||||
CLUSTER_STATUS_VALUES,
|
||||
DEPLOYMENT_STATUS_VALUES,
|
||||
SERVICE_TYPE_VALUES,
|
||||
SERVICE_STATUS_VALUES,
|
||||
SERVICE_MODE_VALUES,
|
||||
DOMAIN_STATUS_VALUES,
|
||||
CERT_STATUS_VALUES,
|
||||
QUOTA_REQUEST_STATUS_VALUES,
|
||||
QUOTA_FIELD_VALUES,
|
||||
USER_ROLE_VALUES,
|
||||
USER_STATUS_VALUES,
|
||||
} from "./enums";
|
||||
export type {
|
||||
ClusterStatus,
|
||||
DeploymentStatus,
|
||||
ServiceTypeDTO,
|
||||
ServiceStatusDTO,
|
||||
ServiceModeDTO,
|
||||
DomainStatus,
|
||||
CertStatus,
|
||||
QuotaRequestStatusDTO,
|
||||
QuotaField,
|
||||
UserRole,
|
||||
UserStatus,
|
||||
} from "./enums";
|
||||
|
||||
// ── DTO (순수 type) ──
|
||||
export type * from "./dto/index";
|
||||
|
||||
// ── Zod 스키마 + 파생 body 타입 + 공유 상수 ──
|
||||
export * from "./schemas";
|
||||
|
||||
// ── bind-env 9종 계약 + 헬퍼 ──
|
||||
export { BIND_ENV_VARS, SERVICE_DEFAULT_PORT, bindEnvPrefix, bindEnvKeys } from "./bind-env";
|
||||
export type { BindEnvVar } from "./bind-env";
|
||||
|
||||
// ── ApiCode 계약 ──
|
||||
export { CODE_STATUS } from "./api-codes";
|
||||
export type { ApiCode } from "./api-codes";
|
||||
|
||||
246
packages/ds-sdk/src/schemas.ts
Normal file
246
packages/ds-sdk/src/schemas.ts
Normal file
@ -0,0 +1,246 @@
|
||||
// Zod 생성/검증 body 스키마 + 공유 상수/정규식. yakconsole api/schemas.ts 미러.
|
||||
// z.infer 로 body 타입 파생. 서버 respond/serialize 무의존.
|
||||
// serviceType/quotaField/status enum 은 ./enums 값 배열을 단일 원천으로 참조(드리프트 방지).
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CLUSTER_STATUS_VALUES,
|
||||
QUOTA_FIELD_VALUES,
|
||||
SERVICE_TYPE_VALUES,
|
||||
USER_STATUS_VALUES,
|
||||
} from "./enums";
|
||||
|
||||
// slug 규칙 (design §2-②): 소문자·숫자·하이픈, 3~24자.
|
||||
// 상수는 폼(new/page.tsx)이 임포트해 재사용 → 클라/서버 검증 드리프트 방지(단일 원천).
|
||||
export const CLUSTER_NAME_MIN = 3;
|
||||
export const CLUSTER_NAME_MAX = 24;
|
||||
export const CLUSTER_NAME_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
||||
export const clusterName = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(CLUSTER_NAME_MIN)
|
||||
.max(CLUSTER_NAME_MAX)
|
||||
.regex(CLUSTER_NAME_REGEX, "소문자·숫자·하이픈만, 3~24자");
|
||||
|
||||
export const fqdn = z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.max(253)
|
||||
.regex(/^(?!-)[a-z0-9-]{1,63}(\.[a-z0-9-]{1,63})+$/, "올바른 도메인 형식이 아닙니다.");
|
||||
|
||||
// 커스텀 노드 스펙 (templateCode "C"). 경계는 yakcloud-api._validated_spec 와 일치시켜 드리프트 방지.
|
||||
const nodeResources = {
|
||||
cores: z.coerce.number().int().min(1).max(32),
|
||||
memoryGb: z.coerce.number().int().min(1).max(128),
|
||||
diskGb: z.coerce.number().int().min(20).max(500),
|
||||
};
|
||||
export const customSpecBody = z
|
||||
.object({
|
||||
controlPlane: z.object({
|
||||
count: z.coerce.number().int().refine((c) => c === 1 || c === 3, "control-plane 는 1 또는 3"),
|
||||
worker: z.boolean().default(false), // CP 에 worker 역할 부여(기본 전용). C 템플릿 전용 옵션.
|
||||
...nodeResources,
|
||||
}),
|
||||
worker: z.object({
|
||||
count: z.coerce.number().int().min(0).max(10),
|
||||
...nodeResources,
|
||||
}),
|
||||
autoscale: z
|
||||
.object({
|
||||
enabled: z.boolean().default(false),
|
||||
minWorkers: z.coerce.number().int().min(0).max(20),
|
||||
maxWorkers: z.coerce.number().int().min(1).max(20),
|
||||
})
|
||||
.refine((a) => a.minWorkers <= a.maxWorkers, {
|
||||
message: "오토스케일 최소는 최대 이하여야 합니다.",
|
||||
path: ["minWorkers"],
|
||||
}),
|
||||
})
|
||||
.refine((s) => s.controlPlane.count + s.worker.count >= 1, {
|
||||
message: "노드가 1개 이상 필요합니다.",
|
||||
path: ["worker", "count"],
|
||||
})
|
||||
.refine((s) => s.controlPlane.worker || s.worker.count >= 1, {
|
||||
message: "control-plane 전용이면 worker 가 1개 이상 필요합니다.",
|
||||
path: ["worker", "count"],
|
||||
});
|
||||
export type CustomSpecBody = z.infer<typeof customSpecBody>;
|
||||
|
||||
// POST /clusters — S/M 은 명명 템플릿, C 는 custom 스펙 필수.
|
||||
export const createClusterBody = z
|
||||
.object({
|
||||
name: clusterName,
|
||||
displayName: z.string().trim().min(1).max(60).optional(),
|
||||
templateCode: z.enum(["S", "M", "C"]),
|
||||
nodeCount: z.coerce.number().int().min(1).max(30),
|
||||
custom: customSpecBody.optional(),
|
||||
goldenPath: z.boolean().optional(), // 샘플 게시판 자동 배포(골든 패스) 선택
|
||||
})
|
||||
.refine((b) => (b.templateCode === "C") === (b.custom !== undefined), {
|
||||
message: "커스텀 스펙은 templateCode 가 C 일 때만 필요합니다.",
|
||||
path: ["custom"],
|
||||
});
|
||||
export type CreateClusterBody = z.infer<typeof createClusterBody>;
|
||||
|
||||
// PATCH /clusters/:id
|
||||
export const patchClusterBody = z.object({
|
||||
displayName: z.string().trim().min(1).max(60),
|
||||
});
|
||||
// DELETE /clusters/:id — confirmName 은 cluster.name 과 일치해야 함
|
||||
export const deleteClusterBody = z.object({ confirmName: z.string() });
|
||||
// PATCH /clusters/:id/scale (P5)
|
||||
export const scaleBody = z.object({ nodeCount: z.coerce.number().int().min(1).max(9) });
|
||||
// PATCH /me
|
||||
export const patchMeBody = z
|
||||
.object({
|
||||
displayName: z.string().trim().min(1).max(60).optional(),
|
||||
locale: z.enum(["ko", "en"]).optional(),
|
||||
emailOptIn: z.boolean().optional(),
|
||||
})
|
||||
.refine((v) => Object.keys(v).length > 0, "변경할 항목이 없습니다.");
|
||||
// POST /clusters/:id/domains (P5)
|
||||
export const addDomainBody = z.object({ fqdn });
|
||||
// POST /clusters/:id/deployments (배포 탭)
|
||||
export const deployName = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3)
|
||||
.max(30)
|
||||
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "소문자·숫자·하이픈, 3~30자");
|
||||
export const envVarSchema = z.object({
|
||||
key: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "환경변수 키 형식이 아닙니다.")
|
||||
.max(100),
|
||||
value: z.string().max(8000),
|
||||
secret: z.boolean().default(false),
|
||||
});
|
||||
const gitRepoField = z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, "owner/repo 형식이어야 합니다.");
|
||||
// 컨테이너 이미지 ref (Phase 1: 사전 빌드 이미지 배포). 예: nginx:1.25, reg.example.com/team/app:sha
|
||||
const imageField = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/:@]+$/, "이미지 ref 형식이 아닙니다.");
|
||||
|
||||
// 큐레이티드 K8s 배포 스펙 (나머지 YAML 은 기본값 생성).
|
||||
// Phase 1: image 로 배포. Phase 3(gitRepo→빌드) 대비해 gitRepo 도 허용 — 둘 중 하나 필수.
|
||||
export const createDeploymentBody = z
|
||||
.object({
|
||||
name: deployName,
|
||||
image: imageField.nullable().default(null),
|
||||
gitRepo: gitRepoField.nullable().default(null),
|
||||
gitBranch: z.string().trim().min(1).max(100).default("main"),
|
||||
port: z.coerce.number().int().min(1).max(65535).nullable().default(null),
|
||||
replicasDesired: z.coerce.number().int().min(0).max(20).default(2),
|
||||
cpuRequest: z.string().trim().min(1).max(20).default("250m"),
|
||||
memRequest: z.string().trim().min(1).max(20).default("256Mi"),
|
||||
healthPath: z.string().trim().max(200).nullable().default(null),
|
||||
exposeHost: z.string().trim().max(253).nullable().default(null),
|
||||
path: z.string().trim().max(200).regex(/^\//, "경로는 / 로 시작해야 합니다.").default("/"),
|
||||
pathType: z.enum(["Prefix", "Exact", "ImplementationSpecific"]).default("Prefix"),
|
||||
rewritePrefix: z.boolean().default(false),
|
||||
env: z.array(envVarSchema).max(100).default([]),
|
||||
})
|
||||
.refine((b) => Boolean(b.image || b.gitRepo), {
|
||||
message: "image 또는 gitRepo 중 하나는 필요합니다.",
|
||||
path: ["image"],
|
||||
});
|
||||
|
||||
// 배포 설정/환경변수 수정 (부분)
|
||||
export const patchDeploymentBody = z.object({
|
||||
port: z.coerce.number().int().min(1).max(65535).nullable().optional(),
|
||||
replicasDesired: z.coerce.number().int().min(0).max(20).optional(),
|
||||
cpuRequest: z.string().trim().min(1).max(20).optional(),
|
||||
memRequest: z.string().trim().min(1).max(20).optional(),
|
||||
healthPath: z.string().trim().max(200).nullable().optional(),
|
||||
exposeHost: z.string().trim().max(253).nullable().optional(),
|
||||
path: z.string().trim().max(200).regex(/^\//, "경로는 / 로 시작해야 합니다.").optional(),
|
||||
pathType: z.enum(["Prefix", "Exact", "ImplementationSpecific"]).optional(),
|
||||
rewritePrefix: z.boolean().optional(),
|
||||
env: z.array(envVarSchema).max(100).optional(),
|
||||
});
|
||||
|
||||
// ── 외부 서비스 (인클러스터 관리형 Mongo/Redis/MinIO) ──
|
||||
export const serviceName = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3)
|
||||
.max(30)
|
||||
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "소문자·숫자·하이픈, 3~30자"); // yakcloud NAME_RE 호환
|
||||
// ./enums SERVICE_TYPE_VALUES 를 단일 원천으로 참조(9종·ServiceTypeDTO 와 동일성 보장).
|
||||
export const serviceType = z.enum(SERVICE_TYPE_VALUES);
|
||||
export const serviceSize = z.enum(["small", "medium", "large"]);
|
||||
// POST /api/v1/services
|
||||
export const createServiceBody = z
|
||||
.object({
|
||||
clusterId: z.string().min(1),
|
||||
type: serviceType,
|
||||
name: serviceName,
|
||||
mode: z.enum(["local", "remote"]).default("local"), // local=관리형 · remote=외부 서버 연결
|
||||
size: serviceSize.default("small"),
|
||||
storageGb: z.coerce.number().int().min(1).max(500).nullable().optional(),
|
||||
persist: z.boolean().optional(), // 미지정 시 redis=false, 그 외 true
|
||||
// remote(외부) 접속정보 — mode=remote 일 때 host·port 필수.
|
||||
host: z.string().trim().max(253).optional(),
|
||||
port: z.coerce.number().int().min(1).max(65535).optional(),
|
||||
username: z.string().trim().max(200).optional(),
|
||||
password: z.string().max(500).optional(),
|
||||
database: z.string().trim().max(128).optional(),
|
||||
vhost: z.string().trim().max(128).optional(),
|
||||
core: z.string().trim().max(128).optional(),
|
||||
dbNum: z.coerce.number().int().min(0).max(15).optional(),
|
||||
})
|
||||
.refine((b) => b.mode !== "remote" || (!!b.host && !!b.port), {
|
||||
message: "외부 연결에는 host 와 port 가 필요합니다.",
|
||||
path: ["host"],
|
||||
});
|
||||
// POST /api/v1/services/:id/bindings — alias 기본값 = 서비스명(서버)
|
||||
export const bindServiceBody = z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
alias: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[a-z][a-z0-9-]*$/, "소문자로 시작, 소문자·숫자·하이픈")
|
||||
.max(30)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// admin
|
||||
export const patchQuotaBody = z.object({
|
||||
maxClusters: z.coerce.number().int().min(0).max(50).optional(),
|
||||
maxNodesPerCluster: z.coerce.number().int().min(1).max(20).optional(),
|
||||
maxDomains: z.coerce.number().int().min(0).max(100).optional(),
|
||||
maxServices: z.coerce.number().int().min(0).max(50).optional(),
|
||||
});
|
||||
// 쿼터 증설 요청/승인 (거버넌스: 사용자 요청 → 관리자 승인 시에만 반영)
|
||||
// ./enums QUOTA_FIELD_VALUES 를 단일 원천으로 참조(QuotaField 와 동일성 보장).
|
||||
export const quotaField = z.enum(QUOTA_FIELD_VALUES);
|
||||
export const createQuotaRequestBody = z.object({
|
||||
field: quotaField,
|
||||
requestedValue: z.coerce.number().int().min(1).max(100),
|
||||
reason: z.string().trim().max(500).optional(),
|
||||
});
|
||||
export const decideQuotaRequestBody = z.object({ approve: z.boolean() });
|
||||
// ./enums USER_STATUS_VALUES 를 단일 원천으로 참조(UserStatus 와 동일성 보장).
|
||||
export const patchUserStatusBody = z.object({ status: z.enum(USER_STATUS_VALUES) });
|
||||
// POST /internal/clusters/:id/status
|
||||
// ./enums CLUSTER_STATUS_VALUES 를 단일 원천으로 참조(ClusterStatus 와 동일성 보장).
|
||||
export const internalStatusBody = z.object({
|
||||
status: z.enum(CLUSTER_STATUS_VALUES),
|
||||
statusMessage: z.string().max(500).optional(),
|
||||
rancherClusterId: z.string().optional(),
|
||||
ingressVip: z.string().optional(),
|
||||
event: z
|
||||
.object({
|
||||
phase: z.string(),
|
||||
message: z.string(),
|
||||
level: z.enum(["info", "warn", "error"]).default("info"),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
Reference in New Issue
Block a user