feat(health-checks): enhance health check functionality and add new routes
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.
This commit is contained in:
Denozordec
2026-08-19 12:26:12 +07:00
parent 9c00b268dc
commit 3f6f402872
64 changed files with 6356 additions and 360 deletions
+1568 -4
View File
File diff suppressed because one or more lines are too long
+308 -5
View File
@@ -117,6 +117,8 @@ var serviceBindings = sqliteTable(
health_check_verify_tls: integer("health_check_verify_tls", {
mode: "boolean"
}).notNull().default(false),
routing_strategy: text("routing_strategy").notNull().default("round_robin"),
operation_version: integer("operation_version").notNull().default(0),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
},
@@ -128,6 +130,59 @@ var serviceBindings = sqliteTable(
)
]
);
var healthChecks = sqliteTable("health_checks", {
id: integer("id").primaryKey({ autoIncrement: true }),
provider: text("provider").notNull().default("local"),
cf_healthcheck_id: text("cf_healthcheck_id"),
cf_zone_id: text("cf_zone_id"),
name: text("name").notNull(),
protocol: text("protocol").notNull().default("tcp"),
path: text("path"),
method: text("method"),
timeout: integer("timeout").notNull().default(5),
interval_sec: integer("interval_sec").notNull().default(30),
retries: integer("retries").notNull().default(2),
expected_status: integer("expected_status"),
consecutive_fails: integer("consecutive_fails").notNull().default(2),
consecutive_successes: integer("consecutive_successes").notNull().default(2),
suspended: integer("suspended", { mode: "boolean" }).notNull().default(false),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
var nodes = sqliteTable(
"nodes",
{
id: integer("id").primaryKey({ autoIncrement: true }),
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
address: text("address").notNull(),
protocol: text("protocol").notNull().default("tcp"),
port: integer("port"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull().default(1),
weight: integer("weight").notNull().default(1),
health_status: text("health_status").notNull().default("unknown"),
health_check_id: integer("health_check_id").references(() => healthChecks.id, {
onDelete: "set null"
}),
consecutive_failures: integer("consecutive_failures").notNull().default(0),
consecutive_successes: integer("consecutive_successes").notNull().default(0),
last_check_at: text("last_check_at"),
last_failure_reason: text("last_failure_reason"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
},
(t) => [unique("nodes_service_address").on(t.service_id, t.address)]
);
var bindingNodes = sqliteTable(
"binding_nodes",
{
binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }),
node_id: integer("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }),
weight: integer("weight").notNull().default(1),
priority: integer("priority").notNull().default(1)
},
(t) => [primaryKey({ columns: [t.binding_id, t.node_id] })]
);
var serviceIps = sqliteTable("service_ips", {
id: integer("id").primaryKey({ autoIncrement: true }),
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
@@ -193,6 +248,7 @@ var ipHealthStatus = sqliteTable(
status: text("status").notNull().default("unknown"),
latency_ms: integer("latency_ms"),
consecutive_failures: integer("consecutive_failures").notNull().default(0),
consecutive_successes: integer("consecutive_successes").notNull().default(0),
last_checked_at: text("last_checked_at"),
last_error: text("last_error"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
@@ -282,6 +338,9 @@ var schema = {
subdomains,
dnsRecords,
serviceBindings,
healthChecks,
nodes,
bindingNodes,
serviceIps,
serviceBindingRecords,
serviceBindingIps,
@@ -495,10 +554,13 @@ __export(repos_exports, {
aggregateIpHealthByRefs: () => aggregateIpHealthByRefs,
aggregateIpHealthByServiceIds: () => aggregateIpHealthByServiceIds,
bindingsToRemove: () => bindingsToRemove,
bumpBindingVersion: () => bumpBindingVersion,
countCertificatesByStatus: () => countCertificatesByStatus,
createDomain: () => createDomain,
createDomainMonitor: () => createDomainMonitor,
createGroup: () => createGroup,
createHealthCheck: () => createHealthCheck,
createNode: () => createNode,
createService: () => createService,
createServiceGroup: () => createServiceGroup,
createSubdomain: () => createSubdomain,
@@ -510,14 +572,20 @@ __export(repos_exports, {
deleteDomain: () => deleteDomain,
deleteDomainMonitor: () => deleteDomainMonitor,
deleteGroup: () => deleteGroup,
deleteHealthCheck: () => deleteHealthCheck,
deleteIpHealthStatusForIp: () => deleteIpHealthStatusForIp,
deleteIpHealthStatusForRef: () => deleteIpHealthStatusForRef,
deleteNode: () => deleteNode,
deleteService: () => deleteService,
deleteServiceGroup: () => deleteServiceGroup,
deleteSubdomain: () => deleteSubdomain,
ensureNode: () => ensureNode,
findBinding: () => findBinding,
findDnsByCfId: () => findDnsByCfId,
findDomainByZoneName: () => findDomainByZoneName,
findHealthCheckByCfId: () => findHealthCheckByCfId,
findNodeByAddress: () => findNodeByAddress,
findNodeByIp: () => findNodeByIp,
findSubdomainByDomainAndName: () => findSubdomainByDomainAndName,
finishSyncJob: () => finishSyncJob,
getBinding: () => getBinding,
@@ -528,7 +596,9 @@ __export(repos_exports, {
getDomainMonitor: () => getDomainMonitor,
getGroup: () => getGroup,
getGroupWithStats: () => getGroupWithStats,
getHealthCheck: () => getHealthCheck,
getIpHealthStatusRow: () => getIpHealthStatusRow,
getNode: () => getNode,
getService: () => getService,
getServiceGroup: () => getServiceGroup,
getSubdomain: () => getSubdomain,
@@ -540,9 +610,11 @@ __export(repos_exports, {
linkGroupDnsRecord: () => linkGroupDnsRecord,
listAllBindings: () => listAllBindings,
listAllDomains: () => listAllDomains,
listAllNodes: () => listAllNodes,
listAllSubdomains: () => listAllSubdomains,
listBindingIps: () => listBindingIps,
listBindingIpsWithMeta: () => listBindingIpsWithMeta,
listBindingNodes: () => listBindingNodes,
listBindingsByDomain: () => listBindingsByDomain,
listBindingsByService: () => listBindingsByService,
listCertificates: () => listCertificates,
@@ -558,7 +630,9 @@ __export(repos_exports, {
listGroupDnsRecords: () => listGroupDnsRecords,
listGroups: () => listGroups,
listHealthCheckTargets: () => listHealthCheckTargets,
listHealthChecks: () => listHealthChecks,
listIpHealthStatus: () => listIpHealthStatus,
listNodes: () => listNodes,
listNotificationLog: () => listNotificationLog,
listOriginIpsForFqdn: () => listOriginIpsForFqdn,
listRecordsForBinding: () => listRecordsForBinding,
@@ -577,6 +651,7 @@ __export(repos_exports, {
replaceServiceIps: () => replaceServiceIps,
setBindingCnameTarget: () => setBindingCnameTarget,
setBindingDnsRecordId: () => setBindingDnsRecordId,
setBindingRoutingStrategy: () => setBindingRoutingStrategy,
setDnsSyncStatus: () => setDnsSyncStatus,
setDomainLastSynced: () => setDomainLastSynced,
setDomainTags: () => setDomainTags,
@@ -586,12 +661,15 @@ __export(repos_exports, {
setServiceLb: () => setServiceLb,
unlinkBindingRecord: () => unlinkBindingRecord,
unlinkGroupDnsRecord: () => unlinkGroupDnsRecord,
updateBindingDomain: () => updateBindingDomain,
updateBindingFields: () => updateBindingFields,
updateBindingLbConfig: () => updateBindingLbConfig,
updateDnsFields: () => updateDnsFields,
updateDomain: () => updateDomain,
updateDomainMonitorResult: () => updateDomainMonitorResult,
updateGroup: () => updateGroup,
updateHealthCheck: () => updateHealthCheck,
updateNode: () => updateNode,
updateService: () => updateService,
updateServiceGroup: () => updateServiceGroup,
updateSubdomain: () => updateSubdomain,
@@ -1073,6 +1151,12 @@ function deleteServiceGroup(db, id) {
const result = db.delete(serviceGroups).where(eq3(serviceGroups.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
}
function insertServiceIpIfMissing(db, serviceId, ip) {
const existing = db.select({ ip: serviceIps.ip }).from(serviceIps).where(and2(eq3(serviceIps.service_id, serviceId), eq3(serviceIps.ip, ip))).get();
if (!existing) {
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
}
}
function listServiceIps(db, serviceId) {
return db.select({ ip: serviceIps.ip }).from(serviceIps).where(eq3(serviceIps.service_id, serviceId)).all().map((r) => r.ip);
}
@@ -1080,7 +1164,207 @@ function replaceServiceIps(db, serviceId, ips) {
db.delete(serviceIps).where(eq3(serviceIps.service_id, serviceId)).run();
for (const ip of ips) {
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
ensureNode(db, serviceId, ip);
}
const keep = new Set(ips);
for (const node of listNodes(db, serviceId)) {
if (keep.has(node.address)) continue;
const bound = db.select({ node_id: bindingNodes.node_id }).from(bindingNodes).where(eq3(bindingNodes.node_id, node.id)).get();
if (!bound) deleteNode(db, node.id);
}
}
function mapNode(row) {
return {
id: row.id,
service_id: row.service_id,
address: row.address,
protocol: row.protocol,
port: row.port,
enabled: Boolean(row.enabled),
priority: row.priority,
weight: row.weight,
health_status: row.health_status,
health_check_id: row.health_check_id,
consecutive_failures: row.consecutive_failures,
consecutive_successes: row.consecutive_successes,
last_check_at: row.last_check_at,
last_failure_reason: row.last_failure_reason,
created_at: row.created_at,
updated_at: row.updated_at
};
}
function listNodes(db, serviceId) {
return db.select().from(nodes).where(eq3(nodes.service_id, serviceId)).all().map(mapNode);
}
function getNode(db, id) {
const row = db.select().from(nodes).where(eq3(nodes.id, id)).get();
if (!row) throw new NotFoundError(`node ${id}`);
return mapNode(row);
}
function findNodeByAddress(db, serviceId, address) {
const row = db.select().from(nodes).where(and2(eq3(nodes.service_id, serviceId), eq3(nodes.address, address))).get();
return row ? mapNode(row) : null;
}
function findNodeByIp(db, address) {
const row = db.select().from(nodes).where(eq3(nodes.address, address)).get();
return row ? mapNode(row) : null;
}
function ensureNode(db, serviceId, address, meta) {
const existing = findNodeByAddress(db, serviceId, address);
if (existing) return existing;
const id = db.insert(nodes).values({
service_id: serviceId,
address,
protocol: meta?.protocol ?? "tcp",
port: meta?.port ?? null,
weight: meta?.weight ?? 1,
priority: meta?.priority ?? 1
}).returning({ id: nodes.id }).get().id;
return getNode(db, id);
}
function createNode(db, serviceId, input) {
getService(db, serviceId);
const existing = findNodeByAddress(db, serviceId, input.address);
if (existing) {
throw new ConflictError(`node ${input.address} already exists`);
}
const id = db.insert(nodes).values({
service_id: serviceId,
address: input.address,
protocol: input.protocol ?? "tcp",
port: input.port ?? null,
enabled: input.enabled ?? true,
priority: input.priority ?? 1,
weight: input.weight ?? 1,
health_check_id: input.health_check_id ?? null
}).returning({ id: nodes.id }).get().id;
insertServiceIpIfMissing(db, serviceId, input.address);
return getNode(db, id);
}
function updateNode(db, id, patch) {
const current = getNode(db, id);
const update = { updated_at: sql2`datetime('now')` };
for (const [key, value] of Object.entries(patch)) {
if (value !== void 0) update[key] = value;
}
db.update(nodes).set(update).where(eq3(nodes.id, id)).run();
if (patch.address && patch.address !== current.address) {
db.delete(serviceIps).where(
and2(
eq3(serviceIps.service_id, current.service_id),
eq3(serviceIps.ip, current.address)
)
).run();
insertServiceIpIfMissing(db, current.service_id, patch.address);
}
return getNode(db, id);
}
function deleteNode(db, id) {
const current = getNode(db, id);
db.delete(nodes).where(eq3(nodes.id, id)).run();
db.delete(serviceIps).where(
and2(
eq3(serviceIps.service_id, current.service_id),
eq3(serviceIps.ip, current.address)
)
).run();
}
function mapHealthCheck(row) {
return {
id: row.id,
provider: row.provider,
cf_healthcheck_id: row.cf_healthcheck_id,
cf_zone_id: row.cf_zone_id,
name: row.name,
protocol: row.protocol,
path: row.path,
method: row.method,
timeout: row.timeout,
interval_sec: row.interval_sec,
retries: row.retries,
expected_status: row.expected_status,
consecutive_fails: row.consecutive_fails,
consecutive_successes: row.consecutive_successes,
suspended: Boolean(row.suspended),
created_at: row.created_at,
updated_at: row.updated_at
};
}
function listHealthChecks(db) {
return db.select().from(healthChecks).all().map(mapHealthCheck);
}
function getHealthCheck(db, id) {
const row = db.select().from(healthChecks).where(eq3(healthChecks.id, id)).get();
if (!row) throw new NotFoundError(`health check ${id}`);
return mapHealthCheck(row);
}
function findHealthCheckByCfId(db, cfId) {
const row = db.select().from(healthChecks).where(eq3(healthChecks.cf_healthcheck_id, cfId)).get();
return row ? mapHealthCheck(row) : null;
}
function createHealthCheck(db, input) {
const id = db.insert(healthChecks).values({
provider: input.provider,
name: input.name,
cf_healthcheck_id: input.cf_healthcheck_id ?? null,
cf_zone_id: input.cf_zone_id ?? null,
protocol: input.protocol ?? "tcp",
path: input.path ?? null,
method: input.method ?? null,
timeout: input.timeout ?? 5,
interval_sec: input.interval_sec ?? 30,
retries: input.retries ?? 2,
expected_status: input.expected_status ?? null,
consecutive_fails: input.consecutive_fails ?? 2,
consecutive_successes: input.consecutive_successes ?? 2,
suspended: input.suspended ?? false
}).returning({ id: healthChecks.id }).get().id;
return getHealthCheck(db, id);
}
function updateHealthCheck(db, id, patch) {
getHealthCheck(db, id);
const update = { updated_at: sql2`datetime('now')` };
for (const [key, value] of Object.entries(patch)) {
if (value !== void 0) update[key] = value;
}
db.update(healthChecks).set(update).where(eq3(healthChecks.id, id)).run();
return getHealthCheck(db, id);
}
function deleteHealthCheck(db, id) {
const result = db.delete(healthChecks).where(eq3(healthChecks.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`health check ${id}`);
}
function bumpBindingVersion(db, bindingId, expected) {
const binding = getBinding(db, bindingId);
if (expected != null && binding.operation_version !== expected) {
throw new ConflictError(`binding ${bindingId} version conflict`);
}
const next = (binding.operation_version ?? 0) + 1;
db.update(serviceBindings).set({
operation_version: next,
updated_at: sql2`datetime('now')`
}).where(eq3(serviceBindings.id, bindingId)).run();
return next;
}
function setBindingRoutingStrategy(db, bindingId, strategy) {
db.update(serviceBindings).set({
routing_strategy: strategy,
lb_mode: strategy,
updated_at: sql2`datetime('now')`
}).where(eq3(serviceBindings.id, bindingId)).run();
}
function listAllNodes(db) {
return db.select().from(nodes).all().map(mapNode);
}
function updateBindingDomain(db, bindingId, domainId, hostname) {
db.update(serviceBindings).set({
domain_id: domainId,
hostname,
updated_at: sql2`datetime('now')`
}).where(eq3(serviceBindings.id, bindingId)).run();
}
function listBindingNodes(db, bindingId) {
return db.select({ node: nodes }).from(bindingNodes).innerJoin(nodes, eq3(bindingNodes.node_id, nodes.id)).where(eq3(bindingNodes.binding_id, bindingId)).all().map((row) => mapNode(row.node));
}
function listBindingIps(db, bindingId) {
return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip);
@@ -1100,7 +1384,9 @@ function replaceBindingIps(db, bindingId, ips) {
);
}
function replaceBindingIpsWithMeta(db, bindingId, entries) {
const binding = getBinding(db, bindingId);
db.delete(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).run();
db.delete(bindingNodes).where(eq3(bindingNodes.binding_id, bindingId)).run();
for (const entry of entries) {
db.insert(serviceBindingIps).values({
binding_id: bindingId,
@@ -1108,13 +1394,26 @@ function replaceBindingIpsWithMeta(db, bindingId, entries) {
weight: entry.weight,
priority: entry.priority
}).run();
const node = ensureNode(db, binding.service_id, entry.ip, {
weight: entry.weight,
priority: entry.priority
});
db.insert(bindingNodes).values({
binding_id: bindingId,
node_id: node.id,
weight: entry.weight,
priority: entry.priority
}).run();
}
}
function updateBindingLbConfig(db, bindingId, patch) {
const update = {
updated_at: sql2`datetime('now')`
};
if (patch.lb_mode !== void 0) update.lb_mode = patch.lb_mode;
if (patch.lb_mode !== void 0) {
update.lb_mode = patch.lb_mode;
update.routing_strategy = patch.lb_mode;
}
if (patch.health_check_enabled !== void 0)
update.health_check_enabled = patch.health_check_enabled;
if (patch.health_check_type !== void 0)
@@ -1552,24 +1851,25 @@ function mergeHealthAggregates(parts) {
function getIpHealthStatusRow(db, scope, refId, ip) {
const rows = db.all(sql2`
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
last_checked_at, last_error
consecutive_successes, last_checked_at, last_error
FROM ip_health_status
WHERE scope = ${scope} AND ref_id = ${refId} AND ip = ${ip}
LIMIT 1
`);
return rows[0] ?? null;
}
function upsertIpHealthStatus(db, scope, refId, ip, status, latencyMs, consecutiveFailures, lastError) {
function upsertIpHealthStatus(db, scope, refId, ip, status, latencyMs, consecutiveFailures, lastError, consecutiveSuccesses = 0) {
db.run(sql2`
INSERT INTO ip_health_status
(scope, ref_id, ip, status, latency_ms, consecutive_failures,
last_checked_at, last_error, created_at, updated_at)
consecutive_successes, last_checked_at, last_error, created_at, updated_at)
VALUES (${scope}, ${refId}, ${ip}, ${status}, ${latencyMs}, ${consecutiveFailures},
datetime('now'), ${lastError}, datetime('now'), datetime('now'))
${consecutiveSuccesses}, datetime('now'), ${lastError}, datetime('now'), datetime('now'))
ON CONFLICT(scope, ref_id, ip) DO UPDATE SET
status = excluded.status,
latency_ms = excluded.latency_ms,
consecutive_failures = excluded.consecutive_failures,
consecutive_successes = excluded.consecutive_successes,
last_checked_at = excluded.last_checked_at,
last_error = excluded.last_error,
updated_at = datetime('now')
@@ -1844,6 +2144,7 @@ export {
appSettings,
appendAudit,
auditLog,
bindingNodes,
certificates,
createDb,
createMemoryDb,
@@ -1856,8 +2157,10 @@ export {
getAppSettingsSecrets,
groups,
healthCheck,
healthChecks,
ipHealthStatus,
listAudit,
nodes,
notificationLog,
repos_exports as repos,
resolveDatabasePath,
@@ -0,0 +1,83 @@
CREATE TABLE IF NOT EXISTS health_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL DEFAULT 'local',
cf_healthcheck_id TEXT,
cf_zone_id TEXT,
name TEXT NOT NULL,
protocol TEXT NOT NULL DEFAULT 'tcp',
path TEXT,
method TEXT,
timeout INTEGER NOT NULL DEFAULT 5,
interval_sec INTEGER NOT NULL DEFAULT 30,
retries INTEGER NOT NULL DEFAULT 2,
expected_status INTEGER,
consecutive_fails INTEGER NOT NULL DEFAULT 2,
consecutive_successes INTEGER NOT NULL DEFAULT 2,
suspended INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
address TEXT NOT NULL,
protocol TEXT NOT NULL DEFAULT 'tcp',
port INTEGER,
enabled INTEGER NOT NULL DEFAULT 1,
priority INTEGER NOT NULL DEFAULT 1,
weight INTEGER NOT NULL DEFAULT 1,
health_status TEXT NOT NULL DEFAULT 'unknown',
health_check_id INTEGER REFERENCES health_checks(id) ON DELETE SET NULL,
consecutive_failures INTEGER NOT NULL DEFAULT 0,
consecutive_successes INTEGER NOT NULL DEFAULT 0,
last_check_at TEXT,
last_failure_reason TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(service_id, address)
);
CREATE TABLE IF NOT EXISTS binding_nodes (
binding_id INTEGER NOT NULL REFERENCES service_bindings(id) ON DELETE CASCADE,
node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
weight INTEGER NOT NULL DEFAULT 1,
priority INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (binding_id, node_id)
);
CREATE INDEX IF NOT EXISTS idx_nodes_service ON nodes(service_id);
CREATE INDEX IF NOT EXISTS idx_binding_nodes_node ON binding_nodes(node_id);
CREATE INDEX IF NOT EXISTS idx_health_checks_cf ON health_checks(cf_healthcheck_id);
ALTER TABLE service_bindings ADD COLUMN routing_strategy TEXT NOT NULL DEFAULT 'round_robin';
ALTER TABLE service_bindings ADD COLUMN operation_version INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ip_health_status ADD COLUMN consecutive_successes INTEGER NOT NULL DEFAULT 0;
INSERT INTO nodes (service_id, address, enabled, priority, weight, health_status)
SELECT service_id, ip, 1, 1, 1, 'unknown'
FROM service_ips
WHERE NOT EXISTS (
SELECT 1 FROM nodes n WHERE n.service_id = service_ips.service_id AND n.address = service_ips.ip
);
INSERT INTO nodes (service_id, address, enabled, priority, weight, health_status)
SELECT DISTINCT sb.service_id, sbi.ip, 1, sbi.priority, sbi.weight, 'unknown'
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
WHERE NOT EXISTS (
SELECT 1 FROM nodes n WHERE n.service_id = sb.service_id AND n.address = sbi.ip
);
INSERT INTO binding_nodes (binding_id, node_id, weight, priority)
SELECT sbi.binding_id, n.id, sbi.weight, sbi.priority
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
JOIN nodes n ON n.service_id = sb.service_id AND n.address = sbi.ip
WHERE NOT EXISTS (
SELECT 1 FROM binding_nodes bn
WHERE bn.binding_id = sbi.binding_id AND bn.node_id = n.id
);
UPDATE service_bindings SET routing_strategy = lb_mode WHERE routing_strategy = 'round_robin';
+398 -5
View File
@@ -11,17 +11,19 @@ import type {
IpHealthState,
IpHealthStatus,
LbMode,
OriginHealthCheck,
Service,
ServiceBinding,
ServiceBindingView,
ServiceGroup,
ServiceNode,
Subdomain,
SyncJob,
} from "@cfdm/shared";
import { dnsRecordNamesMatch, isIpLiteral } from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, notInArray, or, sql } from "drizzle-orm";
import type { Db } from "./client.js";
import { NotFoundError } from "./errors.js";
import { ConflictError, NotFoundError } from "./errors.js";
import {
certificates,
dnsRecords,
@@ -30,7 +32,10 @@ import {
domainTags,
domains,
groups,
healthChecks,
ipHealthStatus,
nodes,
bindingNodes,
notificationLog,
serviceBindingIps,
serviceBindingRecords,
@@ -911,6 +916,17 @@ export function deleteServiceGroup(db: Db, id: number): void {
// --- Service IPs ---
function insertServiceIpIfMissing(db: Db, serviceId: number, ip: string): void {
const existing = db
.select({ ip: serviceIps.ip })
.from(serviceIps)
.where(and(eq(serviceIps.service_id, serviceId), eq(serviceIps.ip, ip)))
.get();
if (!existing) {
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
}
}
export function listServiceIps(db: Db, serviceId: number): string[] {
return db
.select({ ip: serviceIps.ip })
@@ -928,7 +944,363 @@ export function replaceServiceIps(
db.delete(serviceIps).where(eq(serviceIps.service_id, serviceId)).run();
for (const ip of ips) {
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
ensureNode(db, serviceId, ip);
}
const keep = new Set(ips);
for (const node of listNodes(db, serviceId)) {
if (keep.has(node.address)) continue;
const bound = db
.select({ node_id: bindingNodes.node_id })
.from(bindingNodes)
.where(eq(bindingNodes.node_id, node.id))
.get();
if (!bound) deleteNode(db, node.id);
}
}
function mapNode(row: typeof nodes.$inferSelect): ServiceNode {
return {
id: row.id,
service_id: row.service_id,
address: row.address,
protocol: row.protocol,
port: row.port,
enabled: Boolean(row.enabled),
priority: row.priority,
weight: row.weight,
health_status: row.health_status as ServiceNode["health_status"],
health_check_id: row.health_check_id,
consecutive_failures: row.consecutive_failures,
consecutive_successes: row.consecutive_successes,
last_check_at: row.last_check_at,
last_failure_reason: row.last_failure_reason,
created_at: row.created_at,
updated_at: row.updated_at,
};
}
export function listNodes(db: Db, serviceId: number): ServiceNode[] {
return db
.select()
.from(nodes)
.where(eq(nodes.service_id, serviceId))
.all()
.map(mapNode);
}
export function getNode(db: Db, id: number): ServiceNode {
const row = db.select().from(nodes).where(eq(nodes.id, id)).get();
if (!row) throw new NotFoundError(`node ${id}`);
return mapNode(row);
}
export function findNodeByAddress(
db: Db,
serviceId: number,
address: string,
): ServiceNode | null {
const row = db
.select()
.from(nodes)
.where(and(eq(nodes.service_id, serviceId), eq(nodes.address, address)))
.get();
return row ? mapNode(row) : null;
}
export function findNodeByIp(db: Db, address: string): ServiceNode | null {
const row = db.select().from(nodes).where(eq(nodes.address, address)).get();
return row ? mapNode(row) : null;
}
export function ensureNode(
db: Db,
serviceId: number,
address: string,
meta?: { weight?: number; priority?: number; protocol?: string; port?: number | null },
): ServiceNode {
const existing = findNodeByAddress(db, serviceId, address);
if (existing) return existing;
const id = db
.insert(nodes)
.values({
service_id: serviceId,
address,
protocol: meta?.protocol ?? "tcp",
port: meta?.port ?? null,
weight: meta?.weight ?? 1,
priority: meta?.priority ?? 1,
})
.returning({ id: nodes.id })
.get()!.id;
return getNode(db, id);
}
export function createNode(
db: Db,
serviceId: number,
input: {
address: string;
protocol?: string;
port?: number | null;
enabled?: boolean;
priority?: number;
weight?: number;
health_check_id?: number | null;
},
): ServiceNode {
getService(db, serviceId);
const existing = findNodeByAddress(db, serviceId, input.address);
if (existing) {
throw new ConflictError(`node ${input.address} already exists`);
}
const id = db
.insert(nodes)
.values({
service_id: serviceId,
address: input.address,
protocol: input.protocol ?? "tcp",
port: input.port ?? null,
enabled: input.enabled ?? true,
priority: input.priority ?? 1,
weight: input.weight ?? 1,
health_check_id: input.health_check_id ?? null,
})
.returning({ id: nodes.id })
.get()!.id;
insertServiceIpIfMissing(db, serviceId, input.address);
return getNode(db, id);
}
export function updateNode(
db: Db,
id: number,
patch: Partial<{
address: string;
protocol: string;
port: number | null;
enabled: boolean;
priority: number;
weight: number;
health_check_id: number | null;
health_status: string;
consecutive_failures: number;
consecutive_successes: number;
last_check_at: string | null;
last_failure_reason: string | null;
}>,
): ServiceNode {
const current = getNode(db, id);
const update: Record<string, unknown> = { updated_at: sql`datetime('now')` };
for (const [key, value] of Object.entries(patch)) {
if (value !== undefined) update[key] = value;
}
db.update(nodes).set(update).where(eq(nodes.id, id)).run();
if (patch.address && patch.address !== current.address) {
db.delete(serviceIps)
.where(
and(
eq(serviceIps.service_id, current.service_id),
eq(serviceIps.ip, current.address),
),
)
.run();
insertServiceIpIfMissing(db, current.service_id, patch.address);
}
return getNode(db, id);
}
export function deleteNode(db: Db, id: number): void {
const current = getNode(db, id);
db.delete(nodes).where(eq(nodes.id, id)).run();
db.delete(serviceIps)
.where(
and(
eq(serviceIps.service_id, current.service_id),
eq(serviceIps.ip, current.address),
),
)
.run();
}
function mapHealthCheck(row: typeof healthChecks.$inferSelect): OriginHealthCheck {
return {
id: row.id,
provider: row.provider as OriginHealthCheck["provider"],
cf_healthcheck_id: row.cf_healthcheck_id,
cf_zone_id: row.cf_zone_id,
name: row.name,
protocol: row.protocol,
path: row.path,
method: row.method,
timeout: row.timeout,
interval_sec: row.interval_sec,
retries: row.retries,
expected_status: row.expected_status,
consecutive_fails: row.consecutive_fails,
consecutive_successes: row.consecutive_successes,
suspended: Boolean(row.suspended),
created_at: row.created_at,
updated_at: row.updated_at,
};
}
export function listHealthChecks(db: Db): OriginHealthCheck[] {
return db.select().from(healthChecks).all().map(mapHealthCheck);
}
export function getHealthCheck(db: Db, id: number): OriginHealthCheck {
const row = db.select().from(healthChecks).where(eq(healthChecks.id, id)).get();
if (!row) throw new NotFoundError(`health check ${id}`);
return mapHealthCheck(row);
}
export function findHealthCheckByCfId(
db: Db,
cfId: string,
): OriginHealthCheck | null {
const row = db
.select()
.from(healthChecks)
.where(eq(healthChecks.cf_healthcheck_id, cfId))
.get();
return row ? mapHealthCheck(row) : null;
}
export function createHealthCheck(
db: Db,
input: {
provider: string;
name: string;
cf_healthcheck_id?: string | null;
cf_zone_id?: string | null;
protocol?: string;
path?: string | null;
method?: string | null;
timeout?: number;
interval_sec?: number;
retries?: number;
expected_status?: number | null;
consecutive_fails?: number;
consecutive_successes?: number;
suspended?: boolean;
},
): OriginHealthCheck {
const id = db
.insert(healthChecks)
.values({
provider: input.provider,
name: input.name,
cf_healthcheck_id: input.cf_healthcheck_id ?? null,
cf_zone_id: input.cf_zone_id ?? null,
protocol: input.protocol ?? "tcp",
path: input.path ?? null,
method: input.method ?? null,
timeout: input.timeout ?? 5,
interval_sec: input.interval_sec ?? 30,
retries: input.retries ?? 2,
expected_status: input.expected_status ?? null,
consecutive_fails: input.consecutive_fails ?? 2,
consecutive_successes: input.consecutive_successes ?? 2,
suspended: input.suspended ?? false,
})
.returning({ id: healthChecks.id })
.get()!.id;
return getHealthCheck(db, id);
}
export function updateHealthCheck(
db: Db,
id: number,
patch: Partial<{
provider: string;
name: string;
cf_healthcheck_id: string | null;
cf_zone_id: string | null;
protocol: string;
path: string | null;
method: string | null;
timeout: number;
interval_sec: number;
retries: number;
expected_status: number | null;
consecutive_fails: number;
consecutive_successes: number;
suspended: boolean;
}>,
): OriginHealthCheck {
getHealthCheck(db, id);
const update: Record<string, unknown> = { updated_at: sql`datetime('now')` };
for (const [key, value] of Object.entries(patch)) {
if (value !== undefined) update[key] = value;
}
db.update(healthChecks).set(update).where(eq(healthChecks.id, id)).run();
return getHealthCheck(db, id);
}
export function deleteHealthCheck(db: Db, id: number): void {
const result = db.delete(healthChecks).where(eq(healthChecks.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`health check ${id}`);
}
export function bumpBindingVersion(db: Db, bindingId: number, expected?: number): number {
const binding = getBinding(db, bindingId);
if (expected != null && binding.operation_version !== expected) {
throw new ConflictError(`binding ${bindingId} version conflict`);
}
const next = (binding.operation_version ?? 0) + 1;
db.update(serviceBindings)
.set({
operation_version: next,
updated_at: sql`datetime('now')`,
})
.where(eq(serviceBindings.id, bindingId))
.run();
return next;
}
export function setBindingRoutingStrategy(
db: Db,
bindingId: number,
strategy: LbMode,
): void {
db.update(serviceBindings)
.set({
routing_strategy: strategy,
lb_mode: strategy,
updated_at: sql`datetime('now')`,
})
.where(eq(serviceBindings.id, bindingId))
.run();
}
export function listAllNodes(db: Db): ServiceNode[] {
return db.select().from(nodes).all().map(mapNode);
}
export function updateBindingDomain(
db: Db,
bindingId: number,
domainId: number,
hostname: string,
): void {
db.update(serviceBindings)
.set({
domain_id: domainId,
hostname,
updated_at: sql`datetime('now')`,
})
.where(eq(serviceBindings.id, bindingId))
.run();
}
export function listBindingNodes(db: Db, bindingId: number): ServiceNode[] {
return db
.select({ node: nodes })
.from(bindingNodes)
.innerJoin(nodes, eq(bindingNodes.node_id, nodes.id))
.where(eq(bindingNodes.binding_id, bindingId))
.all()
.map((row) => mapNode(row.node));
}
// --- Service Binding IPs ---
@@ -980,9 +1352,13 @@ export function replaceBindingIpsWithMeta(
bindingId: number,
entries: BindingIpMeta[],
): void {
const binding = getBinding(db, bindingId);
db.delete(serviceBindingIps)
.where(eq(serviceBindingIps.binding_id, bindingId))
.run();
db.delete(bindingNodes)
.where(eq(bindingNodes.binding_id, bindingId))
.run();
for (const entry of entries) {
db.insert(serviceBindingIps)
.values({
@@ -992,6 +1368,18 @@ export function replaceBindingIpsWithMeta(
priority: entry.priority,
})
.run();
const node = ensureNode(db, binding.service_id, entry.ip, {
weight: entry.weight,
priority: entry.priority,
});
db.insert(bindingNodes)
.values({
binding_id: bindingId,
node_id: node.id,
weight: entry.weight,
priority: entry.priority,
})
.run();
}
}
@@ -1015,7 +1403,10 @@ export function updateBindingLbConfig(
const update: Record<string, unknown> = {
updated_at: sql`datetime('now')`,
};
if (patch.lb_mode !== undefined) update.lb_mode = patch.lb_mode;
if (patch.lb_mode !== undefined) {
update.lb_mode = patch.lb_mode;
update.routing_strategy = patch.lb_mode;
}
if (patch.health_check_enabled !== undefined)
update.health_check_enabled = patch.health_check_enabled;
if (patch.health_check_type !== undefined)
@@ -1721,7 +2112,7 @@ export function getIpHealthStatusRow(
): IpHealthStatus | null {
const rows = db.all<IpHealthStatus>(sql`
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
last_checked_at, last_error
consecutive_successes, last_checked_at, last_error
FROM ip_health_status
WHERE scope = ${scope} AND ref_id = ${refId} AND ip = ${ip}
LIMIT 1
@@ -1738,17 +2129,19 @@ export function upsertIpHealthStatus(
latencyMs: number | null,
consecutiveFailures: number,
lastError: string | null,
consecutiveSuccesses = 0,
): void {
db.run(sql`
INSERT INTO ip_health_status
(scope, ref_id, ip, status, latency_ms, consecutive_failures,
last_checked_at, last_error, created_at, updated_at)
consecutive_successes, last_checked_at, last_error, created_at, updated_at)
VALUES (${scope}, ${refId}, ${ip}, ${status}, ${latencyMs}, ${consecutiveFailures},
datetime('now'), ${lastError}, datetime('now'), datetime('now'))
${consecutiveSuccesses}, datetime('now'), ${lastError}, datetime('now'), datetime('now'))
ON CONFLICT(scope, ref_id, ip) DO UPDATE SET
status = excluded.status,
latency_ms = excluded.latency_ms,
consecutive_failures = excluded.consecutive_failures,
consecutive_successes = excluded.consecutive_successes,
last_checked_at = excluded.last_checked_at,
last_error = excluded.last_error,
updated_at = datetime('now')
+78
View File
@@ -165,6 +165,8 @@ export const serviceBindings = sqliteTable(
})
.notNull()
.default(false),
routing_strategy: text("routing_strategy").notNull().default("round_robin"),
operation_version: integer("operation_version").notNull().default(0),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -181,6 +183,76 @@ export const serviceBindings = sqliteTable(
],
);
export const healthChecks = sqliteTable("health_checks", {
id: integer("id").primaryKey({ autoIncrement: true }),
provider: text("provider").notNull().default("local"),
cf_healthcheck_id: text("cf_healthcheck_id"),
cf_zone_id: text("cf_zone_id"),
name: text("name").notNull(),
protocol: text("protocol").notNull().default("tcp"),
path: text("path"),
method: text("method"),
timeout: integer("timeout").notNull().default(5),
interval_sec: integer("interval_sec").notNull().default(30),
retries: integer("retries").notNull().default(2),
expected_status: integer("expected_status"),
consecutive_fails: integer("consecutive_fails").notNull().default(2),
consecutive_successes: integer("consecutive_successes").notNull().default(2),
suspended: integer("suspended", { mode: "boolean" }).notNull().default(false),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
updated_at: text("updated_at")
.notNull()
.default(sql`datetime('now')`),
});
export const nodes = sqliteTable(
"nodes",
{
id: integer("id").primaryKey({ autoIncrement: true }),
service_id: integer("service_id")
.notNull()
.references(() => services.id, { onDelete: "cascade" }),
address: text("address").notNull(),
protocol: text("protocol").notNull().default("tcp"),
port: integer("port"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull().default(1),
weight: integer("weight").notNull().default(1),
health_status: text("health_status").notNull().default("unknown"),
health_check_id: integer("health_check_id").references(() => healthChecks.id, {
onDelete: "set null",
}),
consecutive_failures: integer("consecutive_failures").notNull().default(0),
consecutive_successes: integer("consecutive_successes").notNull().default(0),
last_check_at: text("last_check_at"),
last_failure_reason: text("last_failure_reason"),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
updated_at: text("updated_at")
.notNull()
.default(sql`datetime('now')`),
},
(t) => [unique("nodes_service_address").on(t.service_id, t.address)],
);
export const bindingNodes = sqliteTable(
"binding_nodes",
{
binding_id: integer("binding_id")
.notNull()
.references(() => serviceBindings.id, { onDelete: "cascade" }),
node_id: integer("node_id")
.notNull()
.references(() => nodes.id, { onDelete: "cascade" }),
weight: integer("weight").notNull().default(1),
priority: integer("priority").notNull().default(1),
},
(t) => [primaryKey({ columns: [t.binding_id, t.node_id] })],
);
export const serviceIps = sqliteTable("service_ips", {
id: integer("id").primaryKey({ autoIncrement: true }),
service_id: integer("service_id")
@@ -276,6 +348,9 @@ export const ipHealthStatus = sqliteTable(
consecutive_failures: integer("consecutive_failures")
.notNull()
.default(0),
consecutive_successes: integer("consecutive_successes")
.notNull()
.default(0),
last_checked_at: text("last_checked_at"),
last_error: text("last_error"),
created_at: text("created_at")
@@ -399,6 +474,9 @@ export const schema = {
subdomains,
dnsRecords,
serviceBindings,
healthChecks,
nodes,
bindingNodes,
serviceIps,
serviceBindingRecords,
serviceBindingIps,
+219 -1
View File
@@ -60,6 +60,8 @@ interface ServiceBinding {
health_check_interval_sec: number;
health_check_timeout_ms: number;
health_check_verify_tls: boolean;
routing_strategy: LbMode;
operation_version: number;
created_at: string;
updated_at: string;
}
@@ -114,6 +116,23 @@ interface ServiceDomainBindingView {
health_check_verify_tls: boolean;
sync_status: string | null;
}
interface ServiceView$1 {
id: number;
name: string;
slug: string;
service_group_id: number | null;
subdomain: string;
enabled: boolean;
computed_fqdn: string | null;
lb_weight: number;
lb_priority: number;
created_at: string;
updated_at: string;
ips: string[];
domains: ServiceDomainBindingView[];
health_status: IpHealthState;
health_latency_ms: number | null;
}
interface SyncJob {
id: string;
status: string;
@@ -159,6 +178,8 @@ interface JwtClaims {
type LbMode = "round_robin" | "failover" | "weighted";
type HealthCheckType = "tcp" | "http" | "ping" | "dns";
type IpHealthState = "up" | "down" | "degraded" | "unknown";
type NodeHealthState = "unknown" | "checking" | "healthy" | "degraded" | "unhealthy" | "disabled";
type HealthCheckProvider = "local" | "cloudflare";
type HealthCheckScope = "binding" | "group";
interface IpHealthStatus {
scope: HealthCheckScope;
@@ -167,9 +188,75 @@ interface IpHealthStatus {
status: IpHealthState;
latency_ms: number | null;
consecutive_failures: number;
consecutive_successes?: number;
last_checked_at: string | null;
last_error: string | null;
}
interface ServiceNode {
id: number;
service_id: number;
address: string;
protocol: string;
port: number | null;
enabled: boolean;
priority: number;
weight: number;
health_status: NodeHealthState;
health_check_id: number | null;
consecutive_failures: number;
consecutive_successes: number;
last_check_at: string | null;
last_failure_reason: string | null;
created_at: string;
updated_at: string;
}
interface OriginHealthCheck {
id: number;
provider: HealthCheckProvider;
cf_healthcheck_id: string | null;
cf_zone_id: string | null;
name: string;
protocol: string;
path: string | null;
method: string | null;
timeout: number;
interval_sec: number;
retries: number;
expected_status: number | null;
consecutive_fails: number;
consecutive_successes: number;
suspended: boolean;
created_at: string;
updated_at: string;
}
interface ServiceOverview {
service: ServiceView$1;
nodes: ServiceNode[];
health_check: OriginHealthCheck | null;
routing_strategy: LbMode;
active_addresses: string[];
}
interface PatchDnsRecordPayload {
type?: string;
name?: string;
content?: string;
ttl?: number;
proxied?: boolean;
priority?: number;
}
interface CfHealthCheck {
id: string;
address: string;
name: string;
status?: string;
type?: string;
interval?: number;
timeout?: number;
retries?: number;
consecutive_fails?: number;
consecutive_successes?: number;
suspended?: boolean;
}
interface HealthCheckTarget {
scope: HealthCheckScope;
ref_id: number;
@@ -250,6 +337,18 @@ declare const ipHealthStateSchema: z.ZodEnum<{
down: "down";
degraded: "degraded";
}>;
declare const nodeHealthStateSchema: z.ZodEnum<{
unknown: "unknown";
degraded: "degraded";
checking: "checking";
healthy: "healthy";
unhealthy: "unhealthy";
disabled: "disabled";
}>;
declare const healthCheckProviderSchema: z.ZodEnum<{
local: "local";
cloudflare: "cloudflare";
}>;
declare const healthCheckScopeSchema: z.ZodEnum<{
binding: "binding";
group: "group";
@@ -269,6 +368,7 @@ declare const ipHealthStatusSchema: z.ZodObject<{
}>;
latency_ms: z.ZodNullable<z.ZodNumber>;
consecutive_failures: z.ZodNumber;
consecutive_successes: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
last_checked_at: z.ZodNullable<z.ZodString>;
last_error: z.ZodNullable<z.ZodString>;
}, z.core.$strip>;
@@ -1414,6 +1514,124 @@ type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>;
type CreateDomainInput = z.infer<typeof createDomainSchema>;
type LoginInput = z.infer<typeof loginSchema>;
type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>;
declare const serviceNodeSchema: z.ZodObject<{
id: z.ZodNumber;
service_id: z.ZodNumber;
address: z.ZodString;
protocol: z.ZodString;
port: z.ZodNullable<z.ZodNumber>;
enabled: z.ZodCoercedBoolean<unknown>;
priority: z.ZodNumber;
weight: z.ZodNumber;
health_status: z.ZodEnum<{
unknown: "unknown";
degraded: "degraded";
checking: "checking";
healthy: "healthy";
unhealthy: "unhealthy";
disabled: "disabled";
}>;
health_check_id: z.ZodNullable<z.ZodNumber>;
consecutive_failures: z.ZodNumber;
consecutive_successes: z.ZodNumber;
last_check_at: z.ZodNullable<z.ZodString>;
last_failure_reason: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
}, z.core.$strip>;
declare const createServiceNodeSchema: z.ZodObject<{
address: z.ZodString;
protocol: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
tcp: "tcp";
http: "http";
https: "https";
}>>>;
port: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
priority: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
weight: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
health_check_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>;
declare const updateServiceNodeSchema: z.ZodObject<{
address: z.ZodOptional<z.ZodString>;
protocol: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodEnum<{
tcp: "tcp";
http: "http";
https: "https";
}>>>>;
port: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodNumber>>>;
enabled: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodBoolean>>>;
priority: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
weight: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
health_check_id: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodNumber>>>;
}, z.core.$strip>;
declare const originHealthCheckSchema: z.ZodObject<{
id: z.ZodNumber;
provider: z.ZodEnum<{
local: "local";
cloudflare: "cloudflare";
}>;
cf_healthcheck_id: z.ZodNullable<z.ZodString>;
cf_zone_id: z.ZodNullable<z.ZodString>;
name: z.ZodString;
protocol: z.ZodString;
path: z.ZodNullable<z.ZodString>;
method: z.ZodNullable<z.ZodString>;
timeout: z.ZodNumber;
interval_sec: z.ZodNumber;
retries: z.ZodNumber;
expected_status: z.ZodNullable<z.ZodNumber>;
consecutive_fails: z.ZodNumber;
consecutive_successes: z.ZodNumber;
suspended: z.ZodCoercedBoolean<unknown>;
created_at: z.ZodString;
updated_at: z.ZodString;
}, z.core.$strip>;
declare const createOriginHealthCheckSchema: z.ZodObject<{
provider: z.ZodEnum<{
local: "local";
cloudflare: "cloudflare";
}>;
name: z.ZodString;
cf_zone_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
protocol: z.ZodOptional<z.ZodEnum<{
tcp: "tcp";
http: "http";
https: "https";
HTTP: "HTTP";
HTTPS: "HTTPS";
TCP: "TCP";
}>>;
path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
method: z.ZodOptional<z.ZodNullable<z.ZodString>>;
timeout: z.ZodOptional<z.ZodNumber>;
interval_sec: z.ZodOptional<z.ZodNumber>;
retries: z.ZodOptional<z.ZodNumber>;
expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
consecutive_fails: z.ZodOptional<z.ZodNumber>;
consecutive_successes: z.ZodOptional<z.ZodNumber>;
suspended: z.ZodOptional<z.ZodBoolean>;
node_id: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>;
declare const changeIpSchema: z.ZodObject<{
from_ip: z.ZodOptional<z.ZodString>;
to_ip: z.ZodOptional<z.ZodString>;
node_id: z.ZodOptional<z.ZodNumber>;
dry_run: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, z.core.$strip>;
declare const changeDomainSchema: z.ZodObject<{
from_domain_id: z.ZodNumber;
to_domain_id: z.ZodNumber;
hostnames: z.ZodOptional<z.ZodArray<z.ZodString>>;
dry_run: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, z.core.$strip>;
type ServiceNodeRecord = z.infer<typeof serviceNodeSchema>;
type CreateServiceNodeInput = z.infer<typeof createServiceNodeSchema>;
type UpdateServiceNodeInput = z.infer<typeof updateServiceNodeSchema>;
type OriginHealthCheckRecord = z.infer<typeof originHealthCheckSchema>;
type CreateOriginHealthCheckInput = z.infer<typeof createOriginHealthCheckSchema>;
type ChangeIpInput = z.infer<typeof changeIpSchema>;
type ChangeDomainInput = z.infer<typeof changeDomainSchema>;
declare const appSwitcherIconSchema: z.ZodEnum<{
server: "server";
@@ -1617,4 +1835,4 @@ declare const ingestAuditEventSchema: z.ZodObject<{
}, z.core.$strip>;
type IngestAuditEvent = z.infer<typeof ingestAuditEventSchema>;
export { AUDIT_SEVERITIES, AUDIT_SOURCE_APPS, AUDIT_TARGET_TYPES, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type AuditListQuery, type AuditLogEntry, type AuditSeverity, type AuditSourceApp, type AuditTargetType, type BulkUpdateDomainsInput, CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfZone, type CfdmBindingSyncItem, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateDomainMonitorInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainEnvironment, type DomainListItem, type DomainMonitor, type DomainMonitorResult, type DomainMonitorType, type Group, type GroupWithStats, type HealthCheckConfig, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthStatusQuery, type IngestAuditEvent, type IpHealthState, type IpHealthStatus, type JwtClaims, type LbMode, type LoginInput, type LoginRequest, type LoginResponse, type NotificationLog, type ParsedFqdn, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckConfigSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isIpLiteral, isValidIpv4, lbModeSchema, loginSchema, normalizeDnsRecordName, notificationLogSchema, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema };
export { AUDIT_SEVERITIES, AUDIT_SOURCE_APPS, AUDIT_TARGET_TYPES, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type AuditListQuery, type AuditLogEntry, type AuditSeverity, type AuditSourceApp, type AuditTargetType, type BulkUpdateDomainsInput, CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfHealthCheck, type CfZone, type CfdmBindingSyncItem, type ChangeDomainInput, type ChangeIpInput, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateDomainMonitorInput, type CreateGroupInput, type CreateOriginHealthCheckInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceNodeInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainEnvironment, type DomainListItem, type DomainMonitor, type DomainMonitorResult, type DomainMonitorType, type Group, type GroupWithStats, type HealthCheckConfig, type HealthCheckProvider, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthStatusQuery, type IngestAuditEvent, type IpHealthState, type IpHealthStatus, type JwtClaims, type LbMode, type LoginInput, type LoginRequest, type LoginResponse, type NodeHealthState, type NotificationLog, type OriginHealthCheck, type OriginHealthCheckRecord, type ParsedFqdn, type PatchDnsRecordPayload, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceNode, type ServiceNodeRecord, type ServiceOverview, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateServiceNodeInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, changeDomainSchema, changeIpSchema, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createOriginHealthCheckSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceNodeSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckConfigSchema, healthCheckProviderSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isIpLiteral, isValidIpv4, lbModeSchema, loginSchema, nodeHealthStateSchema, normalizeDnsRecordName, notificationLogSchema, originHealthCheckSchema, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceNodeSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateServiceNodeSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema };
+95
View File
@@ -171,6 +171,15 @@ var healthCheckTypeSchema = z.enum(["tcp", "http", "ping", "dns"]);
var domainEnvironmentSchema = z.enum(["prod", "staging", "dev"]);
var domainMonitorTypeSchema = z.enum(["http", "ping", "dns"]);
var ipHealthStateSchema = z.enum(["up", "down", "degraded", "unknown"]);
var nodeHealthStateSchema = z.enum([
"unknown",
"checking",
"healthy",
"degraded",
"unhealthy",
"disabled"
]);
var healthCheckProviderSchema = z.enum(["local", "cloudflare"]);
var healthCheckScopeSchema = z.enum(["binding", "group"]);
var ipHealthStatusSchema = z.object({
scope: healthCheckScopeSchema,
@@ -179,6 +188,7 @@ var ipHealthStatusSchema = z.object({
status: ipHealthStateSchema,
latency_ms: z.number().nullable(),
consecutive_failures: z.number(),
consecutive_successes: z.number().optional().default(0),
last_checked_at: z.string().nullable(),
last_error: z.string().nullable()
});
@@ -366,6 +376,7 @@ var ipv4Schema = z.string().regex(
/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
"\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IPv4"
);
var nodeAddressSchema = z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0438\u043B\u0438 hostname").max(255);
var healthCheckConfigFields = {
health_check_enabled: z.boolean().optional(),
health_check_type: healthCheckTypeSchema.optional(),
@@ -549,6 +560,81 @@ var healthStatusQuerySchema = z.object({
scope: healthCheckScopeSchema,
ref_id: z.coerce.number().int().positive()
});
var serviceNodeSchema = z.object({
id: z.number(),
service_id: z.number(),
address: z.string(),
protocol: z.string(),
port: z.number().nullable(),
enabled: z.coerce.boolean(),
priority: z.number(),
weight: z.number(),
health_status: nodeHealthStateSchema,
health_check_id: z.number().nullable(),
consecutive_failures: z.number(),
consecutive_successes: z.number(),
last_check_at: z.string().nullable(),
last_failure_reason: z.string().nullable(),
created_at: z.string(),
updated_at: z.string()
});
var createServiceNodeSchema = z.object({
address: nodeAddressSchema,
protocol: z.enum(["tcp", "http", "https"]).optional().default("tcp"),
port: z.number().int().min(1).max(65535).nullable().optional(),
enabled: z.boolean().optional().default(true),
priority: z.number().int().min(1).max(100).optional().default(1),
weight: z.number().int().min(1).max(100).optional().default(1),
health_check_id: z.number().int().positive().nullable().optional()
});
var updateServiceNodeSchema = createServiceNodeSchema.partial();
var originHealthCheckSchema = z.object({
id: z.number(),
provider: healthCheckProviderSchema,
cf_healthcheck_id: z.string().nullable(),
cf_zone_id: z.string().nullable(),
name: z.string(),
protocol: z.string(),
path: z.string().nullable(),
method: z.string().nullable(),
timeout: z.number(),
interval_sec: z.number(),
retries: z.number(),
expected_status: z.number().nullable(),
consecutive_fails: z.number(),
consecutive_successes: z.number(),
suspended: z.coerce.boolean(),
created_at: z.string(),
updated_at: z.string()
});
var createOriginHealthCheckSchema = z.object({
provider: healthCheckProviderSchema,
name: z.string().min(1).max(64),
cf_zone_id: z.string().nullable().optional(),
protocol: z.enum(["HTTP", "HTTPS", "TCP", "tcp", "http", "https"]).optional(),
path: z.string().nullable().optional(),
method: z.string().nullable().optional(),
timeout: z.number().int().min(1).max(60).optional(),
interval_sec: z.number().int().min(5).max(3600).optional(),
retries: z.number().int().min(0).max(10).optional(),
expected_status: z.number().int().min(100).max(599).nullable().optional(),
consecutive_fails: z.number().int().min(1).max(20).optional(),
consecutive_successes: z.number().int().min(1).max(20).optional(),
suspended: z.boolean().optional(),
node_id: z.number().int().positive().optional()
});
var changeIpSchema = z.object({
from_ip: z.string().optional(),
to_ip: z.string().min(1).optional(),
node_id: z.number().int().positive().optional(),
dry_run: z.boolean().optional().default(false)
});
var changeDomainSchema = z.object({
from_domain_id: z.number().int().positive(),
to_domain_id: z.number().int().positive(),
hostnames: z.array(z.string().min(1)).optional(),
dry_run: z.boolean().optional().default(false)
});
// src/app-switcher.ts
import { z as z2 } from "zod";
@@ -708,12 +794,16 @@ export {
certificateSchema,
cfdmBindingSyncItemSchema,
cfdmSyncBindingsBodySchema,
changeDomainSchema,
changeIpSchema,
createDnsRecordSchema,
createDomainMonitorSchema,
createDomainSchema,
createGroupSchema,
createOriginHealthCheckSchema,
createServiceBindingSchema,
createServiceGroupSchema,
createServiceNodeSchema,
createServiceSchema,
createServiceWithConfigSchema,
createSubdomainSchema,
@@ -730,6 +820,7 @@ export {
groupSchema,
groupWithStatsSchema,
healthCheckConfigSchema,
healthCheckProviderSchema,
healthCheckScopeSchema,
healthCheckTypeSchema,
healthStatusQuerySchema,
@@ -740,8 +831,10 @@ export {
isValidIpv4,
lbModeSchema,
loginSchema,
nodeHealthStateSchema,
normalizeDnsRecordName,
notificationLogSchema,
originHealthCheckSchema,
parseFqdn,
reorderServicesSchema,
serviceBindingSchema,
@@ -750,6 +843,7 @@ export {
serviceGroupTypeSchema,
serviceGroupViewSchema,
serviceGroupsResponseSchema,
serviceNodeSchema,
serviceSchema,
serviceViewSchema,
shouldMonitorService,
@@ -760,6 +854,7 @@ export {
updateDomainSchema,
updateServiceConfigSchema,
updateServiceGroupSchema,
updateServiceNodeSchema,
updateSubdomainSchema,
validateDnsRecord,
vpsTrackerEventSchema
+7
View File
@@ -21,7 +21,14 @@ export type {
LbMode,
HealthCheckType,
IpHealthState,
NodeHealthState,
HealthCheckProvider,
HealthCheckScope,
IpHealthStatus,
HealthCheckTarget,
ServiceNode,
OriginHealthCheck,
ServiceOverview,
PatchDnsRecordPayload,
CfHealthCheck,
} from "./types.js";
+109
View File
@@ -19,6 +19,19 @@ export type DomainMonitorType = z.infer<typeof domainMonitorTypeSchema>
export const ipHealthStateSchema = z.enum(['up', 'down', 'degraded', 'unknown'])
export type IpHealthState = z.infer<typeof ipHealthStateSchema>
export const nodeHealthStateSchema = z.enum([
'unknown',
'checking',
'healthy',
'degraded',
'unhealthy',
'disabled',
])
export type NodeHealthState = z.infer<typeof nodeHealthStateSchema>
export const healthCheckProviderSchema = z.enum(['local', 'cloudflare'])
export type HealthCheckProvider = z.infer<typeof healthCheckProviderSchema>
export const healthCheckScopeSchema = z.enum(['binding', 'group'])
export type HealthCheckScope = z.infer<typeof healthCheckScopeSchema>
@@ -29,6 +42,7 @@ export const ipHealthStatusSchema = z.object({
status: ipHealthStateSchema,
latency_ms: z.number().nullable(),
consecutive_failures: z.number(),
consecutive_successes: z.number().optional().default(0),
last_checked_at: z.string().nullable(),
last_error: z.string().nullable(),
})
@@ -266,6 +280,11 @@ const ipv4Schema = z
'Некорректный IPv4',
)
const nodeAddressSchema = z
.string()
.min(1, 'Укажите IP или hostname')
.max(255)
const healthCheckConfigFields = {
health_check_enabled: z.boolean().optional(),
health_check_type: healthCheckTypeSchema.optional(),
@@ -512,3 +531,93 @@ export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchem
export type CreateDomainInput = z.infer<typeof createDomainSchema>
export type LoginInput = z.infer<typeof loginSchema>
export type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>
export const serviceNodeSchema = z.object({
id: z.number(),
service_id: z.number(),
address: z.string(),
protocol: z.string(),
port: z.number().nullable(),
enabled: z.coerce.boolean(),
priority: z.number(),
weight: z.number(),
health_status: nodeHealthStateSchema,
health_check_id: z.number().nullable(),
consecutive_failures: z.number(),
consecutive_successes: z.number(),
last_check_at: z.string().nullable(),
last_failure_reason: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
export const createServiceNodeSchema = z.object({
address: nodeAddressSchema,
protocol: z.enum(['tcp', 'http', 'https']).optional().default('tcp'),
port: z.number().int().min(1).max(65535).nullable().optional(),
enabled: z.boolean().optional().default(true),
priority: z.number().int().min(1).max(100).optional().default(1),
weight: z.number().int().min(1).max(100).optional().default(1),
health_check_id: z.number().int().positive().nullable().optional(),
})
export const updateServiceNodeSchema = createServiceNodeSchema.partial()
export const originHealthCheckSchema = z.object({
id: z.number(),
provider: healthCheckProviderSchema,
cf_healthcheck_id: z.string().nullable(),
cf_zone_id: z.string().nullable(),
name: z.string(),
protocol: z.string(),
path: z.string().nullable(),
method: z.string().nullable(),
timeout: z.number(),
interval_sec: z.number(),
retries: z.number(),
expected_status: z.number().nullable(),
consecutive_fails: z.number(),
consecutive_successes: z.number(),
suspended: z.coerce.boolean(),
created_at: z.string(),
updated_at: z.string(),
})
export const createOriginHealthCheckSchema = z.object({
provider: healthCheckProviderSchema,
name: z.string().min(1).max(64),
cf_zone_id: z.string().nullable().optional(),
protocol: z.enum(['HTTP', 'HTTPS', 'TCP', 'tcp', 'http', 'https']).optional(),
path: z.string().nullable().optional(),
method: z.string().nullable().optional(),
timeout: z.number().int().min(1).max(60).optional(),
interval_sec: z.number().int().min(5).max(3600).optional(),
retries: z.number().int().min(0).max(10).optional(),
expected_status: z.number().int().min(100).max(599).nullable().optional(),
consecutive_fails: z.number().int().min(1).max(20).optional(),
consecutive_successes: z.number().int().min(1).max(20).optional(),
suspended: z.boolean().optional(),
node_id: z.number().int().positive().optional(),
})
export const changeIpSchema = z.object({
from_ip: z.string().optional(),
to_ip: z.string().min(1).optional(),
node_id: z.number().int().positive().optional(),
dry_run: z.boolean().optional().default(false),
})
export const changeDomainSchema = z.object({
from_domain_id: z.number().int().positive(),
to_domain_id: z.number().int().positive(),
hostnames: z.array(z.string().min(1)).optional(),
dry_run: z.boolean().optional().default(false),
})
export type ServiceNodeRecord = z.infer<typeof serviceNodeSchema>
export type CreateServiceNodeInput = z.infer<typeof createServiceNodeSchema>
export type UpdateServiceNodeInput = z.infer<typeof updateServiceNodeSchema>
export type OriginHealthCheckRecord = z.infer<typeof originHealthCheckSchema>
export type CreateOriginHealthCheckInput = z.infer<typeof createOriginHealthCheckSchema>
export type ChangeIpInput = z.infer<typeof changeIpSchema>
export type ChangeDomainInput = z.infer<typeof changeDomainSchema>
+83
View File
@@ -121,6 +121,8 @@ export interface ServiceBinding {
health_check_interval_sec: number;
health_check_timeout_ms: number;
health_check_verify_tls: boolean;
routing_strategy: LbMode;
operation_version: number;
created_at: string;
updated_at: string;
}
@@ -267,6 +269,16 @@ export type DomainMonitorType = "http" | "ping" | "dns";
export type IpHealthState = "up" | "down" | "degraded" | "unknown";
export type NodeHealthState =
| "unknown"
| "checking"
| "healthy"
| "degraded"
| "unhealthy"
| "disabled";
export type HealthCheckProvider = "local" | "cloudflare";
export type HealthCheckScope = "binding" | "group";
export interface IpHealthStatus {
@@ -276,10 +288,81 @@ export interface IpHealthStatus {
status: IpHealthState;
latency_ms: number | null;
consecutive_failures: number;
consecutive_successes?: number;
last_checked_at: string | null;
last_error: string | null;
}
export interface ServiceNode {
id: number;
service_id: number;
address: string;
protocol: string;
port: number | null;
enabled: boolean;
priority: number;
weight: number;
health_status: NodeHealthState;
health_check_id: number | null;
consecutive_failures: number;
consecutive_successes: number;
last_check_at: string | null;
last_failure_reason: string | null;
created_at: string;
updated_at: string;
}
export interface OriginHealthCheck {
id: number;
provider: HealthCheckProvider;
cf_healthcheck_id: string | null;
cf_zone_id: string | null;
name: string;
protocol: string;
path: string | null;
method: string | null;
timeout: number;
interval_sec: number;
retries: number;
expected_status: number | null;
consecutive_fails: number;
consecutive_successes: number;
suspended: boolean;
created_at: string;
updated_at: string;
}
export interface ServiceOverview {
service: ServiceView;
nodes: ServiceNode[];
health_check: OriginHealthCheck | null;
routing_strategy: LbMode;
active_addresses: string[];
}
export interface PatchDnsRecordPayload {
type?: string;
name?: string;
content?: string;
ttl?: number;
proxied?: boolean;
priority?: number;
}
export interface CfHealthCheck {
id: string;
address: string;
name: string;
status?: string;
type?: string;
interval?: number;
timeout?: number;
retries?: number;
consecutive_fails?: number;
consecutive_successes?: number;
suspended?: boolean;
}
export interface HealthCheckTarget {
scope: HealthCheckScope;
ref_id: number;