fix(integrations): разворачивать CNAME до IP при sync в VPS Tracker
CNAME-bindings отдавали пустой ips[]; теперь IP берутся из локальной цепочки bindings, DNS resolve4 или IP сервиса — чтобы vps-tracker мог матчить домен к VPS. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -61,7 +61,7 @@ export async function integrationsVpsTrackerRoutes(app: FastifyInstance) {
|
|||||||
return reply.status(401).send({ ok: false, error: "Unauthorized" });
|
return reply.status(401).send({ ok: false, error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindings = buildAllSyncBindings(app.db);
|
const bindings = await buildAllSyncBindings(app.db);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
count: bindings.length,
|
count: bindings.length,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CfdmBindingSyncItem } from "@cfdm/shared";
|
import { resolve4 } from "node:dns/promises";
|
||||||
|
import type { CfdmBindingSyncItem, ServiceBindingView } 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";
|
||||||
|
|
||||||
@@ -7,23 +8,107 @@ function fqdnToDisplay(hostname: string, zoneName: string): string {
|
|||||||
return `${hostname}.${zoneName}`;
|
return `${hostname}.${zoneName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildServiceSyncBindings(
|
function normalizeCnameHost(target: string, zoneName: string): string {
|
||||||
|
const trimmed = target.trim().toLowerCase().replace(/\.$/, "");
|
||||||
|
if (!trimmed) return "";
|
||||||
|
if (trimmed.includes(".")) return trimmed;
|
||||||
|
return `${trimmed}.${zoneName.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BindingIpIndex = {
|
||||||
|
byFqdn: Map<string, ServiceBindingView>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildBindingIndex(bindings: ServiceBindingView[]): BindingIpIndex {
|
||||||
|
const byFqdn = new Map<string, ServiceBindingView>();
|
||||||
|
for (const b of bindings) {
|
||||||
|
const fqdn = fqdnToDisplay(b.hostname, b.zone_name).toLowerCase();
|
||||||
|
byFqdn.set(fqdn, b);
|
||||||
|
}
|
||||||
|
return { byFqdn };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Локальная цепочка CNAME → A (по bindings в CFDM), без внешнего DNS. */
|
||||||
|
function resolveIpsLocally(
|
||||||
|
index: BindingIpIndex,
|
||||||
|
startFqdn: string,
|
||||||
|
depth = 0,
|
||||||
|
seen = new Set<string>(),
|
||||||
|
): string[] {
|
||||||
|
const key = startFqdn.toLowerCase().replace(/\.$/, "");
|
||||||
|
if (!key || depth > 8 || seen.has(key)) return [];
|
||||||
|
seen.add(key);
|
||||||
|
|
||||||
|
const binding = index.byFqdn.get(key);
|
||||||
|
if (!binding) return [];
|
||||||
|
|
||||||
|
if (binding.target_ips.length > 0) {
|
||||||
|
return [...binding.target_ips];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cname = binding.cname_target?.trim();
|
||||||
|
if (!cname) return [];
|
||||||
|
|
||||||
|
const next = normalizeCnameHost(cname, binding.zone_name);
|
||||||
|
return resolveIpsLocally(index, next, depth + 1, seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveIpsViaDns(hostname: string): Promise<string[]> {
|
||||||
|
const host = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||||
|
if (!host) return [];
|
||||||
|
try {
|
||||||
|
// resolve4 следует по CNAME до A-записей
|
||||||
|
return await resolve4(host);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP для матчинга в VPS Tracker:
|
||||||
|
* 1) A-записи binding
|
||||||
|
* 2) разворот локальной CNAME-цепочки по другим bindings
|
||||||
|
* 3) публичный DNS (resolve4)
|
||||||
|
* 4) IP сервиса
|
||||||
|
*/
|
||||||
|
export async function resolveBindingIpsForSync(
|
||||||
|
binding: ServiceBindingView,
|
||||||
|
serviceIps: string[],
|
||||||
|
index: BindingIpIndex,
|
||||||
|
): Promise<string[]> {
|
||||||
|
if (binding.target_ips.length > 0) {
|
||||||
|
return [...binding.target_ips];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cname = binding.cname_target?.trim();
|
||||||
|
if (cname) {
|
||||||
|
const targetFqdn = normalizeCnameHost(cname, binding.zone_name);
|
||||||
|
const local = resolveIpsLocally(index, targetFqdn);
|
||||||
|
if (local.length > 0) return local;
|
||||||
|
|
||||||
|
const viaDns = await resolveIpsViaDns(targetFqdn);
|
||||||
|
if (viaDns.length > 0) return viaDns;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceIps.length > 0) return [...serviceIps];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildServiceSyncBindingsAsync(
|
||||||
db: Db,
|
db: Db,
|
||||||
serviceId: number,
|
serviceId: number,
|
||||||
deletedBindingIds: number[] = [],
|
deletedBindingIds: number[] = [],
|
||||||
): CfdmBindingSyncItem[] {
|
): Promise<CfdmBindingSyncItem[]> {
|
||||||
const service = repos.getService(db, serviceId);
|
const service = repos.getService(db, serviceId);
|
||||||
const serviceIps = repos.listServiceIps(db, serviceId);
|
const serviceIps = repos.listServiceIps(db, serviceId);
|
||||||
|
const allBindings = repos.listAllBindings(db);
|
||||||
|
const index = buildBindingIndex(allBindings);
|
||||||
const bindings = repos.listBindingsByService(db, serviceId);
|
const bindings = repos.listBindingsByService(db, serviceId);
|
||||||
const items: CfdmBindingSyncItem[] = bindings.map((binding) => {
|
|
||||||
const targetIps = repos.listBindingIps(db, binding.id);
|
const items: CfdmBindingSyncItem[] = [];
|
||||||
const ips =
|
for (const binding of bindings) {
|
||||||
targetIps.length > 0
|
const ips = await resolveBindingIpsForSync(binding, serviceIps, index);
|
||||||
? targetIps
|
items.push({
|
||||||
: serviceIps.length > 0
|
|
||||||
? serviceIps
|
|
||||||
: [];
|
|
||||||
return {
|
|
||||||
bindingId: binding.id,
|
bindingId: binding.id,
|
||||||
serviceId: service.id,
|
serviceId: service.id,
|
||||||
serviceName: service.name,
|
serviceName: service.name,
|
||||||
@@ -32,8 +117,8 @@ export function buildServiceSyncBindings(
|
|||||||
zoneName: binding.zone_name,
|
zoneName: binding.zone_name,
|
||||||
hostname: binding.hostname,
|
hostname: binding.hostname,
|
||||||
ips,
|
ips,
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const bindingId of deletedBindingIds) {
|
for (const bindingId of deletedBindingIds) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -52,17 +137,22 @@ export function buildServiceSyncBindings(
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAllSyncBindings(db: Db): CfdmBindingSyncItem[] {
|
export async function buildAllSyncBindings(
|
||||||
|
db: Db,
|
||||||
|
): Promise<CfdmBindingSyncItem[]> {
|
||||||
const bindings = repos.listAllBindings(db);
|
const bindings = repos.listAllBindings(db);
|
||||||
return bindings.map((binding) => {
|
const index = buildBindingIndex(bindings);
|
||||||
const serviceIps = repos.listServiceIps(db, binding.service_id);
|
const serviceIpCache = new Map<number, string[]>();
|
||||||
const ips =
|
|
||||||
binding.target_ips.length > 0
|
const items: CfdmBindingSyncItem[] = [];
|
||||||
? binding.target_ips
|
for (const binding of bindings) {
|
||||||
: serviceIps.length > 0
|
let serviceIps = serviceIpCache.get(binding.service_id);
|
||||||
? serviceIps
|
if (!serviceIps) {
|
||||||
: [];
|
serviceIps = repos.listServiceIps(db, binding.service_id);
|
||||||
return {
|
serviceIpCache.set(binding.service_id, serviceIps);
|
||||||
|
}
|
||||||
|
const ips = await resolveBindingIpsForSync(binding, serviceIps, index);
|
||||||
|
items.push({
|
||||||
bindingId: binding.id,
|
bindingId: binding.id,
|
||||||
serviceId: binding.service_id,
|
serviceId: binding.service_id,
|
||||||
serviceName: binding.service_name,
|
serviceName: binding.service_name,
|
||||||
@@ -71,8 +161,9 @@ export function buildAllSyncBindings(db: Db): CfdmBindingSyncItem[] {
|
|||||||
zoneName: binding.zone_name,
|
zoneName: binding.zone_name,
|
||||||
hostname: binding.hostname,
|
hostname: binding.hostname,
|
||||||
ips,
|
ips,
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncServiceToVpsTracker(
|
export async function syncServiceToVpsTracker(
|
||||||
@@ -87,7 +178,11 @@ export async function syncServiceToVpsTracker(
|
|||||||
const token = config.vpsTrackerIntegrationToken;
|
const token = config.vpsTrackerIntegrationToken;
|
||||||
if (!baseUrl || !token) return;
|
if (!baseUrl || !token) return;
|
||||||
|
|
||||||
const bindings = buildServiceSyncBindings(db, serviceId, deletedBindingIds);
|
const bindings = await buildServiceSyncBindingsAsync(
|
||||||
|
db,
|
||||||
|
serviceId,
|
||||||
|
deletedBindingIds,
|
||||||
|
);
|
||||||
if (bindings.length === 0) return;
|
if (bindings.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -133,7 +228,7 @@ export async function syncAllToVpsTracker(db: Db): Promise<{
|
|||||||
return { ok: false, count: 0, error: "Укажите integration token" };
|
return { ok: false, count: 0, error: "Укажите integration token" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindings = buildAllSyncBindings(db);
|
const bindings = await buildAllSyncBindings(db);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${baseUrl}/api/integrations/cfdm/sync-bindings`, {
|
const res = await fetch(`${baseUrl}/api/integrations/cfdm/sync-bindings`, {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ServiceBindingView } from "@cfdm/shared";
|
||||||
|
import { resolveBindingIpsForSync } from "../src/services/vps-tracker-sync.js";
|
||||||
|
|
||||||
|
function binding(
|
||||||
|
partial: Partial<ServiceBindingView> &
|
||||||
|
Pick<ServiceBindingView, "id" | "hostname" | "zone_name">,
|
||||||
|
): ServiceBindingView {
|
||||||
|
return {
|
||||||
|
domain_id: 1,
|
||||||
|
service_id: 1,
|
||||||
|
dns_record_id: null,
|
||||||
|
group_id: null,
|
||||||
|
group_name: null,
|
||||||
|
service_name: "svc",
|
||||||
|
service_slug: "svc",
|
||||||
|
target_ip: null,
|
||||||
|
target_ips: [],
|
||||||
|
target_ip_weights: {},
|
||||||
|
target_ip_priorities: {},
|
||||||
|
cname_target: null,
|
||||||
|
lb_mode: "off",
|
||||||
|
health_check_enabled: false,
|
||||||
|
health_check_type: "tcp",
|
||||||
|
health_check_port: null,
|
||||||
|
health_check_path: null,
|
||||||
|
health_check_expected_status: null,
|
||||||
|
health_check_interval_sec: 60,
|
||||||
|
health_check_timeout_ms: 3000,
|
||||||
|
health_check_verify_tls: true,
|
||||||
|
sync_status: null,
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
...partial,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveBindingIpsForSync", () => {
|
||||||
|
it("uses A-record IPs when present", async () => {
|
||||||
|
const a = binding({
|
||||||
|
id: 1,
|
||||||
|
hostname: "ihome",
|
||||||
|
zone_name: "rkns.top",
|
||||||
|
target_ips: ["10.0.0.5"],
|
||||||
|
});
|
||||||
|
const index = { byFqdn: new Map([["ihome.rkns.top", a]]) };
|
||||||
|
const ips = await resolveBindingIpsForSync(a, ["9.9.9.9"], index);
|
||||||
|
expect(ips).toEqual(["10.0.0.5"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves CNAME via local binding chain to VPS IP", async () => {
|
||||||
|
const target = binding({
|
||||||
|
id: 1,
|
||||||
|
hostname: "ihome",
|
||||||
|
zone_name: "rkns.top",
|
||||||
|
target_ips: ["203.0.113.10"],
|
||||||
|
});
|
||||||
|
const cname = binding({
|
||||||
|
id: 2,
|
||||||
|
hostname: "imsk",
|
||||||
|
zone_name: "rkns.top",
|
||||||
|
cname_target: "ihome.rkns.top",
|
||||||
|
target_ips: [],
|
||||||
|
});
|
||||||
|
const index = {
|
||||||
|
byFqdn: new Map([
|
||||||
|
["ihome.rkns.top", target],
|
||||||
|
["imsk.rkns.top", cname],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const ips = await resolveBindingIpsForSync(cname, [], index);
|
||||||
|
expect(ips).toEqual(["203.0.113.10"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves short CNAME target relative to zone", async () => {
|
||||||
|
const target = binding({
|
||||||
|
id: 1,
|
||||||
|
hostname: "ihome",
|
||||||
|
zone_name: "rkns.top",
|
||||||
|
target_ips: ["203.0.113.11"],
|
||||||
|
});
|
||||||
|
const cname = binding({
|
||||||
|
id: 2,
|
||||||
|
hostname: "imsk",
|
||||||
|
zone_name: "rkns.top",
|
||||||
|
cname_target: "ihome",
|
||||||
|
target_ips: [],
|
||||||
|
});
|
||||||
|
const index = {
|
||||||
|
byFqdn: new Map([
|
||||||
|
["ihome.rkns.top", target],
|
||||||
|
["imsk.rkns.top", cname],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const ips = await resolveBindingIpsForSync(cname, [], index);
|
||||||
|
expect(ips).toEqual(["203.0.113.11"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to service IPs when CNAME target unknown locally and DNS fails", async () => {
|
||||||
|
const cname = binding({
|
||||||
|
id: 2,
|
||||||
|
hostname: "imsk",
|
||||||
|
zone_name: "rkns.top",
|
||||||
|
cname_target: "definitely-not-resolvable-xyz.invalid",
|
||||||
|
target_ips: [],
|
||||||
|
});
|
||||||
|
const index = { byFqdn: new Map([["imsk.rkns.top", cname]]) };
|
||||||
|
const ips = await resolveBindingIpsForSync(cname, ["198.51.100.7"], index);
|
||||||
|
expect(ips).toEqual(["198.51.100.7"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+22
-8
@@ -10,7 +10,8 @@ import {
|
|||||||
integer,
|
integer,
|
||||||
primaryKey,
|
primaryKey,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text
|
text,
|
||||||
|
unique
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
var groups = sqliteTable("groups", {
|
var groups = sqliteTable("groups", {
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
@@ -94,7 +95,9 @@ var dnsRecords = sqliteTable("dns_records", {
|
|||||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||||
});
|
});
|
||||||
var serviceBindings = sqliteTable("service_bindings", {
|
var serviceBindings = sqliteTable(
|
||||||
|
"service_bindings",
|
||||||
|
{
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
||||||
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
||||||
@@ -111,10 +114,20 @@ var serviceBindings = sqliteTable("service_bindings", {
|
|||||||
health_check_expected_status: integer("health_check_expected_status"),
|
health_check_expected_status: integer("health_check_expected_status"),
|
||||||
health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30),
|
health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30),
|
||||||
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
|
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
|
||||||
health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" }).notNull().default(false),
|
health_check_verify_tls: integer("health_check_verify_tls", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).notNull().default(false),
|
||||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||||
});
|
},
|
||||||
|
(table) => [
|
||||||
|
unique("service_bindings_domain_service_hostname").on(
|
||||||
|
table.domain_id,
|
||||||
|
table.service_id,
|
||||||
|
table.hostname
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
var serviceIps = sqliteTable("service_ips", {
|
var serviceIps = sqliteTable("service_ips", {
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
||||||
@@ -1189,6 +1202,7 @@ function enrichServiceBindingView(db, row) {
|
|||||||
const sync_status = row.sync_status ?? linkedRecords.find((record) => record.sync_status)?.sync_status ?? null;
|
const sync_status = row.sync_status ?? linkedRecords.find((record) => record.sync_status)?.sync_status ?? null;
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
|
cname_target: row.cname_target ?? null,
|
||||||
target_ips,
|
target_ips,
|
||||||
target_ip: target_ips[0] ?? null,
|
target_ip: target_ips[0] ?? null,
|
||||||
target_ip_weights,
|
target_ip_weights,
|
||||||
@@ -1646,14 +1660,14 @@ function listDomainTags(db, domainId) {
|
|||||||
}
|
}
|
||||||
function setDomainTags(db, domainId, tags) {
|
function setDomainTags(db, domainId, tags) {
|
||||||
db.delete(domainTags).where(eq3(domainTags.domain_id, domainId)).run();
|
db.delete(domainTags).where(eq3(domainTags.domain_id, domainId)).run();
|
||||||
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
const unique2 = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
||||||
for (const tag of unique) {
|
for (const tag of unique2) {
|
||||||
db.insert(domainTags).values({ domain_id: domainId, tag }).run();
|
db.insert(domainTags).values({ domain_id: domainId, tag }).run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function addDomainTags(db, domainId, tags) {
|
function addDomainTags(db, domainId, tags) {
|
||||||
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
const unique2 = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
||||||
for (const tag of unique) {
|
for (const tag of unique2) {
|
||||||
db.run(sql2`
|
db.run(sql2`
|
||||||
INSERT INTO domain_tags (domain_id, tag)
|
INSERT INTO domain_tags (domain_id, tag)
|
||||||
VALUES (${domainId}, ${tag})
|
VALUES (${domainId}, ${tag})
|
||||||
|
|||||||
@@ -1154,6 +1154,7 @@ function enrichServiceBindingView(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
|
cname_target: row.cname_target ?? null,
|
||||||
target_ips,
|
target_ips,
|
||||||
target_ip: target_ips[0] ?? null,
|
target_ip: target_ips[0] ?? null,
|
||||||
target_ip_weights,
|
target_ip_weights,
|
||||||
|
|||||||
Vendored
+1
@@ -78,6 +78,7 @@ interface ServiceBindingView {
|
|||||||
target_ips: string[];
|
target_ips: string[];
|
||||||
target_ip_weights: Record<string, number>;
|
target_ip_weights: Record<string, number>;
|
||||||
target_ip_priorities: Record<string, number>;
|
target_ip_priorities: Record<string, number>;
|
||||||
|
cname_target: string | null;
|
||||||
lb_mode: LbMode;
|
lb_mode: LbMode;
|
||||||
health_check_enabled: boolean;
|
health_check_enabled: boolean;
|
||||||
health_check_type: HealthCheckType;
|
health_check_type: HealthCheckType;
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export interface ServiceBindingView {
|
|||||||
target_ips: string[];
|
target_ips: string[];
|
||||||
target_ip_weights: Record<string, number>;
|
target_ip_weights: Record<string, number>;
|
||||||
target_ip_priorities: Record<string, number>;
|
target_ip_priorities: Record<string, number>;
|
||||||
|
cname_target: string | null;
|
||||||
lb_mode: LbMode;
|
lb_mode: LbMode;
|
||||||
health_check_enabled: boolean;
|
health_check_enabled: boolean;
|
||||||
health_check_type: HealthCheckType;
|
health_check_type: HealthCheckType;
|
||||||
|
|||||||
Reference in New Issue
Block a user