Files
cloudflare-domain-manager/apps/api/src/services/change-ip-service.ts
T
Denozordec 3f6f402872
quality / commitlint (push) Skipped
CD / update-wiki (push) Failing after 8s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 50s
quality / api (push) Successful in 41s
CD / quality (push) Successful in 1m40s
CD / publish (push) Successful in 1m33s
feat(health-checks): enhance health check functionality and add new routes
- Introduced origin health check routes and integrated them into the application.
- Updated health check configuration to include success recovery thresholds.
- Expanded error handling with new error codes for health check failures.
- Added new service routes for managing health checks, including creation and listing.
- Improved health check service logic to track consecutive successes and failures.

This commit enhances the health check capabilities, providing better monitoring and management of service health.
2026-08-19 12:26:12 +07:00

143 lines
4.1 KiB
TypeScript

import type { Db } from "@cfdm/db";
import { repos } from "@cfdm/db";
import type { ChangeIpInput } from "@cfdm/shared";
import type { CloudflareClient } from "../lib/cf-client.js";
import { AppError } from "../errors.js";
import { isValidIpv4 } from "../lib/validators.js";
import { withBindingLock } from "./routing/index.js";
import { applyBindingDesiredDns } from "./service-config-service.js";
export interface ChangeIpPreview {
binding_id: number;
hostname: string;
zone_name: string;
from_ip: string;
to_ip: string;
dry_run: boolean;
applied: boolean;
message: string;
}
async function patchRecordContent(
db: Db,
cf: CloudflareClient,
domainId: number,
recordId: number,
content: string,
): Promise<void> {
const domain = repos.getDomain(db, domainId);
const record = repos.getDnsRecord(db, domainId, recordId);
if (!record.cf_record_id) {
throw AppError.dnsUpdateFailed("у DNS-записи нет идентификатора Cloudflare");
}
try {
const patched = await cf.patchDnsRecord(domain.cf_zone_id, record.cf_record_id, {
content,
});
repos.updateDnsFields(
db,
record.id,
patched.type ?? record.record_type,
patched.name ?? record.name,
patched.content ?? content,
patched.ttl ?? record.ttl,
patched.proxied ?? record.proxied,
patched.priority ?? record.priority,
"synced",
patched.id ?? record.cf_record_id,
null,
);
} catch (err) {
if (err instanceof AppError) throw err;
throw AppError.dnsUpdateFailed(
err instanceof Error ? err.message : "не удалось обновить запись в Cloudflare",
);
}
}
export async function changeBindingIp(
db: Db,
cf: CloudflareClient,
bindingId: number,
input: ChangeIpInput,
): Promise<ChangeIpPreview> {
const binding = repos.getBinding(db, bindingId);
const domain = repos.getDomain(db, binding.domain_id);
const current = repos.listBindingIpsWithMeta(db, bindingId);
if (current.length === 0) {
throw AppError.validation("у привязки нет IP для замены");
}
let fromIp = input.from_ip?.trim();
let toIp = input.to_ip?.trim();
if (input.node_id) {
const node = repos.getNode(db, input.node_id);
if (node.service_id !== binding.service_id) {
throw AppError.validation("нода не принадлежит сервису этой привязки");
}
toIp = node.address;
}
if (!fromIp) {
fromIp = current[0]!.ip;
}
if (!toIp) {
throw AppError.invalidIp("укажите новый IP или ноду");
}
if (!isValidIpv4(toIp)) {
throw AppError.invalidIp(`Некорректный IP-адрес: ${toIp}`);
}
if (!current.some((row) => row.ip === fromIp)) {
throw AppError.validation(`IP ${fromIp} нет в привязке`);
}
const preview: ChangeIpPreview = {
binding_id: bindingId,
hostname: binding.hostname,
zone_name: domain.zone_name,
from_ip: fromIp,
to_ip: toIp,
dry_run: Boolean(input.dry_run),
applied: false,
message: `${fromIp}${toIp}`,
};
if (input.dry_run || fromIp === toIp) {
return preview;
}
return withBindingLock(bindingId, async () => {
repos.bumpBindingVersion(db, bindingId);
const next = current.map((row) =>
row.ip === fromIp ? { ...row, ip: toIp } : row,
);
repos.replaceBindingIpsWithMeta(db, bindingId, next);
const records = repos.listRecordsForBinding(db, bindingId);
const match = records.find(
(record) =>
record.content === fromIp &&
(record.record_type.toUpperCase() === "A" ||
record.record_type.toUpperCase() === "AAAA"),
);
if (match) {
await patchRecordContent(db, cf, binding.domain_id, match.id, toIp);
} else {
await applyBindingDesiredDns(
db,
cf,
bindingId,
next.map((row) => row.ip),
);
}
return {
...preview,
dry_run: false,
applied: true,
message: `Запись обновлена в Cloudflare (${fromIp}${toIp}). Распространение зависит от TTL.`,
};
});
}