Merge dev into fix/log-analytics-adjustments

This commit is contained in:
Fred KISSIE
2025-12-10 03:19:14 +01:00
parent 9db2feff77
commit d490cab48c
555 changed files with 9375 additions and 9287 deletions

View File

@@ -16,51 +16,41 @@ import { isTargetValid } from "@server/lib/validators";
import { OpenAPITags, registry } from "@server/openApi";
const createTargetParamsSchema = z.strictObject({
resourceId: z
.string()
.transform(Number)
.pipe(z.int().positive())
});
resourceId: z.string().transform(Number).pipe(z.int().positive())
});
const createTargetSchema = z.strictObject({
siteId: z.int().positive(),
ip: z.string().refine(isTargetValid),
method: z.string().optional().nullable(),
port: z.int().min(1).max(65535),
enabled: z.boolean().default(true),
hcEnabled: z.boolean().optional(),
hcPath: z.string().min(1).optional().nullable(),
hcScheme: z.string().optional().nullable(),
hcMode: z.string().optional().nullable(),
hcHostname: z.string().optional().nullable(),
hcPort: z.int().positive().optional().nullable(),
hcInterval: z.int().positive().min(5).optional().nullable(),
hcUnhealthyInterval: z.int()
.positive()
.min(5)
.optional()
.nullable(),
hcTimeout: z.int().positive().min(1).optional().nullable(),
hcHeaders: z
.array(z.strictObject({ name: z.string(), value: z.string() }))
.nullable()
.optional(),
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z
.enum(["exact", "prefix", "regex"])
.optional()
.nullable(),
rewritePath: z.string().optional().nullable(),
rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable(),
priority: z.int().min(1).max(1000).optional().nullable()
});
siteId: z.int().positive(),
ip: z.string().refine(isTargetValid),
method: z.string().optional().nullable(),
port: z.int().min(1).max(65535),
enabled: z.boolean().default(true),
hcEnabled: z.boolean().optional(),
hcPath: z.string().min(1).optional().nullable(),
hcScheme: z.string().optional().nullable(),
hcMode: z.string().optional().nullable(),
hcHostname: z.string().optional().nullable(),
hcPort: z.int().positive().optional().nullable(),
hcInterval: z.int().positive().min(5).optional().nullable(),
hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(),
hcTimeout: z.int().positive().min(1).optional().nullable(),
hcHeaders: z
.array(z.strictObject({ name: z.string(), value: z.string() }))
.nullable()
.optional(),
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
rewritePath: z.string().optional().nullable(),
rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable(),
priority: z.int().min(1).max(1000).optional().nullable()
});
export type CreateTargetResponse = Target & TargetHealthCheck;
@@ -159,7 +149,9 @@ export async function createTarget(
if (existingTarget) {
// log a warning
logger.warn(`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}`);
logger.warn(
`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}`
);
}
let newTarget: Target[] = [];

View File

@@ -14,8 +14,8 @@ import { getAllowedIps } from "./helpers";
import { OpenAPITags, registry } from "@server/openApi";
const deleteTargetSchema = z.strictObject({
targetId: z.string().transform(Number).pipe(z.int().positive())
});
targetId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
method: "delete",

View File

@@ -11,12 +11,13 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const getTargetSchema = z.strictObject({
targetId: z.string().transform(Number).pipe(z.int().positive())
});
targetId: z.string().transform(Number).pipe(z.int().positive())
});
type GetTargetResponse = Target & Omit<TargetHealthCheck, 'hcHeaders'> & {
hcHeaders: { name: string; value: string; }[] | null;
};
type GetTargetResponse = Target &
Omit<TargetHealthCheck, "hcHeaders"> & {
hcHeaders: { name: string; value: string }[] | null;
};
registry.registerPath({
method: "get",

View File

@@ -30,7 +30,9 @@ interface HealthcheckStatusMessage {
targets: Record<string, TargetHealthStatus>;
}
export const handleHealthcheckStatusMessage: MessageHandler = async (context) => {
export const handleHealthcheckStatusMessage: MessageHandler = async (
context
) => {
const { message, client: c } = context;
const newt = c as Newt;
@@ -59,7 +61,9 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) =>
// Process each target status update
for (const [targetId, healthStatus] of Object.entries(data.targets)) {
logger.debug(`Processing health status for target ${targetId}: ${healthStatus.status}${healthStatus.lastError ? ` (${healthStatus.lastError})` : ''}`);
logger.debug(
`Processing health status for target ${targetId}: ${healthStatus.status}${healthStatus.lastError ? ` (${healthStatus.lastError})` : ""}`
);
// Verify the target belongs to this newt's site before updating
// This prevents unauthorized updates to targets from other sites
@@ -76,7 +80,10 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) =>
siteId: targets.siteId
})
.from(targets)
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
.innerJoin(
resources,
eq(targets.resourceId, resources.resourceId)
)
.innerJoin(sites, eq(targets.siteId, sites.siteId))
.where(
and(
@@ -87,7 +94,9 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) =>
.limit(1);
if (!targetCheck) {
logger.warn(`Target ${targetId} not found or does not belong to site ${newt.siteId}`);
logger.warn(
`Target ${targetId} not found or does not belong to site ${newt.siteId}`
);
errorCount++;
continue;
}
@@ -101,11 +110,15 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) =>
.where(eq(targetHealthCheck.targetId, targetIdNum))
.execute();
logger.debug(`Updated health status for target ${targetId} to ${healthStatus.status}`);
logger.debug(
`Updated health status for target ${targetId} to ${healthStatus.status}`
);
successCount++;
}
logger.debug(`Health status update complete: ${successCount} successful, ${errorCount} errors out of ${Object.keys(data.targets).length} targets`);
logger.debug(
`Health status update complete: ${successCount} successful, ${errorCount} errors out of ${Object.keys(data.targets).length} targets`
);
} catch (error) {
logger.error("Error processing healthcheck status message:", error);
}

View File

@@ -4,7 +4,10 @@ import { eq } from "drizzle-orm";
const currentBannedPorts: number[] = [];
export async function pickPort(siteId: number, trx: Transaction | typeof db): Promise<{
export async function pickPort(
siteId: number,
trx: Transaction | typeof db
): Promise<{
internalPort: number;
targetIps: string[];
}> {

View File

@@ -11,11 +11,8 @@ import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
const listTargetsParamsSchema = z.strictObject({
resourceId: z
.string()
.transform(Number)
.pipe(z.int().positive())
});
resourceId: z.string().transform(Number).pipe(z.int().positive())
});
const listTargetsSchema = z.object({
limit: z
@@ -62,7 +59,7 @@ function queryTargets(resourceId: number) {
pathMatchType: targets.pathMatchType,
rewritePath: targets.rewritePath,
rewritePathType: targets.rewritePathType,
priority: targets.priority,
priority: targets.priority
})
.from(targets)
.leftJoin(sites, eq(sites.siteId, targets.siteId))
@@ -75,8 +72,11 @@ function queryTargets(resourceId: number) {
return baseQuery;
}
type TargetWithParsedHeaders = Omit<Awaited<ReturnType<typeof queryTargets>>[0], 'hcHeaders'> & {
hcHeaders: { name: string; value: string; }[] | null;
type TargetWithParsedHeaders = Omit<
Awaited<ReturnType<typeof queryTargets>>[0],
"hcHeaders"
> & {
hcHeaders: { name: string; value: string }[] | null;
};
export type ListTargetsResponse = {
@@ -136,7 +136,7 @@ export async function listTargets(
const totalCount = totalCountResult[0].count;
// Parse hcHeaders from JSON string back to array for each target
const parsedTargetsList = targetsList.map(target => {
const parsedTargetsList = targetsList.map((target) => {
let parsedHcHeaders = null;
if (target.hcHeaders) {
try {

View File

@@ -16,10 +16,11 @@ import { OpenAPITags, registry } from "@server/openApi";
import { vs } from "@react-email/components";
const updateTargetParamsSchema = z.strictObject({
targetId: z.string().transform(Number).pipe(z.int().positive())
});
targetId: z.string().transform(Number).pipe(z.int().positive())
});
const updateTargetBodySchema = z.strictObject({
const updateTargetBodySchema = z
.strictObject({
siteId: z.int().positive(),
ip: z.string().refine(isTargetValid),
method: z.string().min(1).max(10).optional().nullable(),
@@ -32,22 +33,27 @@ const updateTargetBodySchema = z.strictObject({
hcHostname: z.string().optional().nullable(),
hcPort: z.int().positive().optional().nullable(),
hcInterval: z.int().positive().min(5).optional().nullable(),
hcUnhealthyInterval: z.int()
.positive()
.min(5)
.optional()
.nullable(),
hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(),
hcTimeout: z.int().positive().min(1).optional().nullable(),
hcHeaders: z.array(z.strictObject({ name: z.string(), value: z.string() })).nullable().optional(),
hcHeaders: z
.array(z.strictObject({ name: z.string(), value: z.string() }))
.nullable()
.optional(),
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
pathMatchType: z
.enum(["exact", "prefix", "regex"])
.optional()
.nullable(),
rewritePath: z.string().optional().nullable(),
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
priority: z.int().min(1).max(1000).optional(),
rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable(),
priority: z.int().min(1).max(1000).optional()
})
.refine((data) => Object.keys(data).length > 0, {
error: "At least one field must be provided for update"
@@ -166,7 +172,9 @@ export async function updateTarget(
if (foundTarget) {
// log a warning
logger.warn(`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${target.resourceId}`);
logger.warn(
`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${target.resourceId}`
);
}
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
@@ -205,9 +213,11 @@ export async function updateTarget(
// When health check is disabled, reset hcHealth to "unknown"
// to prevent previously unhealthy targets from being excluded
const hcHealthValue = (parsedBody.data.hcEnabled === false || parsedBody.data.hcEnabled === null)
? "unknown"
: undefined;
const hcHealthValue =
parsedBody.data.hcEnabled === false ||
parsedBody.data.hcEnabled === null
? "unknown"
: undefined;
const [updatedHc] = await db
.update(targetHealthCheck)