feat: интеграция с VPS Tracker
Build, Test, and Push CFDM Docker Image / test (push) Failing after 3m36s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Build, Test, and Push CFDM Docker Image / test (push) Failing after 3m36s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Исходящий sync bindings после updateConfig, настройки в app_settings, страница Интеграции, приём событий vps_down для DNS failover. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -23,6 +23,7 @@ import { AppError } from "../errors.js";
|
||||
import { isValidIpv4 } from "../lib/validators.js";
|
||||
import * as dnsService from "./dns-service.js";
|
||||
import * as domainService from "./domain-service.js";
|
||||
import { syncServiceToVpsTracker } from "./vps-tracker-sync.js";
|
||||
|
||||
export interface ServiceDomainInput {
|
||||
fqdn: string;
|
||||
@@ -1100,6 +1101,7 @@ export async function updateConfig(
|
||||
const keptBindingIds: number[] = [];
|
||||
let service = repos.getService(db, id);
|
||||
const pushDns = shouldPushDns(db, service);
|
||||
let removedBindingIds: number[] = [];
|
||||
|
||||
if (req.domains) {
|
||||
if (req.domains.length > 0) {
|
||||
@@ -1179,6 +1181,7 @@ export async function updateConfig(
|
||||
}
|
||||
|
||||
const removed = repos.bindingsToRemove(db, id, keptBindingIds);
|
||||
removedBindingIds = removed.map((binding) => binding.id);
|
||||
for (const binding of removed) {
|
||||
await cleanupBindingDns(
|
||||
db,
|
||||
@@ -1219,6 +1222,8 @@ export async function updateConfig(
|
||||
await syncGroupDomainForService(db, cf, id);
|
||||
}
|
||||
|
||||
void syncServiceToVpsTracker(db, id, removedBindingIds);
|
||||
|
||||
return buildView(db, id);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Db } from "@cfdm/db";
|
||||
import type { CloudflareClient } from "../lib/cf-client.js";
|
||||
import type { VpsTrackerEvent } from "@cfdm/shared";
|
||||
import * as repos from "@cfdm/db/repos";
|
||||
import { reconcileDnsForTarget } from "./service-config-service.js";
|
||||
|
||||
function collectIps(event: VpsTrackerEvent): Set<string> {
|
||||
const ips = new Set<string>();
|
||||
for (const v of event.vps) {
|
||||
if (v.ip?.trim()) ips.add(v.ip.trim());
|
||||
}
|
||||
return ips;
|
||||
}
|
||||
|
||||
function bindingUsesIps(
|
||||
db: Db,
|
||||
serviceId: number,
|
||||
bindingId: number,
|
||||
ips: Set<string>,
|
||||
): boolean {
|
||||
const targetIps = repos.listBindingIps(db, bindingId);
|
||||
const poolIps = repos.listServiceIps(db, serviceId);
|
||||
const effective = targetIps.length > 0 ? targetIps : poolIps;
|
||||
return effective.some((ip) => ips.has(ip));
|
||||
}
|
||||
|
||||
export async function reconcileForVpsDown(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
event: VpsTrackerEvent,
|
||||
): Promise<number> {
|
||||
const ips = collectIps(event);
|
||||
if (ips.size === 0) return 0;
|
||||
|
||||
const seen = new Set<string>();
|
||||
let reconciled = 0;
|
||||
|
||||
for (const service of repos.listServices(db)) {
|
||||
if (!service.enabled) continue;
|
||||
const bindings = repos.listBindingsByService(db, service.id);
|
||||
for (const binding of bindings) {
|
||||
if (!bindingUsesIps(db, service.id, binding.id, ips)) continue;
|
||||
const key = `binding:${binding.id}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
await reconcileDnsForTarget(db, cf, "binding", binding.id);
|
||||
reconciled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return reconciled;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { CfdmBindingSyncItem } from "@cfdm/shared";
|
||||
import type { Db } from "@cfdm/db";
|
||||
import * as repos from "@cfdm/db/repos";
|
||||
import {
|
||||
getAppSettingsSecrets,
|
||||
touchVpsTrackerSync,
|
||||
} from "@cfdm/db/settings-repo";
|
||||
|
||||
function fqdnToDisplay(hostname: string, zoneName: string): string {
|
||||
if (hostname === "@" || !hostname.trim()) return zoneName;
|
||||
return `${hostname}.${zoneName}`;
|
||||
}
|
||||
|
||||
export function buildServiceSyncBindings(
|
||||
db: Db,
|
||||
serviceId: number,
|
||||
deletedBindingIds: number[] = [],
|
||||
): CfdmBindingSyncItem[] {
|
||||
const service = repos.getService(db, serviceId);
|
||||
const serviceIps = repos.listServiceIps(db, serviceId);
|
||||
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 {
|
||||
bindingId: binding.id,
|
||||
serviceId: service.id,
|
||||
serviceName: service.name,
|
||||
serviceSlug: service.slug,
|
||||
fqdn: fqdnToDisplay(binding.hostname, binding.zone_name),
|
||||
zoneName: binding.zone_name,
|
||||
hostname: binding.hostname,
|
||||
ips,
|
||||
};
|
||||
});
|
||||
|
||||
for (const bindingId of deletedBindingIds) {
|
||||
items.push({
|
||||
bindingId,
|
||||
serviceId: service.id,
|
||||
serviceName: service.name,
|
||||
serviceSlug: service.slug,
|
||||
fqdn: "",
|
||||
zoneName: "",
|
||||
hostname: "",
|
||||
ips: [],
|
||||
deleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function syncServiceToVpsTracker(
|
||||
db: Db,
|
||||
serviceId: number,
|
||||
deletedBindingIds: number[] = [],
|
||||
): Promise<void> {
|
||||
const config = getAppSettingsSecrets(db);
|
||||
if (!config.vpsTrackerSyncEnabled) return;
|
||||
|
||||
const baseUrl = config.vpsTrackerUrl.replace(/\/$/, "");
|
||||
const token = config.vpsTrackerIntegrationToken;
|
||||
if (!baseUrl || !token) return;
|
||||
|
||||
const bindings = buildServiceSyncBindings(db, serviceId, deletedBindingIds);
|
||||
if (bindings.length === 0) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/integrations/cfdm/sync-bindings`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ bindings }),
|
||||
});
|
||||
if (res.ok) {
|
||||
touchVpsTrackerSync(db);
|
||||
} else {
|
||||
console.warn(
|
||||
`VPS Tracker sync failed (${res.status}): ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"VPS Tracker sync error:",
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pingVpsTracker(db: Db): Promise<{
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const config = getAppSettingsSecrets(db);
|
||||
const baseUrl = config.vpsTrackerUrl.replace(/\/$/, "");
|
||||
const token = config.vpsTrackerIntegrationToken;
|
||||
if (!baseUrl) return { ok: false, error: "Укажите URL VPS Tracker" };
|
||||
if (!token) return { ok: false, error: "Укажите integration token" };
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/integrations/cfdm/ping`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `HTTP ${res.status}: ${await res.text()}`,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : "Ошибка сети",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user