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" });
|
||||
}
|
||||
|
||||
const bindings = buildAllSyncBindings(app.db);
|
||||
const bindings = await buildAllSyncBindings(app.db);
|
||||
return {
|
||||
ok: true,
|
||||
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 { repos, getAppSettingsSecrets, touchVpsTrackerSync } from "@cfdm/db";
|
||||
|
||||
@@ -7,23 +8,107 @@ function fqdnToDisplay(hostname: string, zoneName: string): string {
|
||||
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,
|
||||
serviceId: number,
|
||||
deletedBindingIds: number[] = [],
|
||||
): CfdmBindingSyncItem[] {
|
||||
): Promise<CfdmBindingSyncItem[]> {
|
||||
const service = repos.getService(db, serviceId);
|
||||
const serviceIps = repos.listServiceIps(db, serviceId);
|
||||
const allBindings = repos.listAllBindings(db);
|
||||
const index = buildBindingIndex(allBindings);
|
||||
const bindings = repos.listBindingsByService(db, serviceId);
|
||||
const items: CfdmBindingSyncItem[] = bindings.map((binding) => {
|
||||
const targetIps = repos.listBindingIps(db, binding.id);
|
||||
const ips =
|
||||
targetIps.length > 0
|
||||
? targetIps
|
||||
: serviceIps.length > 0
|
||||
? serviceIps
|
||||
: [];
|
||||
return {
|
||||
|
||||
const items: CfdmBindingSyncItem[] = [];
|
||||
for (const binding of bindings) {
|
||||
const ips = await resolveBindingIpsForSync(binding, serviceIps, index);
|
||||
items.push({
|
||||
bindingId: binding.id,
|
||||
serviceId: service.id,
|
||||
serviceName: service.name,
|
||||
@@ -32,8 +117,8 @@ export function buildServiceSyncBindings(
|
||||
zoneName: binding.zone_name,
|
||||
hostname: binding.hostname,
|
||||
ips,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const bindingId of deletedBindingIds) {
|
||||
items.push({
|
||||
@@ -52,17 +137,22 @@ export function buildServiceSyncBindings(
|
||||
return items;
|
||||
}
|
||||
|
||||
export function buildAllSyncBindings(db: Db): CfdmBindingSyncItem[] {
|
||||
export async function buildAllSyncBindings(
|
||||
db: Db,
|
||||
): Promise<CfdmBindingSyncItem[]> {
|
||||
const bindings = repos.listAllBindings(db);
|
||||
return bindings.map((binding) => {
|
||||
const serviceIps = repos.listServiceIps(db, binding.service_id);
|
||||
const ips =
|
||||
binding.target_ips.length > 0
|
||||
? binding.target_ips
|
||||
: serviceIps.length > 0
|
||||
? serviceIps
|
||||
: [];
|
||||
return {
|
||||
const index = buildBindingIndex(bindings);
|
||||
const serviceIpCache = new Map<number, string[]>();
|
||||
|
||||
const items: CfdmBindingSyncItem[] = [];
|
||||
for (const binding of bindings) {
|
||||
let serviceIps = serviceIpCache.get(binding.service_id);
|
||||
if (!serviceIps) {
|
||||
serviceIps = repos.listServiceIps(db, binding.service_id);
|
||||
serviceIpCache.set(binding.service_id, serviceIps);
|
||||
}
|
||||
const ips = await resolveBindingIpsForSync(binding, serviceIps, index);
|
||||
items.push({
|
||||
bindingId: binding.id,
|
||||
serviceId: binding.service_id,
|
||||
serviceName: binding.service_name,
|
||||
@@ -71,8 +161,9 @@ export function buildAllSyncBindings(db: Db): CfdmBindingSyncItem[] {
|
||||
zoneName: binding.zone_name,
|
||||
hostname: binding.hostname,
|
||||
ips,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function syncServiceToVpsTracker(
|
||||
@@ -87,7 +178,11 @@ export async function syncServiceToVpsTracker(
|
||||
const token = config.vpsTrackerIntegrationToken;
|
||||
if (!baseUrl || !token) return;
|
||||
|
||||
const bindings = buildServiceSyncBindings(db, serviceId, deletedBindingIds);
|
||||
const bindings = await buildServiceSyncBindingsAsync(
|
||||
db,
|
||||
serviceId,
|
||||
deletedBindingIds,
|
||||
);
|
||||
if (bindings.length === 0) return;
|
||||
|
||||
try {
|
||||
@@ -133,7 +228,7 @@ export async function syncAllToVpsTracker(db: Db): Promise<{
|
||||
return { ok: false, count: 0, error: "Укажите integration token" };
|
||||
}
|
||||
|
||||
const bindings = buildAllSyncBindings(db);
|
||||
const bindings = await buildAllSyncBindings(db);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/integrations/cfdm/sync-bindings`, {
|
||||
|
||||
Reference in New Issue
Block a user