mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-06 07:38:46 +00:00
Wrap in transactions
This commit is contained in:
@@ -17,16 +17,21 @@ export async function addTargets(
|
|||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendToClient(
|
if (payloadTargets.length > 0) {
|
||||||
newtId,
|
await sendToClient(
|
||||||
{
|
newtId,
|
||||||
type: `newt/${protocol}/add`,
|
{
|
||||||
data: {
|
type: `newt/${protocol}/add`,
|
||||||
targets: payloadTargets
|
data: {
|
||||||
|
targets: payloadTargets
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incrementConfigVersion: true,
|
||||||
|
compress: canCompress(version, "newt")
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const healthCheckTargets = healthCheckData.map((hc) => {
|
const healthCheckTargets = healthCheckData.map((hc) => {
|
||||||
// Ensure all necessary fields are present
|
// Ensure all necessary fields are present
|
||||||
@@ -206,16 +211,18 @@ export async function removeTargets(
|
|||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendToClient(
|
if (payloadTargets.length > 0) {
|
||||||
newtId,
|
await sendToClient(
|
||||||
{
|
newtId,
|
||||||
type: `newt/${protocol}/remove`,
|
{
|
||||||
data: {
|
type: `newt/${protocol}/remove`,
|
||||||
targets: payloadTargets
|
data: {
|
||||||
}
|
targets: payloadTargets
|
||||||
},
|
}
|
||||||
{ incrementConfigVersion: true }
|
},
|
||||||
);
|
{ incrementConfigVersion: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const healthCheckTargets = healthCheckData.map((hc) => {
|
const healthCheckTargets = healthCheckData.map((hc) => {
|
||||||
return hc.targetHealthCheckId;
|
return hc.targetHealthCheckId;
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ export async function deleteResource(
|
|||||||
|
|
||||||
await removeTargets(
|
await removeTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
[target],
|
// [target],
|
||||||
|
[], // deleting the target from newt causes issues because we cant unbind the port. this needs to be fixed in newt before we can do this
|
||||||
[healthCheck],
|
[healthCheck],
|
||||||
deletedResource.protocol,
|
deletedResource.protocol,
|
||||||
newt.version
|
newt.version
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ import { eq } from "drizzle-orm";
|
|||||||
import { pickPort } from "./helpers";
|
import { pickPort } from "./helpers";
|
||||||
import { isTargetValid } from "@server/lib/validators";
|
import { isTargetValid } from "@server/lib/validators";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { fireHealthCheckHealthyAlert, fireHealthCheckUnhealthyAlert, fireHealthCheckUnknownAlert } from "#dynamic/lib/alerts";
|
import {
|
||||||
|
fireHealthCheckHealthyAlert,
|
||||||
|
fireHealthCheckUnhealthyAlert,
|
||||||
|
fireHealthCheckUnknownAlert
|
||||||
|
} from "#dynamic/lib/alerts";
|
||||||
|
|
||||||
const createTargetParamsSchema = z.strictObject({
|
const createTargetParamsSchema = z.strictObject({
|
||||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -142,151 +146,155 @@ export async function createTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingTargets = await db
|
|
||||||
.select()
|
|
||||||
.from(targets)
|
|
||||||
.where(eq(targets.resourceId, resourceId));
|
|
||||||
|
|
||||||
const existingTarget = existingTargets.find(
|
|
||||||
(target) =>
|
|
||||||
target.ip === targetData.ip &&
|
|
||||||
target.port === targetData.port &&
|
|
||||||
target.method === targetData.method &&
|
|
||||||
target.siteId === targetData.siteId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingTarget) {
|
|
||||||
// log a warning
|
|
||||||
logger.warn(
|
|
||||||
`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let newTarget: Target[] = [];
|
let newTarget: Target[] = [];
|
||||||
let healthCheck: TargetHealthCheck[] = [];
|
|
||||||
let targetIps: string[] = [];
|
let targetIps: string[] = [];
|
||||||
if (site.type == "local") {
|
let healthCheck: TargetHealthCheck[] = [];
|
||||||
newTarget = await db
|
await db.transaction(async (trx) => {
|
||||||
.insert(targets)
|
const existingTargets = await trx
|
||||||
.values({
|
.select()
|
||||||
resourceId,
|
.from(targets)
|
||||||
...targetData,
|
.where(eq(targets.resourceId, resourceId));
|
||||||
priority: targetData.priority || 100
|
|
||||||
})
|
const existingTarget = existingTargets.find(
|
||||||
.returning();
|
(target) =>
|
||||||
} else {
|
target.ip === targetData.ip &&
|
||||||
// make sure the target is within the site subnet
|
target.port === targetData.port &&
|
||||||
if (
|
target.method === targetData.method &&
|
||||||
site.type == "wireguard" &&
|
target.siteId === targetData.siteId
|
||||||
!isIpInCidr(targetData.ip, site.subnet!)
|
);
|
||||||
) {
|
|
||||||
return next(
|
if (existingTarget) {
|
||||||
createHttpError(
|
// log a warning
|
||||||
HttpCode.BAD_REQUEST,
|
logger.warn(
|
||||||
`Target IP is not within the site subnet`
|
`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}`
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { internalPort, targetIps: newTargetIps } = await pickPort(
|
if (site.type == "local") {
|
||||||
site.siteId!,
|
newTarget = await trx
|
||||||
db
|
.insert(targets)
|
||||||
);
|
.values({
|
||||||
|
resourceId,
|
||||||
|
...targetData,
|
||||||
|
priority: targetData.priority || 100
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
} else {
|
||||||
|
// make sure the target is within the site subnet
|
||||||
|
if (
|
||||||
|
site.type == "wireguard" &&
|
||||||
|
!isIpInCidr(targetData.ip, site.subnet!)
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Target IP is not within the site subnet`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!internalPort) {
|
const { internalPort, targetIps: newTargetIps } =
|
||||||
return next(
|
await pickPort(site.siteId!, trx);
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
if (!internalPort) {
|
||||||
`No available internal port`
|
return next(
|
||||||
)
|
createHttpError(
|
||||||
);
|
HttpCode.BAD_REQUEST,
|
||||||
|
`No available internal port`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
newTarget = await trx
|
||||||
|
.insert(targets)
|
||||||
|
.values({
|
||||||
|
resourceId,
|
||||||
|
siteId: site.siteId,
|
||||||
|
ip: targetData.ip,
|
||||||
|
method: targetData.method,
|
||||||
|
port: targetData.port,
|
||||||
|
internalPort,
|
||||||
|
enabled: targetData.enabled,
|
||||||
|
path: targetData.path,
|
||||||
|
pathMatchType: targetData.pathMatchType,
|
||||||
|
rewritePath: targetData.rewritePath,
|
||||||
|
rewritePathType: targetData.rewritePathType,
|
||||||
|
priority: targetData.priority || 100
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// add the new target to the targetIps array
|
||||||
|
newTargetIps.push(`${targetData.ip}/32`);
|
||||||
|
|
||||||
|
targetIps = newTargetIps;
|
||||||
}
|
}
|
||||||
|
|
||||||
newTarget = await db
|
let hcHeaders = null;
|
||||||
.insert(targets)
|
if (targetData.hcHeaders) {
|
||||||
|
hcHeaders = JSON.stringify(targetData.hcHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
healthCheck = await trx
|
||||||
|
.insert(targetHealthCheck)
|
||||||
.values({
|
.values({
|
||||||
resourceId,
|
orgId: resource.orgId,
|
||||||
siteId: site.siteId,
|
targetId: newTarget[0].targetId,
|
||||||
ip: targetData.ip,
|
siteId: targetData.siteId,
|
||||||
method: targetData.method,
|
name: `Resource ${resource.name} - ${targetData.ip}:${targetData.port}`,
|
||||||
port: targetData.port,
|
hcEnabled: targetData.hcEnabled ?? false,
|
||||||
internalPort,
|
hcPath: targetData.hcPath ?? null,
|
||||||
enabled: targetData.enabled,
|
hcScheme: targetData.hcScheme ?? null,
|
||||||
path: targetData.path,
|
hcMode: targetData.hcMode ?? null,
|
||||||
pathMatchType: targetData.pathMatchType,
|
hcHostname: targetData.hcHostname ?? null,
|
||||||
rewritePath: targetData.rewritePath,
|
hcPort: targetData.hcPort ?? null,
|
||||||
rewritePathType: targetData.rewritePathType,
|
hcInterval: targetData.hcInterval ?? null,
|
||||||
priority: targetData.priority || 100
|
hcUnhealthyInterval: targetData.hcUnhealthyInterval ?? null,
|
||||||
|
hcTimeout: targetData.hcTimeout ?? null,
|
||||||
|
hcHeaders: hcHeaders,
|
||||||
|
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
||||||
|
hcMethod: targetData.hcMethod ?? null,
|
||||||
|
hcStatus: targetData.hcStatus ?? null,
|
||||||
|
hcHealth: targetData.hcEnabled ? "unhealthy" : "unknown",
|
||||||
|
hcTlsServerName: targetData.hcTlsServerName ?? null,
|
||||||
|
hcHealthyThreshold: targetData.hcHealthyThreshold ?? null,
|
||||||
|
hcUnhealthyThreshold:
|
||||||
|
targetData.hcUnhealthyThreshold ?? null
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// add the new target to the targetIps array
|
if (healthCheck[0].hcHealth === "unhealthy") {
|
||||||
newTargetIps.push(`${targetData.ip}/32`);
|
await fireHealthCheckUnhealthyAlert(
|
||||||
|
healthCheck[0].orgId,
|
||||||
targetIps = newTargetIps;
|
healthCheck[0].targetHealthCheckId,
|
||||||
}
|
healthCheck[0].name,
|
||||||
|
undefined,
|
||||||
let hcHeaders = null;
|
undefined,
|
||||||
if (targetData.hcHeaders) {
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
hcHeaders = JSON.stringify(targetData.hcHeaders);
|
trx
|
||||||
}
|
);
|
||||||
|
} else if (healthCheck[0].hcHealth === "unknown") {
|
||||||
healthCheck = await db
|
// if the health is unknown, we want to fire an alert to notify users to enable health checks
|
||||||
.insert(targetHealthCheck)
|
await fireHealthCheckUnknownAlert(
|
||||||
.values({
|
healthCheck[0].orgId,
|
||||||
orgId: resource.orgId,
|
healthCheck[0].targetHealthCheckId,
|
||||||
targetId: newTarget[0].targetId,
|
healthCheck[0].name,
|
||||||
siteId: targetData.siteId,
|
undefined,
|
||||||
name: `Resource ${resource.name} - ${targetData.ip}:${targetData.port}`,
|
undefined,
|
||||||
hcEnabled: targetData.hcEnabled ?? false,
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
hcPath: targetData.hcPath ?? null,
|
trx
|
||||||
hcScheme: targetData.hcScheme ?? null,
|
);
|
||||||
hcMode: targetData.hcMode ?? null,
|
} else if (healthCheck[0].hcHealth === "healthy") {
|
||||||
hcHostname: targetData.hcHostname ?? null,
|
await fireHealthCheckHealthyAlert(
|
||||||
hcPort: targetData.hcPort ?? null,
|
healthCheck[0].orgId,
|
||||||
hcInterval: targetData.hcInterval ?? null,
|
healthCheck[0].targetHealthCheckId,
|
||||||
hcUnhealthyInterval: targetData.hcUnhealthyInterval ?? null,
|
healthCheck[0].name,
|
||||||
hcTimeout: targetData.hcTimeout ?? null,
|
undefined,
|
||||||
hcHeaders: hcHeaders,
|
undefined,
|
||||||
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
hcMethod: targetData.hcMethod ?? null,
|
trx
|
||||||
hcStatus: targetData.hcStatus ?? null,
|
);
|
||||||
hcHealth: targetData.hcEnabled ? "unhealthy" : "unknown",
|
}
|
||||||
hcTlsServerName: targetData.hcTlsServerName ?? null,
|
});
|
||||||
hcHealthyThreshold: targetData.hcHealthyThreshold ?? null,
|
|
||||||
hcUnhealthyThreshold: targetData.hcUnhealthyThreshold ?? null
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (healthCheck[0].hcHealth === "unhealthy") {
|
|
||||||
await fireHealthCheckUnhealthyAlert(
|
|
||||||
healthCheck[0].orgId,
|
|
||||||
healthCheck[0].targetHealthCheckId,
|
|
||||||
healthCheck[0].name,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
false // dont send the alert because we just want to create the alert, not notify users yet
|
|
||||||
);
|
|
||||||
} else if (healthCheck[0].hcHealth === "unknown") {
|
|
||||||
// if the health is unknown, we want to fire an alert to notify users to enable health checks
|
|
||||||
await fireHealthCheckUnknownAlert(
|
|
||||||
healthCheck[0].orgId,
|
|
||||||
healthCheck[0].targetHealthCheckId,
|
|
||||||
healthCheck[0].name,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
false // dont send the alert because we just want to create the alert, not notify users yet
|
|
||||||
);
|
|
||||||
} else if (healthCheck[0].hcHealth === "healthy") {
|
|
||||||
await fireHealthCheckHealthyAlert(
|
|
||||||
healthCheck[0].orgId,
|
|
||||||
healthCheck[0].targetHealthCheckId,
|
|
||||||
healthCheck[0].name,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
false // dont send the alert because we just want to create the alert, not notify users yet
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (site.pubKey) {
|
if (site.pubKey) {
|
||||||
if (site.type == "wireguard") {
|
if (site.type == "wireguard") {
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { newts, resources, sites, targets } from "@server/db";
|
import { newts, resources, sites, targets } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, ne, and } 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 { addPeer } from "../gerbil/peers";
|
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { removeTargets } from "../newt/targets";
|
import { removeTargets } from "../newt/targets";
|
||||||
import { getAllowedIps } from "./helpers";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { targetHealthCheck } from "@server/db/pg";
|
import { targetHealthCheck } from "@server/db/pg";
|
||||||
|
|
||||||
@@ -80,10 +78,29 @@ export async function deleteTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if there are other targets on the resource
|
||||||
|
const otherTargets = await db
|
||||||
|
.select()
|
||||||
|
.from(targets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targets.resourceId, resource.resourceId),
|
||||||
|
ne(targets.targetId, targetId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (otherTargets.length == 0) {
|
||||||
|
// set the resource status
|
||||||
|
await db
|
||||||
|
.update(resources)
|
||||||
|
.set({ health: "unknown" })
|
||||||
|
.where(eq(resources.resourceId, resource.resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(eq(sites.siteId, targets.siteId))
|
.where(eq(sites.siteId, deletedTarget.siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!site) {
|
if (!site) {
|
||||||
@@ -106,7 +123,8 @@ export async function deleteTarget(
|
|||||||
|
|
||||||
await removeTargets(
|
await removeTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
[deletedTarget],
|
// [deletedTarget],
|
||||||
|
[], // deleting the target from newt causes issues because we cant unbind the port. this needs to be fixed in newt before we can do this
|
||||||
[deletedHealthCheck],
|
[deletedHealthCheck],
|
||||||
resource.protocol,
|
resource.protocol,
|
||||||
newt.version
|
newt.version
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { addPeer } from "../gerbil/peers";
|
import { addPeer } from "../gerbil/peers";
|
||||||
import { addTargets } from "../newt/targets";
|
import { addTargets } from "../newt/targets";
|
||||||
import { fireHealthCheckHealthyAlert, fireHealthCheckUnknownAlert } from "#dynamic/lib/alerts";
|
import { fireHealthCheckHealthyAlert, fireHealthCheckUnknownAlert, fireHealthCheckUnhealthyAlert } from "#dynamic/lib/alerts";
|
||||||
import { pickPort } from "./helpers";
|
import { pickPort } from "./helpers";
|
||||||
import { isTargetValid } from "@server/lib/validators";
|
import { isTargetValid } from "@server/lib/validators";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { fireHealthCheckUnhealthyAlert } from "@server/lib/alerts";
|
|
||||||
|
|
||||||
|
|
||||||
const updateTargetParamsSchema = z.strictObject({
|
const updateTargetParamsSchema = z.strictObject({
|
||||||
targetId: z.string().transform(Number).pipe(z.int().positive())
|
targetId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -168,124 +166,131 @@ export async function updateTarget(
|
|||||||
|
|
||||||
const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null;
|
const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null;
|
||||||
|
|
||||||
const [updatedTarget] = await db
|
let updatedTarget: any;
|
||||||
.update(targets)
|
let updatedHc: any;
|
||||||
.set({
|
await db.transaction(async (trx) => {
|
||||||
siteId: parsedBody.data.siteId,
|
[updatedTarget] = await trx
|
||||||
ip: parsedBody.data.ip,
|
.update(targets)
|
||||||
method: parsedBody.data.method,
|
.set({
|
||||||
port: parsedBody.data.port,
|
siteId: parsedBody.data.siteId,
|
||||||
internalPort,
|
ip: parsedBody.data.ip,
|
||||||
enabled: parsedBody.data.enabled,
|
method: parsedBody.data.method,
|
||||||
path: parsedBody.data.path,
|
port: parsedBody.data.port,
|
||||||
pathMatchType: parsedBody.data.pathMatchType,
|
internalPort,
|
||||||
priority: parsedBody.data.priority,
|
enabled: parsedBody.data.enabled,
|
||||||
rewritePath: pathMatchTypeRemoved ? null : parsedBody.data.rewritePath,
|
path: parsedBody.data.path,
|
||||||
rewritePathType: pathMatchTypeRemoved ? null : parsedBody.data.rewritePathType
|
pathMatchType: parsedBody.data.pathMatchType,
|
||||||
})
|
priority: parsedBody.data.priority,
|
||||||
.where(eq(targets.targetId, targetId))
|
rewritePath: pathMatchTypeRemoved ? null : parsedBody.data.rewritePath,
|
||||||
.returning();
|
rewritePathType: pathMatchTypeRemoved ? null : parsedBody.data.rewritePathType
|
||||||
|
})
|
||||||
|
.where(eq(targets.targetId, targetId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
const [existingHc] = await db
|
const [existingHc] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.where(eq(targetHealthCheck.targetId, targetId))
|
.where(eq(targetHealthCheck.targetId, targetId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!existingHc) {
|
if (!existingHc) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
`Health check for target with ID ${targetId} not found`
|
`Health check for target with ID ${targetId} not found`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hcHeaders = null;
|
let hcHeaders = null;
|
||||||
if (parsedBody.data.hcHeaders) {
|
if (parsedBody.data.hcHeaders) {
|
||||||
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);
|
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When health check is disabled, reset hcHealth to "unknown"
|
// When health check is disabled, reset hcHealth to "unknown"
|
||||||
// to prevent previously unhealthy targets from being excluded.
|
// to prevent previously unhealthy targets from being excluded.
|
||||||
// Also when the site is not a newt, set hcHealth to "unknown".
|
// Also when the site is not a newt, set hcHealth to "unknown".
|
||||||
// If hcEnabled is being turned on (was false, now true), set to "unhealthy"
|
// If hcEnabled is being turned on (was false, now true), set to "unhealthy"
|
||||||
// so the target must pass a health check before being considered healthy.
|
// so the target must pass a health check before being considered healthy.
|
||||||
const hcEnabledTurnedOn =
|
const hcEnabledTurnedOn =
|
||||||
parsedBody.data.hcEnabled === true && existingHc.hcEnabled === false;
|
parsedBody.data.hcEnabled === true && existingHc.hcEnabled === false;
|
||||||
|
|
||||||
let hcHealthValue: "unknown" | "healthy" | "unhealthy" | undefined;
|
let hcHealthValue: "unknown" | "healthy" | "unhealthy" | undefined;
|
||||||
if (
|
if (
|
||||||
parsedBody.data.hcEnabled === false ||
|
parsedBody.data.hcEnabled === false ||
|
||||||
parsedBody.data.hcEnabled === null ||
|
parsedBody.data.hcEnabled === null ||
|
||||||
site.type !== "newt"
|
site.type !== "newt"
|
||||||
) {
|
) {
|
||||||
hcHealthValue = "unknown";
|
hcHealthValue = "unknown";
|
||||||
} else if (hcEnabledTurnedOn) {
|
} else if (hcEnabledTurnedOn) {
|
||||||
hcHealthValue = "unhealthy";
|
hcHealthValue = "unhealthy";
|
||||||
} else {
|
} else {
|
||||||
hcHealthValue = undefined;
|
hcHealthValue = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDisablingHc =
|
const isDisablingHc =
|
||||||
(parsedBody.data.hcEnabled === false ||
|
(parsedBody.data.hcEnabled === false ||
|
||||||
parsedBody.data.hcEnabled === null) &&
|
parsedBody.data.hcEnabled === null) &&
|
||||||
existingHc.hcEnabled === true;
|
existingHc.hcEnabled === true;
|
||||||
|
|
||||||
const [updatedHc] = await db
|
const [updatedHc] = await trx
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
.set({
|
.set({
|
||||||
siteId: parsedBody.data.siteId,
|
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,
|
||||||
hcMode: parsedBody.data.hcMode,
|
hcMode: parsedBody.data.hcMode,
|
||||||
hcHostname: parsedBody.data.hcHostname,
|
hcHostname: parsedBody.data.hcHostname,
|
||||||
hcPort: parsedBody.data.hcPort,
|
hcPort: parsedBody.data.hcPort,
|
||||||
hcInterval: parsedBody.data.hcInterval,
|
hcInterval: parsedBody.data.hcInterval,
|
||||||
hcUnhealthyInterval: parsedBody.data.hcUnhealthyInterval,
|
hcUnhealthyInterval: parsedBody.data.hcUnhealthyInterval,
|
||||||
hcTimeout: parsedBody.data.hcTimeout,
|
hcTimeout: parsedBody.data.hcTimeout,
|
||||||
hcHeaders: hcHeaders,
|
hcHeaders: hcHeaders,
|
||||||
hcFollowRedirects: parsedBody.data.hcFollowRedirects,
|
hcFollowRedirects: parsedBody.data.hcFollowRedirects,
|
||||||
hcMethod: parsedBody.data.hcMethod,
|
hcMethod: parsedBody.data.hcMethod,
|
||||||
hcStatus: parsedBody.data.hcStatus,
|
hcStatus: parsedBody.data.hcStatus,
|
||||||
hcTlsServerName: parsedBody.data.hcTlsServerName,
|
hcTlsServerName: parsedBody.data.hcTlsServerName,
|
||||||
hcHealthyThreshold: parsedBody.data.hcHealthyThreshold,
|
hcHealthyThreshold: parsedBody.data.hcHealthyThreshold,
|
||||||
hcUnhealthyThreshold: parsedBody.data.hcUnhealthyThreshold,
|
hcUnhealthyThreshold: parsedBody.data.hcUnhealthyThreshold,
|
||||||
hcHealth: hcHealthValue
|
hcHealth: hcHealthValue
|
||||||
})
|
})
|
||||||
.where(eq(targetHealthCheck.targetId, targetId))
|
.where(eq(targetHealthCheck.targetId, targetId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (updatedHc.hcHealth === "unhealthy" && existingHc.hcHealth !== "unhealthy") {
|
if (updatedHc.hcHealth === "unhealthy" && existingHc.hcHealth !== "unhealthy") {
|
||||||
await fireHealthCheckUnhealthyAlert(
|
await fireHealthCheckUnhealthyAlert(
|
||||||
updatedHc.orgId,
|
updatedHc.orgId,
|
||||||
updatedHc.targetHealthCheckId,
|
updatedHc.targetHealthCheckId,
|
||||||
updatedHc.name || "",
|
updatedHc.name || "",
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
false // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
);
|
trx
|
||||||
} else if (updatedHc.hcHealth === "unknown" && existingHc.hcHealth !== "unknown") {
|
);
|
||||||
// if the health is unknown, we want to fire an alert to notify users to enable health checks
|
} else if (updatedHc.hcHealth === "unknown" && existingHc.hcHealth !== "unknown") {
|
||||||
await fireHealthCheckUnknownAlert(
|
// if the health is unknown, we want to fire an alert to notify users to enable health checks
|
||||||
updatedHc.orgId,
|
await fireHealthCheckUnknownAlert(
|
||||||
updatedHc.targetHealthCheckId,
|
updatedHc.orgId,
|
||||||
updatedHc.name,
|
updatedHc.targetHealthCheckId,
|
||||||
undefined,
|
updatedHc.name,
|
||||||
undefined,
|
undefined,
|
||||||
false // dont send the alert because we just want to create the alert, not notify users yet
|
undefined,
|
||||||
);
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
} else if (updatedHc.hcHealth === "healthy" && existingHc.hcHealth !== "healthy") {
|
trx
|
||||||
await fireHealthCheckHealthyAlert(
|
);
|
||||||
updatedHc.orgId,
|
} else if (updatedHc.hcHealth === "healthy" && existingHc.hcHealth !== "healthy") {
|
||||||
updatedHc.targetHealthCheckId,
|
await fireHealthCheckHealthyAlert(
|
||||||
updatedHc.name,
|
updatedHc.orgId,
|
||||||
undefined,
|
updatedHc.targetHealthCheckId,
|
||||||
undefined,
|
updatedHc.name,
|
||||||
false // dont send the alert because we just want to create the alert, not notify users yet
|
undefined,
|
||||||
);
|
undefined,
|
||||||
}
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (site.pubKey) {
|
if (site.pubKey) {
|
||||||
if (site.type == "wireguard") {
|
if (site.type == "wireguard") {
|
||||||
@@ -310,6 +315,7 @@ export async function updateTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: {
|
data: {
|
||||||
...updatedTarget,
|
...updatedTarget,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
|||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { build } from "@server/build";
|
||||||
import { tlsNameSchema } from "@server/lib/schemas";
|
import { tlsNameSchema } from "@server/lib/schemas";
|
||||||
import { type GetResourceResponse } from "@server/routers/resource";
|
import { type GetResourceResponse } from "@server/routers/resource";
|
||||||
import type { ListSitesResponse } from "@server/routers/site";
|
import type { ListSitesResponse } from "@server/routers/site";
|
||||||
@@ -953,6 +954,18 @@ function ProxyResourceTargetsForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{build === "saas" &&
|
||||||
|
targets.length > 1 &&
|
||||||
|
new Set(targets.map((t) => t.siteId)).size > 1 && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-3 flex items-start gap-1.5">
|
||||||
|
<AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
|
||||||
|
<span>
|
||||||
|
Round robin routing will not work between
|
||||||
|
sites that are not connected to the same
|
||||||
|
node, but failover will work.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
|
||||||
<form className="self-end mt-4" action={formAction}>
|
<form className="self-end mt-4" action={formAction}>
|
||||||
|
|||||||
@@ -1419,6 +1419,18 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{build === "enterprise" &&
|
||||||
|
targets.length > 1 &&
|
||||||
|
new Set(targets.map((t) => t.siteId)).size > 1 && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-3 flex items-start gap-1.5">
|
||||||
|
<InfoIcon className="h-4 w-4 shrink-0 mt-0.5" />
|
||||||
|
<span>
|
||||||
|
Round robin routing will not work between
|
||||||
|
sites that are not connected to the same
|
||||||
|
node, but failover will work.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user