feat(certificates): enhance service certificate management and monitoring
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 8s
quality / changes (push) Successful in 10s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m8s
quality / api (push) Successful in 1m9s
CD / quality (push) Successful in 2m31s
CD / publish (push) Successful in 1m35s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 8s
quality / changes (push) Successful in 10s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m8s
quality / api (push) Successful in 1m9s
CD / quality (push) Successful in 2m31s
CD / publish (push) Successful in 1m35s
- Added new endpoints for listing and checking service certificates, improving visibility into SSL status. - Integrated certificate monitoring options into service binding updates, allowing for flexible SSL management. - Updated the service detail grid to include SSL monitoring controls, enhancing user interaction with certificate settings. - Refactored related components and schemas to support the new certificate features, ensuring consistency across the application. - Improved test coverage for certificate functionalities, validating the new features and ensuring reliability.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
ALTER TABLE service_bindings ADD COLUMN cert_monitoring TEXT NOT NULL DEFAULT 'auto'
|
||||
CHECK (cert_monitoring IN ('auto', 'required', 'skipped'));
|
||||
|
||||
ALTER TABLE certificates ADD COLUMN service_id INTEGER REFERENCES services(id) ON DELETE SET NULL;
|
||||
|
||||
UPDATE service_bindings
|
||||
SET cert_monitoring = COALESCE(
|
||||
(
|
||||
SELECT d.cert_monitoring FROM domains d
|
||||
WHERE d.id = service_bindings.domain_id
|
||||
),
|
||||
'auto'
|
||||
)
|
||||
WHERE hostname = '@';
|
||||
|
||||
UPDATE service_bindings
|
||||
SET cert_monitoring = COALESCE(
|
||||
(
|
||||
SELECT s.cert_monitoring FROM subdomains s
|
||||
WHERE s.domain_id = service_bindings.domain_id
|
||||
AND s.name = service_bindings.hostname
|
||||
),
|
||||
'auto'
|
||||
)
|
||||
WHERE hostname != '@';
|
||||
|
||||
UPDATE certificates
|
||||
SET service_id = (
|
||||
SELECT sb.service_id
|
||||
FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
WHERE CASE
|
||||
WHEN sb.hostname = '@' THEN d.zone_name
|
||||
ELSE sb.hostname || '.' || d.zone_name
|
||||
END = certificates.hostname
|
||||
LIMIT 1
|
||||
);
|
||||
+67
-16
@@ -851,6 +851,7 @@ function mapServiceBinding(
|
||||
health_check_timeout_ms: row.health_check_timeout_ms,
|
||||
health_check_verify_tls: row.health_check_verify_tls,
|
||||
...mapHealthFields(row),
|
||||
cert_monitoring: row.cert_monitoring ?? "auto",
|
||||
routing_strategy: row.routing_strategy as LbMode,
|
||||
operation_version: row.operation_version,
|
||||
created_at: row.created_at,
|
||||
@@ -1530,6 +1531,7 @@ export interface BindingLbPatch {
|
||||
health_check_provider?: HealthCheckProvider;
|
||||
health_check_providers?: HealthCheckProvider[];
|
||||
health_check_aggregate?: HealthCheckAggregate;
|
||||
cert_monitoring?: string;
|
||||
}
|
||||
|
||||
export function updateBindingLbConfig(
|
||||
@@ -1560,6 +1562,8 @@ export function updateBindingLbConfig(
|
||||
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;
|
||||
if (patch.cert_monitoring !== undefined)
|
||||
update.cert_monitoring = patch.cert_monitoring;
|
||||
Object.assign(update, healthProviderColumns(patch));
|
||||
db.update(serviceBindings)
|
||||
.set(update)
|
||||
@@ -1669,7 +1673,7 @@ const SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.h
|
||||
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.health_check_verify_tls, sb.health_check_provider,
|
||||
sb.health_check_providers, sb.health_check_aggregate, sb.cname_target,
|
||||
sb.health_check_providers, sb.health_check_aggregate, sb.cert_monitoring, 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,
|
||||
@@ -1733,6 +1737,7 @@ function enrichServiceBindingView(
|
||||
return {
|
||||
...row,
|
||||
cname_target: row.cname_target ?? null,
|
||||
cert_monitoring: row.cert_monitoring ?? "auto",
|
||||
...mapHealthFields(row),
|
||||
target_ips,
|
||||
target_ip: target_ips[0] ?? null,
|
||||
@@ -1914,26 +1919,69 @@ export function deleteBinding(db: Db, id: number): void {
|
||||
|
||||
// --- Certificates ---
|
||||
|
||||
const CERTIFICATE_SELECT = `c.id, c.domain_id, c.subdomain_id, c.service_id, c.hostname,
|
||||
c.expires_at, c.last_checked_at, c.last_error, c.status, c.created_at, c.updated_at,
|
||||
s.name AS service_name`;
|
||||
|
||||
type CertificateRow = {
|
||||
id: number;
|
||||
domain_id: number;
|
||||
subdomain_id: number | null;
|
||||
service_id: number | null;
|
||||
hostname: string;
|
||||
expires_at: string | null;
|
||||
last_checked_at: string | null;
|
||||
last_error: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
service_name: string | null;
|
||||
};
|
||||
|
||||
function mapCertificate(row: CertificateRow): Certificate {
|
||||
return {
|
||||
id: row.id,
|
||||
domain_id: row.domain_id,
|
||||
subdomain_id: row.subdomain_id,
|
||||
service_id: row.service_id ?? null,
|
||||
service_name: row.service_name ?? null,
|
||||
hostname: row.hostname,
|
||||
expires_at: row.expires_at,
|
||||
last_checked_at: row.last_checked_at,
|
||||
last_error: row.last_error,
|
||||
status: row.status,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export function listCertificates(db: Db, status?: string): Certificate[] {
|
||||
if (status) {
|
||||
return db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(eq(certificates.status, status))
|
||||
.orderBy(asc(certificates.expires_at))
|
||||
.all() as Certificate[];
|
||||
}
|
||||
return db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.orderBy(asc(certificates.expires_at))
|
||||
.all() as Certificate[];
|
||||
const rows = status
|
||||
? db.all<CertificateRow>(sql`
|
||||
SELECT ${sql.raw(CERTIFICATE_SELECT)}
|
||||
FROM certificates c
|
||||
LEFT JOIN services s ON s.id = c.service_id
|
||||
WHERE c.status = ${status}
|
||||
ORDER BY c.expires_at ASC
|
||||
`)
|
||||
: db.all<CertificateRow>(sql`
|
||||
SELECT ${sql.raw(CERTIFICATE_SELECT)}
|
||||
FROM certificates c
|
||||
LEFT JOIN services s ON s.id = c.service_id
|
||||
ORDER BY c.expires_at ASC
|
||||
`);
|
||||
return rows.map(mapCertificate);
|
||||
}
|
||||
|
||||
export function getCertificate(db: Db, id: number): Certificate {
|
||||
const row = db.select().from(certificates).where(eq(certificates.id, id)).get();
|
||||
const row = db.all<CertificateRow>(sql`
|
||||
SELECT ${sql.raw(CERTIFICATE_SELECT)}
|
||||
FROM certificates c
|
||||
LEFT JOIN services s ON s.id = c.service_id
|
||||
WHERE c.id = ${id}
|
||||
`)[0];
|
||||
if (!row) throw new NotFoundError(`certificate ${id}`);
|
||||
return row as Certificate;
|
||||
return mapCertificate(row);
|
||||
}
|
||||
|
||||
export function upsertCertificateCheck(
|
||||
@@ -1944,6 +1992,7 @@ export function upsertCertificateCheck(
|
||||
expiresAt: string | null,
|
||||
status: string,
|
||||
lastError: string | null,
|
||||
serviceId?: number | null,
|
||||
): Certificate {
|
||||
const existing = db
|
||||
.select()
|
||||
@@ -1956,6 +2005,7 @@ export function upsertCertificateCheck(
|
||||
.set({
|
||||
domain_id: domainId,
|
||||
subdomain_id: subdomainId,
|
||||
service_id: serviceId === undefined ? existing.service_id : serviceId,
|
||||
expires_at: expiresAt,
|
||||
last_checked_at: sql`datetime('now')`,
|
||||
last_error: lastError,
|
||||
@@ -1972,6 +2022,7 @@ export function upsertCertificateCheck(
|
||||
.values({
|
||||
domain_id: domainId,
|
||||
subdomain_id: subdomainId,
|
||||
service_id: serviceId ?? null,
|
||||
hostname,
|
||||
expires_at: expiresAt,
|
||||
last_checked_at: sql`datetime('now')`,
|
||||
|
||||
@@ -177,6 +177,7 @@ export const serviceBindings = sqliteTable(
|
||||
health_check_aggregate: text("health_check_aggregate")
|
||||
.notNull()
|
||||
.default("majority"),
|
||||
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
|
||||
routing_strategy: text("routing_strategy").notNull().default("round_robin"),
|
||||
operation_version: integer("operation_version").notNull().default(0),
|
||||
created_at: text("created_at")
|
||||
@@ -324,6 +325,9 @@ export const certificates = sqliteTable("certificates", {
|
||||
subdomain_id: integer("subdomain_id").references(() => subdomains.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
service_id: integer("service_id").references(() => services.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
hostname: text("hostname").notNull().unique(),
|
||||
expires_at: text("expires_at"),
|
||||
last_checked_at: text("last_checked_at"),
|
||||
|
||||
@@ -178,6 +178,7 @@ export const serviceDomainBindingSchema = z
|
||||
health_check_provider: healthCheckProviderSchema.catch('local'),
|
||||
health_check_providers: healthCheckProvidersSchema.catch(['local']),
|
||||
health_check_aggregate: healthCheckAggregateSchema.catch('majority'),
|
||||
cert_monitoring: certMonitoringSchema.default('auto'),
|
||||
sync_status: z.string().nullable().default(null),
|
||||
})
|
||||
.transform((binding) => ({
|
||||
@@ -266,6 +267,7 @@ export const serviceBindingSchema = z
|
||||
health_check_interval_sec: z.number().default(30),
|
||||
health_check_timeout_ms: z.number().default(3000),
|
||||
health_check_verify_tls: z.coerce.boolean().default(false),
|
||||
cert_monitoring: certMonitoringSchema.default('auto'),
|
||||
sync_status: z.string().nullable().default(null),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
@@ -303,6 +305,8 @@ export const certificateSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
subdomain_id: z.number().nullable(),
|
||||
service_id: z.number().nullable().optional().default(null),
|
||||
service_name: z.string().nullable().optional().default(null),
|
||||
hostname: z.string(),
|
||||
expires_at: z.string().nullable(),
|
||||
last_checked_at: z.string().nullable(),
|
||||
@@ -312,6 +316,19 @@ export const certificateSchema = z.object({
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const serviceCertificateRowSchema = z.object({
|
||||
binding_id: z.number(),
|
||||
domain_id: z.number(),
|
||||
service_id: z.number(),
|
||||
hostname: z.string(),
|
||||
cert_monitoring: certMonitoringSchema,
|
||||
id: z.number().nullable(),
|
||||
status: z.string(),
|
||||
expires_at: z.string().nullable(),
|
||||
last_checked_at: z.string().nullable(),
|
||||
last_error: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type Group = z.infer<typeof groupSchema>
|
||||
export type GroupWithStats = z.infer<typeof groupWithStatsSchema>
|
||||
export type Service = z.infer<typeof serviceSchema>
|
||||
@@ -324,6 +341,7 @@ export type Domain = z.infer<typeof domainSchema>
|
||||
export type DomainListItem = z.infer<typeof domainListItemSchema>
|
||||
export type DnsRecord = z.infer<typeof dnsRecordSchema>
|
||||
export type Certificate = z.infer<typeof certificateSchema>
|
||||
export type ServiceCertificateRow = z.infer<typeof serviceCertificateRowSchema>
|
||||
|
||||
export const createGroupSchema = z.object({
|
||||
name: z.string().min(1, 'Укажите название'),
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface Certificate {
|
||||
id: number;
|
||||
domain_id: number;
|
||||
subdomain_id: number | null;
|
||||
service_id: number | null;
|
||||
service_name: string | null;
|
||||
hostname: string;
|
||||
expires_at: string | null;
|
||||
last_checked_at: string | null;
|
||||
@@ -139,6 +141,7 @@ export interface ServiceBinding {
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
cert_monitoring: string;
|
||||
routing_strategy: LbMode;
|
||||
operation_version: number;
|
||||
created_at: string;
|
||||
@@ -173,6 +176,7 @@ export interface ServiceBindingView {
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
cert_monitoring: string;
|
||||
sync_status: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -201,6 +205,7 @@ export interface ServiceDomainBindingView {
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
cert_monitoring: string;
|
||||
sync_status: string | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user