import type { Db } from "@cfdm/db"; import { repos } from "@cfdm/db"; import type { CreateServiceNodeInput, ServiceNode, ServiceOverview, UpdateServiceNodeInput, } from "@cfdm/shared"; import { AppError } from "../errors.js"; import { isValidIpv4 } from "../lib/validators.js"; import { getView } from "./service-config-service.js"; import { selectActiveIpsByMode } from "./routing/index.js"; function assertAddress(address: string): void { if (!isValidIpv4(address)) { throw AppError.invalidIp(`Некорректный IP-адрес: ${address}`); } } export function listNodes(db: Db, serviceId: number): ServiceNode[] { repos.getService(db, serviceId); return repos.listNodes(db, serviceId); } export function createNode( db: Db, serviceId: number, input: CreateServiceNodeInput, ): ServiceNode { repos.getService(db, serviceId); assertAddress(input.address); try { return repos.createNode(db, serviceId, { address: input.address, protocol: input.protocol, port: input.port, enabled: input.enabled, priority: input.priority, weight: input.weight, health_check_id: input.health_check_id, }); } catch (err) { if (err instanceof Error && err.name === "ConflictError") { throw AppError.conflict(err.message); } throw err; } } export function updateNode( db: Db, serviceId: number, nodeId: number, patch: UpdateServiceNodeInput, ): ServiceNode { const node = repos.getNode(db, nodeId); if (node.service_id !== serviceId) { throw AppError.notFound(`node ${nodeId}`); } if (patch.address) assertAddress(patch.address); return repos.updateNode(db, nodeId, patch); } export function deleteNode(db: Db, serviceId: number, nodeId: number): void { const node = repos.getNode(db, nodeId); if (node.service_id !== serviceId) { throw AppError.notFound(`node ${nodeId}`); } repos.deleteNode(db, nodeId); } export async function getOverview( db: Db, serviceId: number, ): Promise { const service = await getView(db, serviceId); const nodes = repos.listNodes(db, serviceId); const bindings = repos.listBindingsByService(db, serviceId); const first = bindings[0]; const routing = first?.routing_strategy ?? first?.lb_mode ?? "round_robin"; const healthCheck = nodes .map((n) => n.health_check_id) .find((id): id is number => id != null) != null ? repos.getHealthCheck( db, nodes.find((n) => n.health_check_id != null)!.health_check_id!, ) : null; const active = new Set(); for (const binding of bindings) { const metas = repos.listBindingIpsWithMeta(db, binding.id); const rows = metas.map((entry) => { const status = repos.getIpHealthStatusRow(db, "binding", binding.id, entry.ip); return { ip: entry.ip, weight: entry.weight, priority: entry.priority, health: status ? status.status : ("unknown" as const), }; }); for (const ip of selectActiveIpsByMode( { lb_mode: binding.lb_mode, health_check_enabled: binding.health_check_enabled, }, rows, )) { active.add(ip); } } return { service, nodes, health_check: healthCheck, routing_strategy: routing, active_addresses: [...active], }; } export function opsSummary(db: Db) { const allNodes = repos.listAllNodes(db); const services = repos.listServices(db); const domains = repos.listDomains(db); const healthy = allNodes.filter( (n) => n.health_status === "healthy" || n.health_status === "up", ).length; const unhealthy = allNodes.filter( (n) => n.health_status === "unhealthy" || n.health_status === "down", ).length; const failoverActive = repos.listAllBindings(db).filter((binding) => { if (binding.lb_mode !== "failover" || !binding.health_check_enabled) { return false; } return binding.target_ips.some((ip) => { const row = repos.getIpHealthStatusRow(db, "binding", binding.id, ip); return row?.status === "down"; }); }).length; return { domains: domains.length, services: services.length, nodes: allNodes.length, healthy, unhealthy, active_failovers: failoverActive, }; }