Keep standalone agent machines out of the Virtualization Hosts thresholds list

A machine monitored only by the Pulse agent was listed under Alerts,
Thresholds, Virtualization Hosts as well as under Machines, because the
section was fed by every resource of type `agent` rather than by
provider-owned virtualization nodes.

The two rows do not share an override identity. The Machines row resolves
through `hostOverrideIdCandidates`, which prefers the agent-derived id, while
the Virtualization Hosts row reads and writes `overridesMap.get(node.id)` on
the bare resource id. A custom threshold saved from the Virtualization Hosts
row therefore landed under a key agent alerting never reads, so the machine
kept alerting on the global default with no visible reason.

The section now takes `virtualizationHostResources`, which excludes standalone
Pulse-agent resources via the existing `isPulseAgentPlatformResource`
classifier already used by the standalone page, platform navigation and the
resource drawer. Provider-owned nodes are unaffected, since they carry
provider-owner platform evidence. Those machines lose nothing: they keep their
Machines row, which is the one alerting honours.

Reported via support by a customer whose cloud host kept firing at the 85%
global agent memory limit despite a 100% override saved on the wrong row.
This commit is contained in:
rcourtman
2026-07-24 21:15:49 +01:00
parent d82d3bd627
commit 2118e33459
4 changed files with 63 additions and 2 deletions
@@ -88,7 +88,7 @@ export function AlertsConfigurationSurface(props: AlertsConfigurationSurfaceProp
pbsInstances={state.pbsInstances()} pbsInstances={state.pbsInstances()}
pmgInstances={state.pmgInstances()} pmgInstances={state.pmgInstances()}
containerRuntimes={state.containerRuntimeResources()} containerRuntimes={state.containerRuntimeResources()}
nodes={props.byType('agent')} nodes={state.virtualizationHostResources()}
agents={state.agentResources()} agents={state.agentResources()}
storage={props storage={props
.allResources() .allResources()
@@ -62,6 +62,53 @@ describe('useAlertOverridesState', () => {
expect(overviewOverrides()).toEqual(result.overrides()); expect(overviewOverrides()).toEqual(result.overrides());
}); });
it('keeps standalone agent machines out of the virtualization host list while retaining provider-owned nodes', () => {
const [hasUnsavedChanges] = createSignal(false);
const [, setOverviewOverrides] = createSignal([]);
const resources = [
// A cloud box running only the Pulse agent. It belongs to the Machines tab,
// whose overrides resolve by agent identity, so a second row here would save
// an override that agent alerting never reads.
makeResource({
id: 'docker-host-gcp-01',
name: 'docker-host-gcp-01',
type: 'agent',
platformType: 'agent',
sourceType: 'agent',
platformData: {
agent: { agentId: 'agent-gcp-01', platform: 'Linux' },
},
}),
// A real Proxmox node must still appear under Virtualization Hosts.
makeResource({
id: 'cluster-a:pve01',
name: 'pve01',
type: 'agent',
platformType: 'proxmox-pve',
sourceType: 'api',
platformData: {
proxmox: { node: 'pve01', instance: 'cluster-a' },
},
}),
];
const { result } = renderHook(() =>
useAlertOverridesState({
allResources: () => resources,
byType: (resourceType) => resources.filter((resource) => resource.type === resourceType),
children: () => [],
hasUnsavedChanges,
setOverviewOverrides,
}),
);
expect(result.virtualizationHostResources().map((resource) => resource.id)).toEqual([
'cluster-a:pve01',
]);
// The standalone machine is still reachable, via the Machines tab.
expect(result.agentResources().map((resource) => resource.id)).toContain('docker-host-gcp-01');
});
it('projects guest overrides without agent resources and clears stale overrides when config is emptied', async () => { it('projects guest overrides without agent resources and clears stale overrides when config is emptied', async () => {
const [hasUnsavedChanges] = createSignal(false); const [hasUnsavedChanges] = createSignal(false);
const [overviewOverrides, setOverviewOverrides] = createSignal([]); const [overviewOverrides, setOverviewOverrides] = createSignal([]);
@@ -3,7 +3,10 @@ import { createEffect, createMemo, createSignal, type Accessor } from 'solid-js'
import type { PBSInstance, PMGInstance } from '@/types/api'; import type { PBSInstance, PMGInstance } from '@/types/api';
import type { RawOverrideConfig } from '@/types/alerts'; import type { RawOverrideConfig } from '@/types/alerts';
import type { Resource, ResourceType } from '@/types/resource'; import type { Resource, ResourceType } from '@/types/resource';
import { isAgentFacetInfrastructureResource } from '@/utils/agentResources'; import {
isAgentFacetInfrastructureResource,
isPulseAgentPlatformResource,
} from '@/utils/agentResources';
import { pbsInstanceFromResource, pmgInstanceFromResource } from '@/utils/resourceStateAdapters'; import { pbsInstanceFromResource, pmgInstanceFromResource } from '@/utils/resourceStateAdapters';
import { import {
@@ -48,6 +51,15 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
props.allResources().filter((resource) => isAgentFacetInfrastructureResource(resource)), props.allResources().filter((resource) => isAgentFacetInfrastructureResource(resource)),
); );
// Standalone Pulse-agent machines are not virtualization hosts. They own a row
// in the Machines tab, which resolves overrides by agent identity. Leaving them
// in the Virtualization Hosts list gave the same machine a second row that saved
// overrides under the bare resource id, an identity agent alerting never reads,
// so the override silently did nothing.
const virtualizationHostResources = createMemo(() =>
props.byType('agent').filter((resource) => !isPulseAgentPlatformResource(resource)),
);
const pbsInstances = createMemo<PBSInstance[]>(() => const pbsInstances = createMemo<PBSInstance[]>(() =>
props props
.allResources() .allResources()
@@ -157,6 +169,7 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
replaceRawOverridesConfig, replaceRawOverridesConfig,
allGuests, allGuests,
agentResources, agentResources,
virtualizationHostResources,
containerRuntimeResources, containerRuntimeResources,
pbsInstances, pbsInstances,
pmgInstances, pmgInstances,
@@ -140,6 +140,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
...configurationSnapshotState, ...configurationSnapshotState,
allGuests: overridesState.allGuests, allGuests: overridesState.allGuests,
agentResources: overridesState.agentResources, agentResources: overridesState.agentResources,
virtualizationHostResources: overridesState.virtualizationHostResources,
containerRuntimeResources: overridesState.containerRuntimeResources, containerRuntimeResources: overridesState.containerRuntimeResources,
pbsInstances: overridesState.pbsInstances, pbsInstances: overridesState.pbsInstances,
pmgInstances: overridesState.pmgInstances, pmgInstances: overridesState.pmgInstances,