feat(health-check): add TLS verification option to health check configuration
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m59s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 7s

Introduced a new `verify_tls` boolean option in health check configurations across various services, allowing users to specify whether to validate TLS certificates during health checks. Updated related components and services to accommodate this new option, ensuring proper handling in both the backend and frontend. Enhanced tests to validate the new functionality and ensure correct behavior with different configurations.
This commit is contained in:
Denozordec
2026-07-20 17:28:02 +07:00
parent 9783974949
commit 1d497d01c2
123 changed files with 351 additions and 15912 deletions
+72 -5
View File
File diff suppressed because one or more lines are too long
+26 -52
View File
@@ -50,6 +50,7 @@ var serviceGroups = sqliteTable("service_groups", {
health_check_expected_status: integer("health_check_expected_status"),
health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30),
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
health_check_verify_tls: integer("health_check_verify_tls", { 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')`)
});
@@ -110,6 +111,7 @@ var serviceBindings = sqliteTable("service_bindings", {
health_check_expected_status: integer("health_check_expected_status"),
health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30),
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
health_check_verify_tls: integer("health_check_verify_tls", { 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')`)
});
@@ -188,6 +190,7 @@ var ipHealthStatus = sqliteTable(
var appSettings = sqliteTable("app_settings", {
id: text("id").primaryKey(),
app_switcher_json: text("app_switcher_json"),
// deprecated: portal is SoT; column kept for migrate compat
vps_tracker_url: text("vps_tracker_url"),
vps_tracker_integration_token: text("vps_tracker_integration_token"),
vps_tracker_sync_enabled: integer("vps_tracker_sync_enabled", {
@@ -328,49 +331,10 @@ var ConflictError = class extends Error {
// src/settings-repo.ts
import { eq } from "drizzle-orm";
import { appSwitcherConfigSchema } from "@cfdm/shared";
var SETTINGS_ID = "settings-main";
var DEFAULT_APP_SWITCHER = {
menuLabel: "\u041F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F",
apps: [
{
id: "vps-tracker",
name: "VPS Tracker",
subtitle: "\u0423\u0447\u0451\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043B\u044C\u043D\u044B\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u0432",
url: "http://192.168.100.67:3001",
icon: "server",
shortcut: "\u23181"
},
{
id: "cfdm",
name: "CF Domain Manager",
subtitle: "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E\u043C\u0435\u043D\u0430\u043C\u0438",
url: "http://192.168.100.67:6363",
icon: "cloud",
shortcut: "\u23182"
},
{
id: "evobgp",
name: "EvoBGP",
subtitle: "BGP \u043C\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044F",
url: "http://192.168.100.67:3000",
icon: "globe",
shortcut: "\u23183"
}
]
};
function parseAppSwitcher(raw) {
if (!raw?.trim()) return DEFAULT_APP_SWITCHER;
try {
return appSwitcherConfigSchema.parse(JSON.parse(raw));
} catch {
return DEFAULT_APP_SWITCHER;
}
}
function toDto(row) {
return {
id: row.id,
appSwitcher: parseAppSwitcher(row.app_switcher_json),
vpsTrackerUrl: row.vps_tracker_url?.trim() ?? "",
vpsTrackerIntegrationTokenSet: Boolean(
row.vps_tracker_integration_token?.trim()
@@ -405,7 +369,7 @@ function updateAppSettings(db, patch) {
}
const current = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
db.update(appSettings).set({
app_switcher_json: patch.appSwitcher !== void 0 ? JSON.stringify(patch.appSwitcher) : current.app_switcher_json,
// app_switcher_json: deprecated — source of truth is auth-portal
vps_tracker_url: patch.vpsTrackerUrl !== void 0 ? patch.vpsTrackerUrl : current.vps_tracker_url,
vps_tracker_integration_token: patch.vpsTrackerIntegrationToken !== void 0 && patch.vpsTrackerIntegrationToken.trim() !== "" ? patch.vpsTrackerIntegrationToken : current.vps_tracker_integration_token,
vps_tracker_sync_enabled: patch.vpsTrackerSyncEnabled !== void 0 ? patch.vpsTrackerSyncEnabled : current.vps_tracker_sync_enabled,
@@ -420,9 +384,6 @@ function touchVpsTrackerSync(db) {
updated_at: (/* @__PURE__ */ new Date()).toISOString()
}).where(eq(appSettings.id, SETTINGS_ID)).run();
}
function getAppSwitcher(db) {
return getAppSettings(db).appSwitcher;
}
// src/repos.ts
var repos_exports = {};
@@ -910,6 +871,7 @@ function mapServiceGroup(row) {
health_check_expected_status: row.health_check_expected_status,
health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
created_at: row.created_at,
updated_at: row.updated_at
};
@@ -935,7 +897,8 @@ function createServiceGroup(db, name, groupType, icon, domain, lbPatch) {
health_check_path: lbPatch?.health_check_path ?? null,
health_check_expected_status: lbPatch?.health_check_expected_status ?? null,
health_check_interval_sec: lbPatch?.health_check_interval_sec ?? 30,
health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3e3
health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3e3,
health_check_verify_tls: lbPatch?.health_check_verify_tls ?? false
}).returning({ id: serviceGroups.id }).get().id;
return getServiceGroup(db, id);
}
@@ -963,6 +926,8 @@ function updateServiceGroup(db, id, name, groupType, icon, domain, lbPatch) {
update.health_check_interval_sec = lbPatch.health_check_interval_sec;
if (lbPatch.health_check_timeout_ms !== void 0)
update.health_check_timeout_ms = lbPatch.health_check_timeout_ms;
if (lbPatch.health_check_verify_tls !== void 0)
update.health_check_verify_tls = lbPatch.health_check_verify_tls;
}
const result = db.update(serviceGroups).set(update).where(eq2(serviceGroups.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
@@ -1033,6 +998,8 @@ function updateBindingLbConfig(db, bindingId, patch) {
update.health_check_interval_sec = patch.health_check_interval_sec;
if (patch.health_check_timeout_ms !== void 0)
update.health_check_timeout_ms = patch.health_check_timeout_ms;
if (patch.health_check_verify_tls !== void 0)
update.health_check_verify_tls = patch.health_check_verify_tls;
db.update(serviceBindings).set(update).where(eq2(serviceBindings.id, bindingId)).run();
}
function setBindingCnameTarget(db, bindingId, target) {
@@ -1091,7 +1058,7 @@ function dnsRecordMatchesHostname(recordName, hostname, zoneName) {
var SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
sb.health_check_timeout_ms, sb.cname_target,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.cname_target,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
dr.content AS target_ip, dr.sync_status,
@@ -1492,7 +1459,8 @@ function listHealthCheckTargets(db) {
sb.health_check_port AS port,
sb.health_check_path AS path,
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
JOIN domains d ON d.id = sb.domain_id
@@ -1505,7 +1473,8 @@ function listHealthCheckTargets(db) {
sg.health_check_port AS port,
sg.health_check_path AS path,
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls
FROM services s
JOIN service_ips sip ON sip.service_id = s.id
JOIN service_groups sg ON sg.id = s.service_group_id
@@ -1521,7 +1490,8 @@ function listHealthCheckTargets(db) {
sg.health_check_port AS port,
sg.health_check_path AS path,
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
JOIN domains d ON d.id = sb.domain_id
@@ -1540,7 +1510,8 @@ function listHealthCheckTargets(db) {
sb.health_check_port AS port,
sb.health_check_path AS path,
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
JOIN services s ON s.id = sb.service_id
@@ -1556,7 +1527,8 @@ function listHealthCheckTargets(db) {
sg.health_check_port AS port,
sg.health_check_path AS path,
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
JOIN services s ON s.id = sb.service_id
@@ -1575,7 +1547,10 @@ function listHealthCheckTargets(db) {
...groupInheritedBindingTargets,
...cnameBindingTargets,
...groupInheritedCnameBindingTargets
];
].map((t) => ({
...t,
verify_tls: Boolean(t.verify_tls)
}));
}
function listDomainTags(db, domainId) {
return db.select({ tag: domainTags.tag }).from(domainTags).where(eq2(domainTags.domain_id, domainId)).all().map((r) => r.tag);
@@ -1700,7 +1675,6 @@ export {
domains,
getAppSettings,
getAppSettingsSecrets,
getAppSwitcher,
groups,
healthCheck,
ipHealthStatus,
@@ -0,0 +1,2 @@
ALTER TABLE service_groups ADD COLUMN health_check_verify_tls INTEGER NOT NULL DEFAULT 0;
ALTER TABLE service_bindings ADD COLUMN health_check_verify_tls INTEGER NOT NULL DEFAULT 0;
+23 -7
View File
@@ -736,6 +736,7 @@ function mapServiceGroup(row: typeof serviceGroups.$inferSelect): ServiceGroup {
health_check_expected_status: row.health_check_expected_status,
health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
created_at: row.created_at,
updated_at: row.updated_at,
};
@@ -769,6 +770,7 @@ export interface ServiceGroupLbPatch {
health_check_expected_status?: number | null;
health_check_interval_sec?: number;
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
}
export function createServiceGroup(
@@ -794,6 +796,7 @@ export function createServiceGroup(
health_check_expected_status: lbPatch?.health_check_expected_status ?? null,
health_check_interval_sec: lbPatch?.health_check_interval_sec ?? 30,
health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3000,
health_check_verify_tls: lbPatch?.health_check_verify_tls ?? false,
})
.returning({ id: serviceGroups.id })
.get()!.id;
@@ -832,6 +835,8 @@ export function updateServiceGroup(
update.health_check_interval_sec = lbPatch.health_check_interval_sec;
if (lbPatch.health_check_timeout_ms !== undefined)
update.health_check_timeout_ms = lbPatch.health_check_timeout_ms;
if (lbPatch.health_check_verify_tls !== undefined)
update.health_check_verify_tls = lbPatch.health_check_verify_tls;
}
const result = db
.update(serviceGroups)
@@ -956,6 +961,7 @@ export interface BindingLbPatch {
health_check_expected_status?: number | null;
health_check_interval_sec?: number;
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
}
export function updateBindingLbConfig(
@@ -981,6 +987,8 @@ export function updateBindingLbConfig(
update.health_check_interval_sec = patch.health_check_interval_sec;
if (patch.health_check_timeout_ms !== undefined)
update.health_check_timeout_ms = patch.health_check_timeout_ms;
if (patch.health_check_verify_tls !== undefined)
update.health_check_verify_tls = patch.health_check_verify_tls;
db.update(serviceBindings)
.set(update)
.where(eq(serviceBindings.id, bindingId))
@@ -1088,7 +1096,7 @@ function dnsRecordMatchesHostname(
const SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
sb.health_check_timeout_ms, sb.cname_target,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.cname_target,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
dr.content AS target_ip, dr.sync_status,
@@ -1734,7 +1742,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sb.health_check_port AS port,
sb.health_check_path AS path,
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
JOIN domains d ON d.id = sb.domain_id
@@ -1748,7 +1757,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sg.health_check_port AS port,
sg.health_check_path AS path,
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls
FROM services s
JOIN service_ips sip ON sip.service_id = s.id
JOIN service_groups sg ON sg.id = s.service_group_id
@@ -1769,7 +1779,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sg.health_check_port AS port,
sg.health_check_path AS path,
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
JOIN domains d ON d.id = sb.domain_id
@@ -1790,7 +1801,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sb.health_check_port AS port,
sb.health_check_path AS path,
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
JOIN services s ON s.id = sb.service_id
@@ -1809,7 +1821,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sg.health_check_port AS port,
sg.health_check_path AS path,
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
JOIN services s ON s.id = sb.service_id
@@ -1829,7 +1842,10 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
...groupInheritedBindingTargets,
...cnameBindingTargets,
...groupInheritedCnameBindingTargets,
];
].map((t) => ({
...t,
verify_tls: Boolean(t.verify_tls),
}));
}
// --- Domain tags ---
+7 -1
View File
@@ -60,6 +60,9 @@ export const serviceGroups = sqliteTable("service_groups", {
health_check_timeout_ms: integer("health_check_timeout_ms")
.notNull()
.default(3000),
health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" })
.notNull()
.default(false),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -154,6 +157,9 @@ export const serviceBindings = sqliteTable("service_bindings", {
health_check_timeout_ms: integer("health_check_timeout_ms")
.notNull()
.default(3000),
health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" })
.notNull()
.default(false),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -271,7 +277,7 @@ export const ipHealthStatus = sqliteTable(
export const appSettings = sqliteTable("app_settings", {
id: text("id").primaryKey(),
app_switcher_json: text("app_switcher_json"),
app_switcher_json: text("app_switcher_json"), // deprecated: portal is SoT; column kept for migrate compat
vps_tracker_url: text("vps_tracker_url"),
vps_tracker_integration_token: text("vps_tracker_integration_token"),
vps_tracker_sync_enabled: integer("vps_tracker_sync_enabled", {
+1 -51
View File
@@ -1,43 +1,11 @@
import { eq } from "drizzle-orm";
import { appSwitcherConfigSchema, type AppSwitcherConfig } from "@cfdm/shared";
import type { Db } from "./client.js";
import { appSettings } from "./schema.js";
const SETTINGS_ID = "settings-main";
const DEFAULT_APP_SWITCHER: AppSwitcherConfig = {
menuLabel: "Приложения",
apps: [
{
id: "vps-tracker",
name: "VPS Tracker",
subtitle: "Учёт виртуальных серверов",
url: "http://192.168.100.67:3001",
icon: "server",
shortcut: "⌘1",
},
{
id: "cfdm",
name: "CF Domain Manager",
subtitle: "Управление доменами",
url: "http://192.168.100.67:6363",
icon: "cloud",
shortcut: "⌘2",
},
{
id: "evobgp",
name: "EvoBGP",
subtitle: "BGP маршрутизация",
url: "http://192.168.100.67:3000",
icon: "globe",
shortcut: "⌘3",
},
],
};
export type AppSettingsDto = {
id: string;
appSwitcher: AppSwitcherConfig;
vpsTrackerUrl: string;
vpsTrackerIntegrationTokenSet: boolean;
vpsTrackerSyncEnabled: boolean;
@@ -46,26 +14,15 @@ export type AppSettingsDto = {
};
export type AppSettingsPatch = {
appSwitcher?: AppSwitcherConfig;
vpsTrackerUrl?: string;
vpsTrackerIntegrationToken?: string;
vpsTrackerSyncEnabled?: boolean;
showQuickActions?: boolean;
};
function parseAppSwitcher(raw: string | null | undefined): AppSwitcherConfig {
if (!raw?.trim()) return DEFAULT_APP_SWITCHER;
try {
return appSwitcherConfigSchema.parse(JSON.parse(raw));
} catch {
return DEFAULT_APP_SWITCHER;
}
}
function toDto(row: typeof appSettings.$inferSelect): AppSettingsDto {
return {
id: row.id,
appSwitcher: parseAppSwitcher(row.app_switcher_json),
vpsTrackerUrl: row.vps_tracker_url?.trim() ?? "",
vpsTrackerIntegrationTokenSet: Boolean(
row.vps_tracker_integration_token?.trim(),
@@ -127,10 +84,7 @@ export function updateAppSettings(db: Db, patch: AppSettingsPatch): AppSettingsD
db.update(appSettings)
.set({
app_switcher_json:
patch.appSwitcher !== undefined
? JSON.stringify(patch.appSwitcher)
: current.app_switcher_json,
// app_switcher_json: deprecated — source of truth is auth-portal
vps_tracker_url:
patch.vpsTrackerUrl !== undefined
? patch.vpsTrackerUrl
@@ -165,7 +119,3 @@ export function touchVpsTrackerSync(db: Db): void {
.where(eq(appSettings.id, SETTINGS_ID))
.run();
}
export function getAppSwitcher(db: Db): AppSwitcherConfig {
return getAppSettings(db).appSwitcher;
}