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
- 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.
143 lines
4.1 KiB
TypeScript
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.`,
|
|
};
|
|
});
|
|
}
|