mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-17 14:34:42 +00:00
Merge branch 'alerting-rules' of https://github.com/fosrl/pangolin into alerting-rules
This commit is contained in:
@@ -194,6 +194,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}).notNull(),
|
||||||
name: varchar("name"),
|
name: varchar("name"),
|
||||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||||
hcPath: varchar("hcPath"),
|
hcPath: varchar("hcPath"),
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}).notNull(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export async function updateProxyResources(
|
|||||||
.insert(targetHealthCheck)
|
.insert(targetHealthCheck)
|
||||||
.values({
|
.values({
|
||||||
name: `${targetData.hostname}:${targetData.port}`,
|
name: `${targetData.hostname}:${targetData.port}`,
|
||||||
|
siteId: site.siteId,
|
||||||
targetId: newTarget.targetId,
|
targetId: newTarget.targetId,
|
||||||
orgId: orgId,
|
orgId: orgId,
|
||||||
hcEnabled: healthcheckData?.enabled || false,
|
hcEnabled: healthcheckData?.enabled || false,
|
||||||
|
|||||||
@@ -13,13 +13,15 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, targetHealthCheck } from "@server/db";
|
import { db, targetHealthCheck, newts, sites } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { addStandaloneHealthCheck } from "@server/routers/newt/targets";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
@@ -27,6 +29,7 @@ const paramsSchema = z.strictObject({
|
|||||||
|
|
||||||
const bodySchema = z.strictObject({
|
const bodySchema = z.strictObject({
|
||||||
name: z.string().nonempty(),
|
name: z.string().nonempty(),
|
||||||
|
siteId: z.number().int().positive(),
|
||||||
hcEnabled: z.boolean().default(false),
|
hcEnabled: z.boolean().default(false),
|
||||||
hcMode: z.string().default("http"),
|
hcMode: z.string().default("http"),
|
||||||
hcHostname: z.string().optional(),
|
hcHostname: z.string().optional(),
|
||||||
@@ -97,6 +100,7 @@ export async function createHealthCheck(
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
siteId,
|
||||||
hcEnabled,
|
hcEnabled,
|
||||||
hcMode,
|
hcMode,
|
||||||
hcHostname,
|
hcHostname,
|
||||||
@@ -120,6 +124,7 @@ export async function createHealthCheck(
|
|||||||
.values({
|
.values({
|
||||||
targetId: null,
|
targetId: null,
|
||||||
orgId,
|
orgId,
|
||||||
|
siteId,
|
||||||
name,
|
name,
|
||||||
hcEnabled,
|
hcEnabled,
|
||||||
hcMode,
|
hcMode,
|
||||||
@@ -140,6 +145,31 @@ export async function createHealthCheck(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
// Push health check to newt if the site is a newt site
|
||||||
|
if (siteId) {
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (site && site.type === "newt") {
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, site.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (newt) {
|
||||||
|
await addStandaloneHealthCheck(
|
||||||
|
newt.newtId,
|
||||||
|
record,
|
||||||
|
newt.version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response<CreateHealthCheckResponse>(res, {
|
return response<CreateHealthCheckResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
targetHealthCheckId: record.targetHealthCheckId
|
targetHealthCheckId: record.targetHealthCheckId
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, targetHealthCheck } from "@server/db";
|
import { db, targetHealthCheck, newts, sites } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -21,6 +21,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { removeStandaloneHealthCheck } from "@server/routers/newt/targets";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -91,6 +92,21 @@ export async function deleteHealthCheck(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Remove health check from newt if the site is a newt site
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, existing.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (newt) {
|
||||||
|
await removeStandaloneHealthCheck(
|
||||||
|
newt.newtId,
|
||||||
|
healthCheckId,
|
||||||
|
newt.version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function getHealthCheckStatusHistory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entityType = "healthCheck";
|
const entityType = "healthCheck";
|
||||||
const entityId = parsedParams.data.healthCheckId
|
const entityId = parsedParams.data.healthCheckId;
|
||||||
const { days } = parsedQuery.data;
|
const { days } = parsedQuery.data;
|
||||||
|
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db, targetHealthCheck, targets, resources } from "@server/db";
|
import { db, targetHealthCheck, targets, resources, sites } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -97,6 +97,9 @@ export async function listHealthChecks(
|
|||||||
.select({
|
.select({
|
||||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||||
name: targetHealthCheck.name,
|
name: targetHealthCheck.name,
|
||||||
|
siteId: targetHealthCheck.siteId,
|
||||||
|
siteName: sites.name,
|
||||||
|
siteNiceId: sites.niceId,
|
||||||
hcEnabled: targetHealthCheck.hcEnabled,
|
hcEnabled: targetHealthCheck.hcEnabled,
|
||||||
hcHealth: targetHealthCheck.hcHealth,
|
hcHealth: targetHealthCheck.hcHealth,
|
||||||
hcMode: targetHealthCheck.hcMode,
|
hcMode: targetHealthCheck.hcMode,
|
||||||
@@ -121,6 +124,7 @@ export async function listHealthChecks(
|
|||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.leftJoin(targets, eq(targetHealthCheck.targetId, targets.targetId))
|
.leftJoin(targets, eq(targetHealthCheck.targetId, targets.targetId))
|
||||||
.leftJoin(resources, eq(targets.resourceId, resources.resourceId))
|
.leftJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
|
.leftJoin(sites, eq(targetHealthCheck.siteId, sites.siteId))
|
||||||
.where(whereClause)
|
.where(whereClause)
|
||||||
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
|
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@@ -136,6 +140,9 @@ export async function listHealthChecks(
|
|||||||
healthChecks: list.map((row) => ({
|
healthChecks: list.map((row) => ({
|
||||||
targetHealthCheckId: row.targetHealthCheckId,
|
targetHealthCheckId: row.targetHealthCheckId,
|
||||||
name: row.name ?? "",
|
name: row.name ?? "",
|
||||||
|
siteId: row.siteId ?? null,
|
||||||
|
siteName: row.siteName ?? null,
|
||||||
|
siteNiceId: row.siteNiceId ?? null,
|
||||||
hcEnabled: row.hcEnabled,
|
hcEnabled: row.hcEnabled,
|
||||||
hcHealth: (row.hcHealth ?? "unknown") as
|
hcHealth: (row.hcHealth ?? "unknown") as
|
||||||
| "unknown"
|
| "unknown"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, targetHealthCheck } from "@server/db";
|
import { db, targetHealthCheck, newts, sites } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -21,6 +21,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { addStandaloneHealthCheck } from "@server/routers/newt/targets";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -34,6 +35,7 @@ const paramsSchema = z
|
|||||||
|
|
||||||
const bodySchema = z.strictObject({
|
const bodySchema = z.strictObject({
|
||||||
name: z.string().nonempty().optional(),
|
name: z.string().nonempty().optional(),
|
||||||
|
siteId: z.number().int().positive().optional(),
|
||||||
hcEnabled: z.boolean().optional(),
|
hcEnabled: z.boolean().optional(),
|
||||||
hcMode: z.string().optional(),
|
hcMode: z.string().optional(),
|
||||||
hcHostname: z.string().optional(),
|
hcHostname: z.string().optional(),
|
||||||
@@ -55,6 +57,7 @@ const bodySchema = z.strictObject({
|
|||||||
export type UpdateHealthCheckResponse = {
|
export type UpdateHealthCheckResponse = {
|
||||||
targetHealthCheckId: number;
|
targetHealthCheckId: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
|
siteId: number | null;
|
||||||
hcEnabled: boolean;
|
hcEnabled: boolean;
|
||||||
hcHealth: string | null;
|
hcHealth: string | null;
|
||||||
hcMode: string | null;
|
hcMode: string | null;
|
||||||
@@ -125,10 +128,7 @@ export async function updateHealthCheck(
|
|||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(
|
eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
|
||||||
targetHealthCheck.targetHealthCheckId,
|
|
||||||
healthCheckId
|
|
||||||
),
|
|
||||||
eq(targetHealthCheck.orgId, orgId),
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
isNull(targetHealthCheck.targetId)
|
isNull(targetHealthCheck.targetId)
|
||||||
)
|
)
|
||||||
@@ -145,6 +145,7 @@ export async function updateHealthCheck(
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
siteId,
|
||||||
hcEnabled,
|
hcEnabled,
|
||||||
hcMode,
|
hcMode,
|
||||||
hcHostname,
|
hcHostname,
|
||||||
@@ -166,6 +167,7 @@ export async function updateHealthCheck(
|
|||||||
const updateData: Record<string, unknown> = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (name !== undefined) updateData.name = name;
|
if (name !== undefined) updateData.name = name;
|
||||||
|
if (siteId !== undefined) updateData.siteId = siteId;
|
||||||
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
|
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
|
||||||
if (hcMode !== undefined) updateData.hcMode = hcMode;
|
if (hcMode !== undefined) updateData.hcMode = hcMode;
|
||||||
if (hcHostname !== undefined) updateData.hcHostname = hcHostname;
|
if (hcHostname !== undefined) updateData.hcHostname = hcHostname;
|
||||||
@@ -193,19 +195,28 @@ export async function updateHealthCheck(
|
|||||||
.set(updateData)
|
.set(updateData)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(
|
eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
|
||||||
targetHealthCheck.targetHealthCheckId,
|
|
||||||
healthCheckId
|
|
||||||
),
|
|
||||||
eq(targetHealthCheck.orgId, orgId),
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
isNull(targetHealthCheck.targetId)
|
isNull(targetHealthCheck.targetId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
// Push updated health check to newt if the site is a newt site
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, updated.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (newt) {
|
||||||
|
await addStandaloneHealthCheck(newt.newtId, updated, newt.version);
|
||||||
|
}
|
||||||
|
|
||||||
return response<UpdateHealthCheckResponse>(res, {
|
return response<UpdateHealthCheckResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
targetHealthCheckId: updated.targetHealthCheckId,
|
targetHealthCheckId: updated.targetHealthCheckId,
|
||||||
|
siteId: updated.siteId ?? null,
|
||||||
name: updated.name ?? null,
|
name: updated.name ?? null,
|
||||||
hcEnabled: updated.hcEnabled,
|
hcEnabled: updated.hcEnabled,
|
||||||
hcHealth: updated.hcHealth ?? null,
|
hcHealth: updated.hcHealth ?? null,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ export type ListHealthChecksResponse = {
|
|||||||
healthChecks: {
|
healthChecks: {
|
||||||
targetHealthCheckId: number;
|
targetHealthCheckId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
siteId: number | null;
|
||||||
|
siteName: string | null;
|
||||||
|
siteNiceId: string | null;
|
||||||
hcEnabled: boolean;
|
hcEnabled: boolean;
|
||||||
hcHealth: "unknown" | "healthy" | "unhealthy";
|
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||||
hcMode: string | null;
|
hcMode: string | null;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
generateSubnetProxyTargetV2,
|
generateSubnetProxyTargetV2,
|
||||||
SubnetProxyTargetV2
|
SubnetProxyTargetV2
|
||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
import { supportsTargetHealthChecksV2 } from "./targets";
|
|
||||||
|
|
||||||
export async function buildClientConfigurationForNewtClient(
|
export async function buildClientConfigurationForNewtClient(
|
||||||
site: Site,
|
site: Site,
|
||||||
@@ -205,7 +204,14 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
port: targets.port,
|
port: targets.port,
|
||||||
internalPort: targets.internalPort,
|
internalPort: targets.internalPort,
|
||||||
enabled: targets.enabled,
|
enabled: targets.enabled,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol
|
||||||
|
})
|
||||||
|
.from(targets)
|
||||||
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
|
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
|
||||||
|
|
||||||
|
const allHealthChecks = await db
|
||||||
|
.select({
|
||||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||||
hcEnabled: targetHealthCheck.hcEnabled,
|
hcEnabled: targetHealthCheck.hcEnabled,
|
||||||
hcPath: targetHealthCheck.hcPath,
|
hcPath: targetHealthCheck.hcPath,
|
||||||
@@ -224,13 +230,8 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
hcHealthyThreshold: targetHealthCheck.hcHealthyThreshold,
|
hcHealthyThreshold: targetHealthCheck.hcHealthyThreshold,
|
||||||
hcUnhealthyThreshold: targetHealthCheck.hcUnhealthyThreshold
|
hcUnhealthyThreshold: targetHealthCheck.hcUnhealthyThreshold
|
||||||
})
|
})
|
||||||
.from(targets)
|
.from(targetHealthCheck)
|
||||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
.where(eq(targetHealthCheck.siteId, siteId));
|
||||||
.leftJoin(
|
|
||||||
targetHealthCheck,
|
|
||||||
eq(targets.targetId, targetHealthCheck.targetId)
|
|
||||||
)
|
|
||||||
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
|
|
||||||
|
|
||||||
const { tcpTargets, udpTargets } = allTargets.reduce(
|
const { tcpTargets, udpTargets } = allTargets.reduce(
|
||||||
(acc, target) => {
|
(acc, target) => {
|
||||||
@@ -254,7 +255,7 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
|
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
|
||||||
);
|
);
|
||||||
|
|
||||||
const healthCheckTargets = allTargets.map((target) => {
|
const healthCheckTargets = allHealthChecks.map((target) => {
|
||||||
// make sure the stuff is defined
|
// make sure the stuff is defined
|
||||||
const isTCP = target.hcMode?.toLowerCase() === "tcp";
|
const isTCP = target.hcMode?.toLowerCase() === "tcp";
|
||||||
if (!target.hcHostname || !target.hcPort || !target.hcInterval) {
|
if (!target.hcHostname || !target.hcPort || !target.hcInterval) {
|
||||||
@@ -278,9 +279,7 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: supportsTargetHealthChecksV2(version)
|
id: target.targetHealthCheckId,
|
||||||
? target.targetId
|
|
||||||
: target.targetHealthCheckId,
|
|
||||||
hcEnabled: target.hcEnabled,
|
hcEnabled: target.hcEnabled,
|
||||||
hcPath: target.hcPath,
|
hcPath: target.hcPath,
|
||||||
hcScheme: target.hcScheme,
|
hcScheme: target.hcScheme,
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ import { Target, TargetHealthCheck } from "@server/db";
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
import semver from "semver";
|
|
||||||
|
|
||||||
const NEWT_V2_TARGET_HEALTH_CHECK_VERSION = ">=1.12.0";
|
|
||||||
|
|
||||||
export function supportsTargetHealthChecksV2(version?: string | null) {
|
|
||||||
return version ? semver.satisfies(version, NEWT_V2_TARGET_HEALTH_CHECK_VERSION) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addTargets(
|
export async function addTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
@@ -90,7 +83,7 @@ export async function addTargets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: supportsTargetHealthChecksV2(version) ? target.targetId : hc.targetHealthCheckId,
|
id: hc.targetHealthCheckId,
|
||||||
hcEnabled: hc.hcEnabled,
|
hcEnabled: hc.hcEnabled,
|
||||||
hcPath: hc.hcPath,
|
hcPath: hc.hcPath,
|
||||||
hcScheme: hc.hcScheme,
|
hcScheme: hc.hcScheme,
|
||||||
@@ -127,6 +120,96 @@ export async function addTargets(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addStandaloneHealthCheck(
|
||||||
|
newtId: string,
|
||||||
|
healthCheck: TargetHealthCheck,
|
||||||
|
version?: string | null
|
||||||
|
) {
|
||||||
|
const isTCP = healthCheck.hcMode?.toLowerCase() === "tcp";
|
||||||
|
if (
|
||||||
|
!healthCheck.hcHostname ||
|
||||||
|
!healthCheck.hcPort ||
|
||||||
|
!healthCheck.hcInterval
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`Skipping standalone health check ${healthCheck.targetHealthCheckId} due to missing fields`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isTCP && (!healthCheck.hcPath || !healthCheck.hcMethod)) {
|
||||||
|
logger.debug(
|
||||||
|
`Skipping standalone health check ${healthCheck.targetHealthCheckId} due to missing HTTP health check fields`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hcHeadersParse = healthCheck.hcHeaders
|
||||||
|
? JSON.parse(healthCheck.hcHeaders)
|
||||||
|
: null;
|
||||||
|
const hcHeadersSend: { [key: string]: string } = {};
|
||||||
|
if (hcHeadersParse) {
|
||||||
|
hcHeadersParse.forEach((header: { name: string; value: string }) => {
|
||||||
|
hcHeadersSend[header.name] = header.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let hcStatus: number | undefined = undefined;
|
||||||
|
if (healthCheck.hcStatus) {
|
||||||
|
const parsedStatus = parseInt(healthCheck.hcStatus.toString());
|
||||||
|
if (!isNaN(parsedStatus)) {
|
||||||
|
hcStatus = parsedStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClient(
|
||||||
|
newtId,
|
||||||
|
{
|
||||||
|
type: `newt/healthcheck/add`,
|
||||||
|
data: {
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
id: healthCheck.targetHealthCheckId,
|
||||||
|
hcEnabled: healthCheck.hcEnabled,
|
||||||
|
hcPath: healthCheck.hcPath,
|
||||||
|
hcScheme: healthCheck.hcScheme,
|
||||||
|
hcMode: healthCheck.hcMode,
|
||||||
|
hcHostname: healthCheck.hcHostname,
|
||||||
|
hcPort: healthCheck.hcPort,
|
||||||
|
hcInterval: healthCheck.hcInterval,
|
||||||
|
hcUnhealthyInterval: healthCheck.hcUnhealthyInterval,
|
||||||
|
hcTimeout: healthCheck.hcTimeout,
|
||||||
|
hcHeaders: hcHeadersSend,
|
||||||
|
hcFollowRedirects: healthCheck.hcFollowRedirects,
|
||||||
|
hcMethod: healthCheck.hcMethod,
|
||||||
|
hcStatus: hcStatus,
|
||||||
|
hcTlsServerName: healthCheck.hcTlsServerName,
|
||||||
|
hcHealthyThreshold: healthCheck.hcHealthyThreshold,
|
||||||
|
hcUnhealthyThreshold: healthCheck.hcUnhealthyThreshold
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeStandaloneHealthCheck(
|
||||||
|
newtId: string,
|
||||||
|
healthCheckId: number,
|
||||||
|
version?: string | null
|
||||||
|
) {
|
||||||
|
await sendToClient(
|
||||||
|
newtId,
|
||||||
|
{
|
||||||
|
type: `newt/healthcheck/remove`,
|
||||||
|
data: {
|
||||||
|
ids: [healthCheckId]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeTargets(
|
export async function removeTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ export async function createTarget(
|
|||||||
.values({
|
.values({
|
||||||
orgId: resource.orgId,
|
orgId: resource.orgId,
|
||||||
targetId: newTarget[0].targetId,
|
targetId: newTarget[0].targetId,
|
||||||
|
siteId: targetData.siteId,
|
||||||
name: `Resource ${resource.name} - ${targetData.ip}:${targetData.port}`,
|
name: `Resource ${resource.name} - ${targetData.ip}:${targetData.port}`,
|
||||||
hcEnabled: targetData.hcEnabled ?? false,
|
hcEnabled: targetData.hcEnabled ?? false,
|
||||||
hcPath: targetData.hcPath ?? null,
|
hcPath: targetData.hcPath ?? null,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
fireHealthCheckHealthyAlert,
|
fireHealthCheckHealthyAlert,
|
||||||
fireHealthCheckNotHealthyAlert
|
fireHealthCheckNotHealthyAlert
|
||||||
} from "#dynamic/lib/alerts";
|
} from "#dynamic/lib/alerts";
|
||||||
import { supportsTargetHealthChecksV2 } from "@server/routers/newt/targets";
|
|
||||||
|
|
||||||
interface TargetHealthStatus {
|
interface TargetHealthStatus {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -74,8 +73,6 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
const isV2 = supportsTargetHealthChecksV2(newt.version);
|
|
||||||
|
|
||||||
// Process each target status update
|
// Process each target status update
|
||||||
for (const [targetId, healthStatus] of Object.entries(data.targets)) {
|
for (const [targetId, healthStatus] of Object.entries(data.targets)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -91,78 +88,34 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetCheck: {
|
const [targetCheck] = await db
|
||||||
targetId: number;
|
.select({
|
||||||
siteId: number | null;
|
targetId: targets.targetId,
|
||||||
orgId: string | null;
|
siteId: targets.siteId,
|
||||||
targetHealthCheckId: number;
|
orgId: targetHealthCheck.orgId,
|
||||||
resourceOrgId: string | null;
|
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||||
resourceId: number | null;
|
resourceOrgId: resources.orgId,
|
||||||
name: string | null;
|
resourceId: resources.resourceId,
|
||||||
hcStatus: string | null;
|
name: targetHealthCheck.name,
|
||||||
} | undefined;
|
hcStatus: targetHealthCheck.hcHealth
|
||||||
|
})
|
||||||
if (isV2) {
|
.from(targetHealthCheck)
|
||||||
// New newt (>= 1.12.0): the key is the targetId
|
.innerJoin(
|
||||||
[targetCheck] = await db
|
targets,
|
||||||
.select({
|
eq(targetHealthCheck.targetId, targets.targetId)
|
||||||
targetId: targets.targetId,
|
)
|
||||||
siteId: targets.siteId,
|
.innerJoin(
|
||||||
orgId: targetHealthCheck.orgId,
|
resources,
|
||||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
eq(targets.resourceId, resources.resourceId)
|
||||||
resourceOrgId: resources.orgId,
|
)
|
||||||
resourceId: resources.resourceId,
|
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
name: targetHealthCheck.name,
|
.where(
|
||||||
hcStatus: targetHealthCheck.hcHealth
|
and(
|
||||||
})
|
eq(targetHealthCheck.targetHealthCheckId, targetIdNum),
|
||||||
.from(targets)
|
eq(sites.siteId, newt.siteId)
|
||||||
.innerJoin(
|
|
||||||
resources,
|
|
||||||
eq(targets.resourceId, resources.resourceId)
|
|
||||||
)
|
)
|
||||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
)
|
||||||
.innerJoin(
|
.limit(1);
|
||||||
targetHealthCheck,
|
|
||||||
eq(targets.targetId, targetHealthCheck.targetId)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(targets.targetId, targetIdNum),
|
|
||||||
eq(sites.siteId, newt.siteId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
} else {
|
|
||||||
// Old newt (< 1.12.0): the key is the targetHealthCheckId
|
|
||||||
[targetCheck] = await db
|
|
||||||
.select({
|
|
||||||
targetId: targets.targetId,
|
|
||||||
siteId: targets.siteId,
|
|
||||||
orgId: targetHealthCheck.orgId,
|
|
||||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
|
||||||
resourceOrgId: resources.orgId,
|
|
||||||
resourceId: resources.resourceId,
|
|
||||||
name: targetHealthCheck.name,
|
|
||||||
hcStatus: targetHealthCheck.hcHealth
|
|
||||||
})
|
|
||||||
.from(targetHealthCheck)
|
|
||||||
.innerJoin(
|
|
||||||
targets,
|
|
||||||
eq(targetHealthCheck.targetId, targets.targetId)
|
|
||||||
)
|
|
||||||
.innerJoin(
|
|
||||||
resources,
|
|
||||||
eq(targets.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(targetHealthCheck.targetHealthCheckId, targetIdNum),
|
|
||||||
eq(sites.siteId, newt.siteId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetCheck) {
|
if (!targetCheck) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ export async function updateTarget(
|
|||||||
const [updatedHc] = await db
|
const [updatedHc] = await db
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
.set({
|
.set({
|
||||||
|
siteId: parsedBody.data.siteId,
|
||||||
hcEnabled: parsedBody.data.hcEnabled || false,
|
hcEnabled: parsedBody.data.hcEnabled || false,
|
||||||
hcPath: parsedBody.data.hcPath,
|
hcPath: parsedBody.data.hcPath,
|
||||||
hcScheme: parsedBody.data.hcScheme,
|
hcScheme: parsedBody.data.hcScheme,
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { SitesSelector } from "@app/components/site-selector";
|
||||||
|
import type { Selectedsite } from "@app/components/site-selector";
|
||||||
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
export type HealthCheckConfig = {
|
export type HealthCheckConfig = {
|
||||||
hcEnabled: boolean;
|
hcEnabled: boolean;
|
||||||
@@ -84,6 +89,9 @@ export type HealthCheckRow = {
|
|||||||
resourceId: number | null;
|
resourceId: number | null;
|
||||||
resourceName: string | null;
|
resourceName: string | null;
|
||||||
resourceNiceId: string | null;
|
resourceNiceId: string | null;
|
||||||
|
siteId: number | null;
|
||||||
|
siteName: string | null;
|
||||||
|
siteNiceId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HealthCheckCredenzaProps =
|
export type HealthCheckCredenzaProps =
|
||||||
@@ -132,6 +140,7 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedSite, setSelectedSite] = useState<Selectedsite | null>(null);
|
||||||
|
|
||||||
const healthCheckSchema = z
|
const healthCheckSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -280,8 +289,14 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
|||||||
hcStatus: initialValues.hcStatus ?? null,
|
hcStatus: initialValues.hcStatus ?? null,
|
||||||
hcHeaders: parsedHeaders
|
hcHeaders: parsedHeaders
|
||||||
});
|
});
|
||||||
|
if (initialValues.siteId && initialValues.siteName) {
|
||||||
|
setSelectedSite({ siteId: initialValues.siteId, name: initialValues.siteName, type: "" });
|
||||||
|
} else {
|
||||||
|
setSelectedSite(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
form.reset(DEFAULT_VALUES);
|
form.reset(DEFAULT_VALUES);
|
||||||
|
setSelectedSite(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
@@ -331,6 +346,7 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: (values as any).name,
|
name: (values as any).name,
|
||||||
|
siteId: selectedSite?.siteId,
|
||||||
hcEnabled: values.hcEnabled,
|
hcEnabled: values.hcEnabled,
|
||||||
hcMode: values.hcMode,
|
hcMode: values.hcMode,
|
||||||
hcScheme: values.hcScheme,
|
hcScheme: values.hcScheme,
|
||||||
@@ -439,6 +455,42 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Site picker (submit mode only) */}
|
||||||
|
{mode === "submit" && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("site")}</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between",
|
||||||
|
!selectedSite && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{selectedSite ? selectedSite.name : t("siteSelect")}
|
||||||
|
</span>
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0 w-full min-w-64">
|
||||||
|
<SitesSelector
|
||||||
|
orgId={orgId!}
|
||||||
|
selectedSite={selectedSite}
|
||||||
|
onSelectSite={(site) => {
|
||||||
|
setSelectedSite(site);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<HorizontalTabs
|
<HorizontalTabs
|
||||||
clientSide
|
clientSide
|
||||||
|
|||||||
@@ -240,6 +240,27 @@ export default function HealthChecksTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "site",
|
||||||
|
friendlyName: "Site",
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">Site</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original;
|
||||||
|
if (!r.siteId || !r.siteName || !r.siteNiceId) {
|
||||||
|
return <span className="text-neutral-400">-</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link href={`/${orgId}/settings/sites/${r.siteNiceId}/general`}>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{r.siteName}
|
||||||
|
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "health",
|
id: "health",
|
||||||
friendlyName: t("standaloneHcColumnHealth"),
|
friendlyName: t("standaloneHcColumnHealth"),
|
||||||
|
|||||||
@@ -335,6 +335,9 @@ export const orgQueries = {
|
|||||||
healthChecks: {
|
healthChecks: {
|
||||||
targetHealthCheckId: number;
|
targetHealthCheckId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
siteId: number | null;
|
||||||
|
siteName: string | null;
|
||||||
|
siteNiceId: string | null;
|
||||||
hcEnabled: boolean;
|
hcEnabled: boolean;
|
||||||
hcHealth: "unknown" | "healthy" | "unhealthy";
|
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||||
hcMode: string | null;
|
hcMode: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user