Handle crud to newt with new hcs

This commit is contained in:
Owen
2026-04-21 14:21:58 -07:00
parent b1293e6f56
commit 7b3c10c7b0
5 changed files with 172 additions and 22 deletions

View File

@@ -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()
@@ -143,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

View File

@@ -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,

View File

@@ -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({
@@ -127,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)
) )
@@ -197,16 +195,24 @@ 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,

View File

@@ -205,7 +205,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 +231,13 @@ 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(
.leftJoin( and(
targetHealthCheck, eq(targetHealthCheck.siteId, siteId),
eq(targets.targetId, targetHealthCheck.targetId) eq(targetHealthCheck.hcEnabled, true)
) )
.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 +261,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) {

View File

@@ -7,7 +7,9 @@ import semver from "semver";
const NEWT_V2_TARGET_HEALTH_CHECK_VERSION = ">=1.12.0"; const NEWT_V2_TARGET_HEALTH_CHECK_VERSION = ">=1.12.0";
export function supportsTargetHealthChecksV2(version?: string | null) { export function supportsTargetHealthChecksV2(version?: string | null) {
return version ? semver.satisfies(version, NEWT_V2_TARGET_HEALTH_CHECK_VERSION) : false; return version
? semver.satisfies(version, NEWT_V2_TARGET_HEALTH_CHECK_VERSION)
: false;
} }
export async function addTargets( export async function addTargets(
@@ -90,7 +92,9 @@ export async function addTargets(
} }
return { return {
id: supportsTargetHealthChecksV2(version) ? target.targetId : hc.targetHealthCheckId, id: supportsTargetHealthChecksV2(version)
? target.targetId
: hc.targetHealthCheckId,
hcEnabled: hc.hcEnabled, hcEnabled: hc.hcEnabled,
hcPath: hc.hcPath, hcPath: hc.hcPath,
hcScheme: hc.hcScheme, hcScheme: hc.hcScheme,
@@ -127,6 +131,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[],