feat(sync): передавать lbMode в payload для VPS Tracker
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 4s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 53s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m54s
CD / publish (push) Successful in 1m29s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 4s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 53s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m54s
CD / publish (push) Successful in 1m29s
Тип HA уходит в bindings, чтобы схема могла показать резервирование. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,43 @@
|
|||||||
import { resolve4 } from "node:dns/promises";
|
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 { isIpLiteral } from "@cfdm/shared";
|
||||||
import type { Db } from "@cfdm/db";
|
import type { Db } from "@cfdm/db";
|
||||||
import { repos, getAppSettingsSecrets, touchVpsTrackerSync } 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<number, LbMode | undefined>,
|
||||||
|
): 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 {
|
function fqdnToDisplay(hostname: string, zoneName: string): string {
|
||||||
if (hostname === "@" || !hostname.trim()) return zoneName;
|
if (hostname === "@" || !hostname.trim()) return zoneName;
|
||||||
return `${hostname}.${zoneName}`;
|
return `${hostname}.${zoneName}`;
|
||||||
@@ -126,6 +160,8 @@ export async function buildServiceSyncBindingsAsync(
|
|||||||
const allBindings = repos.listAllBindings(db);
|
const allBindings = repos.listAllBindings(db);
|
||||||
const index = buildBindingIndex(allBindings);
|
const index = buildBindingIndex(allBindings);
|
||||||
const bindings = allBindings.filter((row) => row.service_id === serviceId);
|
const bindings = allBindings.filter((row) => row.service_id === serviceId);
|
||||||
|
const groupLbCache = new Map<number, LbMode | undefined>();
|
||||||
|
const groupLb = groupLbModeForService(db, serviceId, groupLbCache);
|
||||||
|
|
||||||
const items: CfdmBindingSyncItem[] = [];
|
const items: CfdmBindingSyncItem[] = [];
|
||||||
for (const binding of bindings) {
|
for (const binding of bindings) {
|
||||||
@@ -140,6 +176,7 @@ export async function buildServiceSyncBindingsAsync(
|
|||||||
hostname: binding.hostname,
|
hostname: binding.hostname,
|
||||||
ips,
|
ips,
|
||||||
cnameTarget: cnameTargetForSync(binding),
|
cnameTarget: cnameTargetForSync(binding),
|
||||||
|
lbMode: resolveLbModeForSync(binding.lb_mode, groupLb),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +203,7 @@ export async function buildAllSyncBindings(
|
|||||||
const bindings = repos.listAllBindings(db);
|
const bindings = repos.listAllBindings(db);
|
||||||
const index = buildBindingIndex(bindings);
|
const index = buildBindingIndex(bindings);
|
||||||
const serviceIpCache = new Map<number, string[]>();
|
const serviceIpCache = new Map<number, string[]>();
|
||||||
|
const groupLbCache = new Map<number, LbMode | undefined>();
|
||||||
|
|
||||||
const items: CfdmBindingSyncItem[] = [];
|
const items: CfdmBindingSyncItem[] = [];
|
||||||
for (const binding of bindings) {
|
for (const binding of bindings) {
|
||||||
@@ -175,6 +213,7 @@ export async function buildAllSyncBindings(
|
|||||||
serviceIpCache.set(binding.service_id, serviceIps);
|
serviceIpCache.set(binding.service_id, serviceIps);
|
||||||
}
|
}
|
||||||
const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
|
const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
|
||||||
|
const groupLb = groupLbModeForService(db, binding.service_id, groupLbCache);
|
||||||
items.push({
|
items.push({
|
||||||
bindingId: binding.id,
|
bindingId: binding.id,
|
||||||
serviceId: binding.service_id,
|
serviceId: binding.service_id,
|
||||||
@@ -185,6 +224,7 @@ export async function buildAllSyncBindings(
|
|||||||
hostname: binding.hostname,
|
hostname: binding.hostname,
|
||||||
ips,
|
ips,
|
||||||
cnameTarget: cnameTargetForSync(binding),
|
cnameTarget: cnameTargetForSync(binding),
|
||||||
|
lbMode: resolveLbModeForSync(binding.lb_mode, groupLb),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ServiceBindingView } from "@cfdm/shared";
|
import {
|
||||||
import { resolveBindingIpsForSync } from "../src/services/vps-tracker-sync.js";
|
cfdmBindingSyncItemSchema,
|
||||||
|
type ServiceBindingView,
|
||||||
|
} from "@cfdm/shared";
|
||||||
|
import {
|
||||||
|
resolveBindingIpsForSync,
|
||||||
|
resolveLbModeForSync,
|
||||||
|
} from "../src/services/vps-tracker-sync.js";
|
||||||
|
|
||||||
function binding(
|
function binding(
|
||||||
partial: Partial<ServiceBindingView> &
|
partial: Partial<ServiceBindingView> &
|
||||||
@@ -160,3 +166,42 @@ describe("resolveBindingIpsForSync", () => {
|
|||||||
expect(ips).toEqual(["203.0.113.55"]);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Vendored
+10
@@ -2247,6 +2247,11 @@ declare const cfdmBindingSyncItemSchema: z.ZodObject<{
|
|||||||
hostname: z.ZodString;
|
hostname: z.ZodString;
|
||||||
ips: z.ZodArray<z.ZodString>;
|
ips: z.ZodArray<z.ZodString>;
|
||||||
cnameTarget: z.ZodOptional<z.ZodString>;
|
cnameTarget: z.ZodOptional<z.ZodString>;
|
||||||
|
lbMode: z.ZodOptional<z.ZodEnum<{
|
||||||
|
round_robin: "round_robin";
|
||||||
|
failover: "failover";
|
||||||
|
weighted: "weighted";
|
||||||
|
}>>;
|
||||||
deleted: z.ZodOptional<z.ZodBoolean>;
|
deleted: z.ZodOptional<z.ZodBoolean>;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
declare const cfdmSyncBindingsBodySchema: z.ZodObject<{
|
declare const cfdmSyncBindingsBodySchema: z.ZodObject<{
|
||||||
@@ -2260,6 +2265,11 @@ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{
|
|||||||
hostname: z.ZodString;
|
hostname: z.ZodString;
|
||||||
ips: z.ZodArray<z.ZodString>;
|
ips: z.ZodArray<z.ZodString>;
|
||||||
cnameTarget: z.ZodOptional<z.ZodString>;
|
cnameTarget: z.ZodOptional<z.ZodString>;
|
||||||
|
lbMode: z.ZodOptional<z.ZodEnum<{
|
||||||
|
round_robin: "round_robin";
|
||||||
|
failover: "failover";
|
||||||
|
weighted: "weighted";
|
||||||
|
}>>;
|
||||||
deleted: z.ZodOptional<z.ZodBoolean>;
|
deleted: z.ZodOptional<z.ZodBoolean>;
|
||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
fullSync: z.ZodOptional<z.ZodBoolean>;
|
fullSync: z.ZodOptional<z.ZodBoolean>;
|
||||||
|
|||||||
Vendored
+2
@@ -850,6 +850,8 @@ var cfdmBindingSyncItemSchema = z3.object({
|
|||||||
ips: z3.array(z3.string()),
|
ips: z3.array(z3.string()),
|
||||||
/** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */
|
/** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */
|
||||||
cnameTarget: z3.string().optional(),
|
cnameTarget: z3.string().optional(),
|
||||||
|
/** HA-режим binding (fallback — service group). Optional для старых payload. */
|
||||||
|
lbMode: z3.enum(["round_robin", "failover", "weighted"]).optional(),
|
||||||
deleted: z3.boolean().optional()
|
deleted: z3.boolean().optional()
|
||||||
});
|
});
|
||||||
var cfdmSyncBindingsBodySchema = z3.object({
|
var cfdmSyncBindingsBodySchema = z3.object({
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export const cfdmBindingSyncItemSchema = z.object({
|
|||||||
ips: z.array(z.string()),
|
ips: z.array(z.string()),
|
||||||
/** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */
|
/** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */
|
||||||
cnameTarget: z.string().optional(),
|
cnameTarget: z.string().optional(),
|
||||||
|
/** HA-режим binding (fallback — service group). Optional для старых payload. */
|
||||||
|
lbMode: z.enum(["round_robin", "failover", "weighted"]).optional(),
|
||||||
deleted: z.boolean().optional(),
|
deleted: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user