This commit is contained in:
Owen
2025-10-04 18:36:44 -07:00
parent 3123f858bb
commit c2c907852d
320 changed files with 35785 additions and 2984 deletions

View File

@@ -69,9 +69,16 @@ export async function applyBlueprint(
`Updating target ${target.targetId} on site ${site.sites.siteId}`
);
// see if you can find a matching target health check from the healthchecksToUpdate array
const matchingHealthcheck =
result.healthchecksToUpdate.find(
(hc) => hc.targetId === target.targetId
);
await addProxyTargets(
site.newt.newtId,
[target],
matchingHealthcheck ? [matchingHealthcheck] : [],
result.proxyResource.protocol,
result.proxyResource.proxyPort
);

View File

@@ -8,6 +8,8 @@ import {
roleResources,
roles,
Target,
TargetHealthCheck,
targetHealthCheck,
Transaction,
userOrgs,
userResources,
@@ -22,6 +24,7 @@ import {
TargetData
} from "./types";
import logger from "@server/logger";
import { createCertificate } from "@server/routers/private/certificates/createCertificate";
import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password";
@@ -30,6 +33,7 @@ import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
export type ProxyResourcesResults = {
proxyResource: Resource;
targetsToUpdate: Target[];
healthchecksToUpdate: TargetHealthCheck[];
}[];
export async function updateProxyResources(
@@ -43,7 +47,8 @@ export async function updateProxyResources(
for (const [resourceNiceId, resourceData] of Object.entries(
config["proxy-resources"]
)) {
const targetsToUpdate: Target[] = [];
let targetsToUpdate: Target[] = [];
let healthchecksToUpdate: TargetHealthCheck[] = [];
let resource: Resource;
async function createTarget( // reusable function to create a target
@@ -114,6 +119,33 @@ export async function updateProxyResources(
.returning();
targetsToUpdate.push(newTarget);
const healthcheckData = targetData.healthcheck;
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null;
const [newHealthcheck] = await trx
.insert(targetHealthCheck)
.values({
targetId: newTarget.targetId,
hcEnabled: healthcheckData?.enabled || false,
hcPath: healthcheckData?.path,
hcScheme: healthcheckData?.scheme,
hcMode: healthcheckData?.mode,
hcHostname: healthcheckData?.hostname,
hcPort: healthcheckData?.port,
hcInterval: healthcheckData?.interval,
hcUnhealthyInterval: healthcheckData?.unhealthyInterval,
hcTimeout: healthcheckData?.timeout,
hcHeaders: hcHeaders,
hcFollowRedirects: healthcheckData?.followRedirects,
hcMethod: healthcheckData?.method,
hcStatus: healthcheckData?.status,
hcHealth: "unknown"
})
.returning();
healthchecksToUpdate.push(newHealthcheck);
}
// Find existing resource by niceId and orgId
@@ -360,6 +392,64 @@ export async function updateProxyResources(
targetsToUpdate.push(finalUpdatedTarget);
}
const healthcheckData = targetData.healthcheck;
const [oldHealthcheck] = await trx
.select()
.from(targetHealthCheck)
.where(
eq(
targetHealthCheck.targetId,
existingTarget.targetId
)
)
.limit(1);
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null;
const [newHealthcheck] = await trx
.update(targetHealthCheck)
.set({
hcEnabled: healthcheckData?.enabled || false,
hcPath: healthcheckData?.path,
hcScheme: healthcheckData?.scheme,
hcMode: healthcheckData?.mode,
hcHostname: healthcheckData?.hostname,
hcPort: healthcheckData?.port,
hcInterval: healthcheckData?.interval,
hcUnhealthyInterval:
healthcheckData?.unhealthyInterval,
hcTimeout: healthcheckData?.timeout,
hcHeaders: hcHeaders,
hcFollowRedirects: healthcheckData?.followRedirects,
hcMethod: healthcheckData?.method,
hcStatus: healthcheckData?.status
})
.where(
eq(
targetHealthCheck.targetId,
existingTarget.targetId
)
)
.returning();
if (
checkIfHealthcheckChanged(
oldHealthcheck,
newHealthcheck
)
) {
healthchecksToUpdate.push(newHealthcheck);
// if the target is not already in the targetsToUpdate array, add it
if (
!targetsToUpdate.find(
(t) => t.targetId === updatedTarget.targetId
)
) {
targetsToUpdate.push(updatedTarget);
}
}
} else {
await createTarget(existingResource.resourceId, targetData);
}
@@ -573,7 +663,8 @@ export async function updateProxyResources(
results.push({
proxyResource: resource,
targetsToUpdate
targetsToUpdate,
healthchecksToUpdate
});
}
@@ -783,6 +874,36 @@ async function syncWhitelistUsers(
}
}
function checkIfHealthcheckChanged(
existing: TargetHealthCheck | undefined,
incoming: TargetHealthCheck | undefined
) {
if (!existing && incoming) return true;
if (existing && !incoming) return true;
if (!existing || !incoming) return false;
if (existing.hcEnabled !== incoming.hcEnabled) return true;
if (existing.hcPath !== incoming.hcPath) return true;
if (existing.hcScheme !== incoming.hcScheme) return true;
if (existing.hcMode !== incoming.hcMode) return true;
if (existing.hcHostname !== incoming.hcHostname) return true;
if (existing.hcPort !== incoming.hcPort) return true;
if (existing.hcInterval !== incoming.hcInterval) return true;
if (existing.hcUnhealthyInterval !== incoming.hcUnhealthyInterval)
return true;
if (existing.hcTimeout !== incoming.hcTimeout) return true;
if (existing.hcFollowRedirects !== incoming.hcFollowRedirects) return true;
if (existing.hcMethod !== incoming.hcMethod) return true;
if (existing.hcStatus !== incoming.hcStatus) return true;
if (
JSON.stringify(existing.hcHeaders) !==
JSON.stringify(incoming.hcHeaders)
)
return true;
return false;
}
function checkIfTargetChanged(
existing: Target | undefined,
incoming: Target | undefined
@@ -832,6 +953,8 @@ async function getDomain(
);
}
await createCertificate(domain.domainId, fullDomain, trx);
return domain;
}

View File

@@ -5,6 +5,22 @@ export const SiteSchema = z.object({
"docker-socket-enabled": z.boolean().optional().default(true)
});
export const TargetHealthCheckSchema = z.object({
hostname: z.string(),
port: z.number().int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
path: z.string().optional(),
scheme: z.string().optional(),
mode: z.string().default("http"),
interval: z.number().int().default(30),
unhealthyInterval: z.number().int().default(30),
timeout: z.number().int().default(5),
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null),
followRedirects: z.boolean().default(true),
method: z.string().default("GET"),
status: z.number().int().optional()
});
// Schema for individual target within a resource
export const TargetSchema = z.object({
site: z.string().optional(),
@@ -15,6 +31,7 @@ export const TargetSchema = z.object({
"internal-port": z.number().int().min(1).max(65535).optional(),
path: z.string().optional(),
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(),
healthcheck: TargetHealthCheckSchema.optional(),
rewritePath: z.string().optional(),
"rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable()
});