Working on newt compat

This commit is contained in:
Owen
2026-04-21 09:47:20 -07:00
parent b59262b7af
commit ed327626bb
5 changed files with 143 additions and 40 deletions

View File

@@ -21,6 +21,7 @@ import {
generateSubnetProxyTargetV2, generateSubnetProxyTargetV2,
SubnetProxyTargetV2 SubnetProxyTargetV2
} from "@server/lib/ip"; } from "@server/lib/ip";
import { supportsTargetHealthChecksV2 } from "./targets";
export async function buildClientConfigurationForNewtClient( export async function buildClientConfigurationForNewtClient(
site: Site, site: Site,
@@ -86,7 +87,8 @@ export async function buildClientConfigurationForNewtClient(
// ) // )
// ); // );
if (!client.clientSitesAssociationsCache.isJitMode) { // if we are adding sites through jit then dont add the site to the olm if (!client.clientSitesAssociationsCache.isJitMode) {
// if we are adding sites through jit then dont add the site to the olm
// update the peer info on the olm // update the peer info on the olm
// if the peer has not been added yet this will be a no-op // if the peer has not been added yet this will be a no-op
await updatePeer(client.clients.clientId, { await updatePeer(client.clients.clientId, {
@@ -189,7 +191,10 @@ export async function buildClientConfigurationForNewtClient(
}; };
} }
export async function buildTargetConfigurationForNewtClient(siteId: number) { export async function buildTargetConfigurationForNewtClient(
siteId: number,
version?: string | null
) {
// Get all enabled targets with their resource protocol information // Get all enabled targets with their resource protocol information
const allTargets = await db const allTargets = await db
.select({ .select({
@@ -201,7 +206,7 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
internalPort: targets.internalPort, internalPort: targets.internalPort,
enabled: targets.enabled, enabled: targets.enabled,
protocol: resources.protocol, protocol: resources.protocol,
hcId: targetHealthCheck.targetHealthCheckId, targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
hcEnabled: targetHealthCheck.hcEnabled, hcEnabled: targetHealthCheck.hcEnabled,
hcPath: targetHealthCheck.hcPath, hcPath: targetHealthCheck.hcPath,
hcScheme: targetHealthCheck.hcScheme, hcScheme: targetHealthCheck.hcScheme,
@@ -273,8 +278,9 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
} }
return { return {
id: target.targetId, id: supportsTargetHealthChecksV2(version)
hcId: target.hcId, ? target.targetId
: target.targetHealthCheckId,
hcEnabled: target.hcEnabled, hcEnabled: target.hcEnabled,
hcPath: target.hcPath, hcPath: target.hcPath,
hcScheme: target.hcScheme, hcScheme: target.hcScheme,

View File

@@ -192,7 +192,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
} }
const { tcpTargets, udpTargets, validHealthCheckTargets } = const { tcpTargets, udpTargets, validHealthCheckTargets } =
await buildTargetConfigurationForNewtClient(siteId); await buildTargetConfigurationForNewtClient(siteId, newtVersion);
logger.debug( logger.debug(
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`

View File

@@ -2,6 +2,13 @@ import { Target, TargetHealthCheck } from "@server/db";
import { sendToClient } from "#dynamic/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger"; import logger from "@server/logger";
import { canCompress } from "@server/lib/clientVersionChecks"; import { canCompress } from "@server/lib/clientVersionChecks";
import semver from "semver";
const NEWT_V2_TARGET_HEALTH_CHECK_VERSION = ">=1.12.0";
export function supportsTargetHealthChecksV2(version?: string | null) {
return version ? semver.satisfies(version, NEWT_V2_TARGET_HEALTH_CHECK_VERSION) : false;
}
export async function addTargets( export async function addTargets(
newtId: string, newtId: string,
@@ -83,8 +90,7 @@ export async function addTargets(
} }
return { return {
id: target.targetId, id: supportsTargetHealthChecksV2(version) ? target.targetId : hc.targetHealthCheckId,
hcId: hc.targetHealthCheckId,
hcEnabled: hc.hcEnabled, hcEnabled: hc.hcEnabled,
hcPath: hc.hcPath, hcPath: hc.hcPath,
hcScheme: hc.hcScheme, hcScheme: hc.hcScheme,

View File

@@ -14,6 +14,7 @@ import {
fireHealthCheckHealthyAlert, fireHealthCheckHealthyAlert,
fireHealthCheckNotHealthyAlert fireHealthCheckNotHealthyAlert
} from "#dynamic/lib/alerts"; } from "#dynamic/lib/alerts";
import { supportsTargetHealthChecksV2 } from "@server/routers/newt/targets";
interface TargetHealthStatus { interface TargetHealthStatus {
status: string; status: string;
@@ -73,6 +74,8 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
let successCount = 0; let successCount = 0;
let errorCount = 0; let errorCount = 0;
const isV2 = supportsTargetHealthChecksV2(newt.version);
// Process each target status update // Process each target status update
for (const [targetId, healthStatus] of Object.entries(data.targets)) { for (const [targetId, healthStatus] of Object.entries(data.targets)) {
logger.debug( logger.debug(
@@ -88,34 +91,78 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
continue; continue;
} }
const [targetCheck] = await db let targetCheck: {
.select({ targetId: number;
targetId: targets.targetId, siteId: number | null;
siteId: targets.siteId, orgId: string | null;
orgId: targetHealthCheck.orgId, targetHealthCheckId: number;
targetHealthCheckId: targetHealthCheck.targetHealthCheckId, resourceOrgId: string | null;
resourceOrgId: resources.orgId, resourceId: number | null;
resourceId: resources.resourceId, name: string | null;
name: targetHealthCheck.name, hcStatus: string | null;
hcStatus: targetHealthCheck.hcHealth } | undefined;
})
.from(targets) if (isV2) {
.innerJoin( // New newt (>= 1.12.0): the key is the targetId
resources, [targetCheck] = await db
eq(targets.resourceId, resources.resourceId) .select({
) targetId: targets.targetId,
.innerJoin(sites, eq(targets.siteId, sites.siteId)) siteId: targets.siteId,
.innerJoin( orgId: targetHealthCheck.orgId,
targetHealthCheck, targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
eq(targets.targetId, targetHealthCheck.targetId) resourceOrgId: resources.orgId,
) resourceId: resources.resourceId,
.where( name: targetHealthCheck.name,
and( hcStatus: targetHealthCheck.hcHealth
eq(targets.targetId, targetIdNum), })
eq(sites.siteId, newt.siteId) .from(targets)
.innerJoin(
resources,
eq(targets.resourceId, resources.resourceId)
) )
) .innerJoin(sites, eq(targets.siteId, sites.siteId))
.limit(1); .innerJoin(
targetHealthCheck,
eq(targets.targetId, targetHealthCheck.targetId)
)
.where(
and(
eq(targets.targetId, targetIdNum),
eq(sites.siteId, newt.siteId)
)
)
.limit(1);
} else {
// Old newt (< 1.12.0): the key is the targetHealthCheckId
[targetCheck] = await db
.select({
targetId: targets.targetId,
siteId: targets.siteId,
orgId: targetHealthCheck.orgId,
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
resourceOrgId: resources.orgId,
resourceId: resources.resourceId,
name: targetHealthCheck.name,
hcStatus: targetHealthCheck.hcHealth
})
.from(targetHealthCheck)
.innerJoin(
targets,
eq(targetHealthCheck.targetId, targets.targetId)
)
.innerJoin(
resources,
eq(targets.resourceId, resources.resourceId)
)
.innerJoin(sites, eq(targets.siteId, sites.siteId))
.where(
and(
eq(targetHealthCheck.targetHealthCheckId, targetIdNum),
eq(sites.siteId, newt.siteId)
)
)
.limit(1);
}
if (!targetCheck) { if (!targetCheck) {
logger.warn( logger.warn(
@@ -142,7 +189,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
| "healthy" | "healthy"
| "unhealthy" | "unhealthy"
}) })
.where(eq(targetHealthCheck.targetId, targetIdNum)); .where(eq(targetHealthCheck.targetId, targetCheck.targetId));
// Log the state change to status history // Log the state change to status history
await db.insert(statusHistory).values({ await db.insert(statusHistory).values({
@@ -170,7 +217,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
.where( .where(
and( and(
eq(targets.resourceId, targetCheck.resourceId), eq(targets.resourceId, targetCheck.resourceId),
eq(targets.targetId, targetIdNum) // only check the other targets, not the one we just updated eq(targets.targetId, targetCheck.targetId) // only check the other targets, not the one we just updated
) )
); );

View File

@@ -46,7 +46,7 @@ import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { ChevronsUpDown, Plus, Trash2 } from "lucide-react"; import { ChevronsUpDown, Plus, Trash2 } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import type { Control, UseFormReturn } from "react-hook-form"; import type { Control, UseFormReturn } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
@@ -484,8 +484,8 @@ function NotifyActionFields({
number | null number | null
>(null); >(null);
const { data: orgUsers = [] } = useQuery(orgQueries.users({ orgId })); const { data: orgUsers = [], isLoading: isLoadingUsers } = useQuery(orgQueries.users({ orgId }));
const { data: orgRoles = [] } = useQuery(orgQueries.roles({ orgId })); const { data: orgRoles = [], isLoading: isLoadingRoles } = useQuery(orgQueries.roles({ orgId }));
const allUsers = useMemo( const allUsers = useMemo(
() => () =>
@@ -508,6 +508,50 @@ function NotifyActionFields({
[orgRoles] [orgRoles]
); );
const hasResolvedTagsRef = useRef(false);
useEffect(() => {
if (isLoadingUsers || isLoadingRoles) return;
if (hasResolvedTagsRef.current) return;
const currentUserTags = form.getValues(
`actions.${index}.userTags`
) as Tag[];
const currentRoleTags = form.getValues(
`actions.${index}.roleTags`
) as Tag[];
const resolvedUserTags = currentUserTags.map((tag) => {
const match = allUsers.find((u) => u.id === tag.id);
return match ? { id: tag.id, text: match.text } : tag;
});
const resolvedRoleTags = currentRoleTags.map((tag) => {
const match = allRoles.find((r) => r.id === tag.id);
return match ? { id: tag.id, text: match.text } : tag;
});
const userTagsNeedUpdate = resolvedUserTags.some(
(t, i) => t.text !== currentUserTags[i]?.text
);
const roleTagsNeedUpdate = resolvedRoleTags.some(
(t, i) => t.text !== currentRoleTags[i]?.text
);
if (userTagsNeedUpdate) {
form.setValue(`actions.${index}.userTags`, resolvedUserTags, {
shouldDirty: false
});
}
if (roleTagsNeedUpdate) {
form.setValue(`actions.${index}.roleTags`, resolvedRoleTags, {
shouldDirty: false
});
}
hasResolvedTagsRef.current = true;
}, [isLoadingUsers, isLoadingRoles, allUsers, allRoles]);
const userTags = (useWatch({ control, name: `actions.${index}.userTags` }) ?? []) as Tag[]; const userTags = (useWatch({ control, name: `actions.${index}.userTags` }) ?? []) as Tag[];
const roleTags = (useWatch({ control, name: `actions.${index}.roleTags` }) ?? []) as Tag[]; const roleTags = (useWatch({ control, name: `actions.${index}.roleTags` }) ?? []) as Tag[];
const emailTags = (useWatch({ control, name: `actions.${index}.emailTags` }) ?? []) as Tag[]; const emailTags = (useWatch({ control, name: `actions.${index}.emailTags` }) ?? []) as Tag[];