mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-20 11:56:38 +00:00
Merge dev into fix/log-analytics-adjustments
This commit is contained in:
@@ -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[] = [];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user