From 7eb195c5d70468b6cfd242275857a094de55ffc2 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 1 Sep 2026 11:30:41 +0700 Subject: [PATCH] =?UTF-8?q?feat(sync):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D1=82=D1=8C=20lbMode=20=D0=B2=20payload=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20VPS=20Tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тип HA уходит в bindings, чтобы схема могла показать резервирование. Co-authored-by: Cursor --- apps/api/src/services/vps-tracker-sync.ts | 42 +++++++++++++++- apps/api/test/vps-tracker-sync.test.ts | 49 ++++++++++++++++++- packages/shared/dist/index.d.ts | 10 ++++ packages/shared/dist/index.js | 2 + .../shared/src/integration-vps-tracker.ts | 2 + 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/apps/api/src/services/vps-tracker-sync.ts b/apps/api/src/services/vps-tracker-sync.ts index b4c176e..2440bfa 100644 --- a/apps/api/src/services/vps-tracker-sync.ts +++ b/apps/api/src/services/vps-tracker-sync.ts @@ -1,9 +1,43 @@ import { resolve4 } from "node:dns/promises"; -import type { CfdmBindingSyncItem, ServiceBindingView } from "@cfdm/shared"; +import type { CfdmBindingSyncItem, LbMode, ServiceBindingView } from "@cfdm/shared"; import { isIpLiteral } from "@cfdm/shared"; import type { Db } from "@cfdm/db"; import { repos, getAppSettingsSecrets, touchVpsTrackerSync } from "@cfdm/db"; +export function isLbMode(value: unknown): value is LbMode { + return value === "round_robin" || value === "failover" || value === "weighted"; +} + +/** lb_mode binding, иначе service group. */ +export function resolveLbModeForSync( + bindingLbMode: string | undefined | null, + groupLbMode?: string | null, +): LbMode | undefined { + if (isLbMode(bindingLbMode)) return bindingLbMode; + if (isLbMode(groupLbMode)) return groupLbMode; + return undefined; +} + +function groupLbModeForService( + db: Db, + serviceId: number, + cache: Map, +): LbMode | undefined { + if (cache.has(serviceId)) return cache.get(serviceId); + let mode: LbMode | undefined; + try { + const service = repos.getService(db, serviceId); + if (service.service_group_id != null) { + const group = repos.getServiceGroup(db, service.service_group_id); + mode = resolveLbModeForSync(undefined, group.lb_mode); + } + } catch { + mode = undefined; + } + cache.set(serviceId, mode); + return mode; +} + function fqdnToDisplay(hostname: string, zoneName: string): string { if (hostname === "@" || !hostname.trim()) return zoneName; return `${hostname}.${zoneName}`; @@ -126,6 +160,8 @@ export async function buildServiceSyncBindingsAsync( const allBindings = repos.listAllBindings(db); const index = buildBindingIndex(allBindings); const bindings = allBindings.filter((row) => row.service_id === serviceId); + const groupLbCache = new Map(); + const groupLb = groupLbModeForService(db, serviceId, groupLbCache); const items: CfdmBindingSyncItem[] = []; for (const binding of bindings) { @@ -140,6 +176,7 @@ export async function buildServiceSyncBindingsAsync( hostname: binding.hostname, ips, cnameTarget: cnameTargetForSync(binding), + lbMode: resolveLbModeForSync(binding.lb_mode, groupLb), }); } @@ -166,6 +203,7 @@ export async function buildAllSyncBindings( const bindings = repos.listAllBindings(db); const index = buildBindingIndex(bindings); const serviceIpCache = new Map(); + const groupLbCache = new Map(); const items: CfdmBindingSyncItem[] = []; for (const binding of bindings) { @@ -175,6 +213,7 @@ export async function buildAllSyncBindings( serviceIpCache.set(binding.service_id, serviceIps); } const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db); + const groupLb = groupLbModeForService(db, binding.service_id, groupLbCache); items.push({ bindingId: binding.id, serviceId: binding.service_id, @@ -185,6 +224,7 @@ export async function buildAllSyncBindings( hostname: binding.hostname, ips, cnameTarget: cnameTargetForSync(binding), + lbMode: resolveLbModeForSync(binding.lb_mode, groupLb), }); } return items; diff --git a/apps/api/test/vps-tracker-sync.test.ts b/apps/api/test/vps-tracker-sync.test.ts index 0eb7e30..5509784 100644 --- a/apps/api/test/vps-tracker-sync.test.ts +++ b/apps/api/test/vps-tracker-sync.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from "vitest"; -import type { ServiceBindingView } from "@cfdm/shared"; -import { resolveBindingIpsForSync } from "../src/services/vps-tracker-sync.js"; +import { + cfdmBindingSyncItemSchema, + type ServiceBindingView, +} from "@cfdm/shared"; +import { + resolveBindingIpsForSync, + resolveLbModeForSync, +} from "../src/services/vps-tracker-sync.js"; function binding( partial: Partial & @@ -160,3 +166,42 @@ describe("resolveBindingIpsForSync", () => { expect(ips).toEqual(["203.0.113.55"]); }); }); + +describe("resolveLbModeForSync", () => { + it("prefers binding lb_mode", () => { + expect(resolveLbModeForSync("failover", "round_robin")).toBe("failover"); + }); + + it("falls back to service group lb_mode", () => { + expect(resolveLbModeForSync("off", "weighted")).toBe("weighted"); + expect(resolveLbModeForSync(undefined, "round_robin")).toBe("round_robin"); + }); + + it("returns undefined when neither is a known mode", () => { + expect(resolveLbModeForSync("off", "off")).toBeUndefined(); + expect(resolveLbModeForSync(null, undefined)).toBeUndefined(); + }); +}); + +describe("cfdmBindingSyncItemSchema lbMode", () => { + const base = { + bindingId: 1, + serviceId: 10, + serviceName: "VPN", + serviceSlug: "vpn", + fqdn: "vpn.example.com", + zoneName: "example.com", + hostname: "vpn", + ips: ["203.0.113.10"], + }; + + it("accepts optional lbMode on sync payload", () => { + const parsed = cfdmBindingSyncItemSchema.parse({ ...base, lbMode: "failover" }); + expect(parsed.lbMode).toBe("failover"); + }); + + it("accepts payload without lbMode (legacy)", () => { + const parsed = cfdmBindingSyncItemSchema.parse(base); + expect(parsed.lbMode).toBeUndefined(); + }); +}); diff --git a/packages/shared/dist/index.d.ts b/packages/shared/dist/index.d.ts index 1430784..386b561 100644 --- a/packages/shared/dist/index.d.ts +++ b/packages/shared/dist/index.d.ts @@ -2247,6 +2247,11 @@ declare const cfdmBindingSyncItemSchema: z.ZodObject<{ hostname: z.ZodString; ips: z.ZodArray; cnameTarget: z.ZodOptional; + lbMode: z.ZodOptional>; deleted: z.ZodOptional; }, z.core.$strip>; declare const cfdmSyncBindingsBodySchema: z.ZodObject<{ @@ -2260,6 +2265,11 @@ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{ hostname: z.ZodString; ips: z.ZodArray; cnameTarget: z.ZodOptional; + lbMode: z.ZodOptional>; deleted: z.ZodOptional; }, z.core.$strip>>; fullSync: z.ZodOptional; diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js index af3154e..44cd92c 100644 --- a/packages/shared/dist/index.js +++ b/packages/shared/dist/index.js @@ -850,6 +850,8 @@ var cfdmBindingSyncItemSchema = z3.object({ ips: z3.array(z3.string()), /** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */ cnameTarget: z3.string().optional(), + /** HA-режим binding (fallback — service group). Optional для старых payload. */ + lbMode: z3.enum(["round_robin", "failover", "weighted"]).optional(), deleted: z3.boolean().optional() }); var cfdmSyncBindingsBodySchema = z3.object({ diff --git a/packages/shared/src/integration-vps-tracker.ts b/packages/shared/src/integration-vps-tracker.ts index a219f57..2edd2e0 100644 --- a/packages/shared/src/integration-vps-tracker.ts +++ b/packages/shared/src/integration-vps-tracker.ts @@ -11,6 +11,8 @@ export const cfdmBindingSyncItemSchema = z.object({ ips: z.array(z.string()), /** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */ cnameTarget: z.string().optional(), + /** HA-режим binding (fallback — service group). Optional для старых payload. */ + lbMode: z.enum(["round_robin", "failover", "weighted"]).optional(), deleted: z.boolean().optional(), });