mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-22 23:43:30 +00:00
Hand off host-local uninstall commands for removed agents in Agent Doctor
Removed targets previously dead-ended with no next step. Their row expansion now offers the host-local uninstall command, resolved from the retained diagnostic identity via the strict platform resolver, or both labeled Linux-family and Windows commands when the platform is unknown (the current wire reality, since removed records retain no platform). The handoff carries the diagnostic's agent id and hostname as uninstall identity flags and states the command runs on the affected host, not from Pulse. The governed uninstall builders now accept a narrowed AgentUninstallIdentity view so surfaces without a ledger-backed inventory row reuse the same transport.
This commit is contained in:
parent
c87ad1cf26
commit
4fc86ec187
6 changed files with 210 additions and 10 deletions
|
|
@ -1990,8 +1990,19 @@ only workload telemetry, e.g. Docker-only or Kubernetes-only agents) are now
|
|||
appended as diagnostics-only targets, honoring the scoped-agent filter, so a
|
||||
critical workload-only agent can no longer vanish from the fleet view.
|
||||
Diagnostics-only rows render the diagnostic's status, reasons, and evidence
|
||||
but offer no host-local command (there is no ledger connection to derive one
|
||||
from). Page presentation follows the same honesty rule: the
|
||||
but offer no host-local update command (there is no ledger connection to
|
||||
derive an update from). Removed rows are the deliberate exception in the
|
||||
uninstall direction: a removed agent's one remaining next step is host-side
|
||||
cleanup, so a removed target resolves its command platform from the retained
|
||||
diagnostic identity through the strict platform resolver and its row
|
||||
expansion hands off the host-local uninstall command
|
||||
(`getInfrastructureAgentDoctorUninstallHandoff`) — the single matching
|
||||
platform command when the identity resolves, or both explicitly labeled
|
||||
Linux-family and Windows commands when it does not, never one guessed
|
||||
executable. The handoff carries the diagnostic's agent id and hostname as
|
||||
uninstall identity flags, and its copy states the command runs on the
|
||||
affected host itself, not from Pulse. Page presentation follows the same
|
||||
honesty rule: the
|
||||
"Target" column renders a version only when one is actually published, the
|
||||
host-local command explainer renders only when at least one row offers a
|
||||
command, and the summary chips list only non-zero status counts. The summary
|
||||
|
|
@ -2001,8 +2012,8 @@ also offers plain-text diagnostic reports (fleet-level over the currently
|
|||
visible rows and per-agent from a row expansion) via
|
||||
`formatInfrastructureAgentDoctorReport`; reports carry status, versions,
|
||||
last-seen, reasons, identity evidence, and non-command repair actions, and
|
||||
must never embed host-local update commands because those can carry install
|
||||
tokens.
|
||||
must never embed host-local update or uninstall commands because those can
|
||||
carry install tokens.
|
||||
|
||||
### Governed action readiness remains outside agent lifecycle authority
|
||||
|
||||
|
|
|
|||
|
|
@ -5972,6 +5972,15 @@ canonical identity when inventory already has it: shell uninstall payloads must
|
|||
carry `--agent-id`, and PowerShell uninstall payloads must carry
|
||||
`PULSE_AGENT_ID`, so deregistration targets the intended governed agent record
|
||||
instead of depending on local fallback files or hostname lookup.
|
||||
That uninstall identity seam is deliberately narrower than the unified
|
||||
inventory row: the governed uninstall builders in
|
||||
`frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
|
||||
accept a caller-supplied identity view carrying only
|
||||
`agentActionId`/`agentId`/`hostname` (the exported `AgentUninstallIdentity`
|
||||
pick), so lifecycle surfaces without a ledger-backed inventory row — Agent
|
||||
Doctor's removed-agent diagnostics handoff — bind copied uninstall payloads to
|
||||
the retained diagnostic identity through the same governed builders instead of
|
||||
inventing a second uninstall transport.
|
||||
The same identity-preservation contract applies to copied upgrade transport:
|
||||
shell upgrade payloads must carry `--agent-id` and `--hostname`, and
|
||||
PowerShell upgrade payloads must carry `PULSE_AGENT_ID` and `PULSE_HOSTNAME`,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@/utils/unifiedAgentInventoryPresentation';
|
||||
import {
|
||||
formatInfrastructureAgentDoctorReport,
|
||||
getInfrastructureAgentDoctorUninstallHandoff,
|
||||
summarizeInfrastructureAgentDoctorTargets,
|
||||
type InfrastructureAgentDoctorStatus,
|
||||
type InfrastructureAgentDoctorTarget,
|
||||
|
|
@ -373,6 +374,8 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
|
|||
(target.diagnostic?.repairActions ?? []).filter(
|
||||
(action) => action.code !== 'copy_upgrade_command',
|
||||
);
|
||||
const uninstallHandoff = () =>
|
||||
getInfrastructureAgentDoctorUninstallHandoff(target);
|
||||
const expanded = () => isExpanded(target);
|
||||
|
||||
return (
|
||||
|
|
@ -526,6 +529,50 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
|
|||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show when={uninstallHandoff()}>
|
||||
{(handoff) => (
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs text-muted">
|
||||
This agent was removed from Pulse, but the agent software may
|
||||
still be installed on its host. Finish detaching it by running
|
||||
the uninstall command on the affected host itself. Pulse does
|
||||
not run commands remotely.
|
||||
</p>
|
||||
<For each={handoff().commands}>
|
||||
{(entry) => (
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted">
|
||||
{entry.label}
|
||||
</div>
|
||||
<div class="relative">
|
||||
<CommandCopyButton
|
||||
onClick={() =>
|
||||
void copyCommand(
|
||||
operations.getPlatformUninstallCommand(
|
||||
entry.platform,
|
||||
handoff().identity,
|
||||
),
|
||||
)
|
||||
}
|
||||
title="Copy host-local agent uninstall command"
|
||||
label={`Copy ${entry.label} uninstall command for ${target.displayName}`}
|
||||
/>
|
||||
<pre class="overflow-x-auto rounded-md bg-base p-3 pr-12 text-xs text-base-content">
|
||||
<code>
|
||||
{operations.getPlatformUninstallCommand(
|
||||
entry.platform,
|
||||
handoff().identity,
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
collectInfrastructureAgentDoctorTargets,
|
||||
diagnosticConnectionID,
|
||||
formatInfrastructureAgentDoctorReport,
|
||||
getInfrastructureAgentDoctorUninstallHandoff,
|
||||
resolveKnownAgentCommandPlatform,
|
||||
type InfrastructureAgentDoctorTarget,
|
||||
} from '../infrastructureAgentUpdateCommandsModel';
|
||||
|
|
@ -278,6 +279,90 @@ describe('Agent Doctor model', () => {
|
|||
).not.toContain('removed');
|
||||
});
|
||||
|
||||
it('hands removed targets an uninstall command scoped to their reported platform', () => {
|
||||
const removedWindows = diagnosticFixture({
|
||||
connectionId: 'agent:removed-win',
|
||||
rowKey: 'removed-win',
|
||||
id: 'removed-win',
|
||||
agentId: 'removed-win-agent',
|
||||
name: 'removed-win',
|
||||
hostname: 'win-host',
|
||||
platform: 'Windows Server 2022',
|
||||
status: 'removed',
|
||||
});
|
||||
const removedLinux = diagnosticFixture({
|
||||
connectionId: 'agent:removed-deb',
|
||||
rowKey: 'removed-deb',
|
||||
id: 'removed-deb',
|
||||
agentId: 'removed-deb-agent',
|
||||
name: 'removed-deb',
|
||||
hostname: 'deb-host',
|
||||
platform: 'debian',
|
||||
status: 'removed',
|
||||
});
|
||||
|
||||
const targets = collectInfrastructureAgentDoctorTargets({
|
||||
rows: [],
|
||||
diagnostics: [removedWindows, removedLinux],
|
||||
diagnosticsAvailable: true,
|
||||
});
|
||||
const windowsTarget = targets.find((target) => target.displayName === 'removed-win');
|
||||
const linuxTarget = targets.find((target) => target.displayName === 'removed-deb');
|
||||
|
||||
expect(windowsTarget?.commandPlatform).toBe('windows');
|
||||
expect(getInfrastructureAgentDoctorUninstallHandoff(windowsTarget!)).toEqual({
|
||||
identity: { agentId: 'removed-win-agent', hostname: 'win-host' },
|
||||
commands: [{ label: 'Windows PowerShell', platform: 'windows' }],
|
||||
});
|
||||
|
||||
expect(linuxTarget?.commandPlatform).toBe('linux');
|
||||
expect(getInfrastructureAgentDoctorUninstallHandoff(linuxTarget!)).toEqual({
|
||||
identity: { agentId: 'removed-deb-agent', hostname: 'deb-host' },
|
||||
commands: [{ label: 'Linux / macOS / FreeBSD', platform: 'linux' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('offers both labeled uninstall families when a removed agent has no recognized platform', () => {
|
||||
const removed = diagnosticFixture({
|
||||
connectionId: 'agent:removed-host',
|
||||
rowKey: 'removed-host',
|
||||
id: 'removed-host',
|
||||
agentId: undefined,
|
||||
name: 'removed-host',
|
||||
hostname: undefined,
|
||||
status: 'removed',
|
||||
});
|
||||
|
||||
const [target] = collectInfrastructureAgentDoctorTargets({
|
||||
rows: [],
|
||||
diagnostics: [removed],
|
||||
diagnosticsAvailable: true,
|
||||
});
|
||||
|
||||
expect(target.commandPlatform).toBeNull();
|
||||
expect(getInfrastructureAgentDoctorUninstallHandoff(target)).toEqual({
|
||||
identity: { agentId: 'removed-host', hostname: undefined },
|
||||
commands: [
|
||||
{ label: 'Linux / macOS / FreeBSD', platform: 'linux' },
|
||||
{ label: 'Windows PowerShell', platform: 'windows' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the uninstall handoff off non-removed targets', () => {
|
||||
const connection = connectionFixture();
|
||||
const [target] = collectInfrastructureAgentDoctorTargets({
|
||||
rows: [rowFixture(connection)],
|
||||
connections: [connection],
|
||||
diagnostics: [diagnosticFixture()],
|
||||
diagnosticsAvailable: true,
|
||||
targetVersion: '6.2.0',
|
||||
});
|
||||
|
||||
expect(target.status).not.toBe('removed');
|
||||
expect(getInfrastructureAgentDoctorUninstallHandoff(target)).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps integration-monitored machines out of the doctor', () => {
|
||||
const agent = connectionFixture({ agentUpdateAvailable: false });
|
||||
const esxi = connectionFixture({
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@ const diagnosticOnlyDoctorTarget = (
|
|||
reasons: diagnostic.reasons ?? [],
|
||||
evidence: evidenceFor(undefined, diagnostic),
|
||||
needsUpdate: false,
|
||||
commandPlatform: null,
|
||||
commandPlatform: resolveKnownAgentCommandPlatform(diagnostic.platform),
|
||||
profileLabel: diagnostic.profileName?.trim() || diagnostic.profileId?.trim() || undefined,
|
||||
profileVersionLabel: diagnostic.profileVersion
|
||||
? `Expected v${diagnostic.profileVersion} · deployed v${diagnostic.deployedProfileVersion || 0}`
|
||||
|
|
@ -672,6 +672,46 @@ export const collectInfrastructureAgentDoctorTargets = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type InfrastructureAgentDoctorUninstallHandoff = {
|
||||
identity: { agentId?: string; hostname?: string };
|
||||
commands: { label: string; platform: AgentCommandPlatform }[];
|
||||
};
|
||||
|
||||
const UNINSTALL_SHELL_LABEL = 'Linux / macOS / FreeBSD';
|
||||
const UNINSTALL_WINDOWS_LABEL = 'Windows PowerShell';
|
||||
|
||||
/**
|
||||
* Removed targets keep one legitimate next step: uninstalling the agent on
|
||||
* the host itself. When the retained diagnostic identity resolves to a known
|
||||
* platform the handoff carries that single command; when it does not, both
|
||||
* host families are offered explicitly labeled rather than guessing one.
|
||||
*/
|
||||
export const getInfrastructureAgentDoctorUninstallHandoff = (
|
||||
target: InfrastructureAgentDoctorTarget,
|
||||
): InfrastructureAgentDoctorUninstallHandoff | null => {
|
||||
if (target.status !== 'removed') return null;
|
||||
const identity = {
|
||||
agentId: target.diagnostic?.agentId?.trim() || target.diagnostic?.id?.trim() || undefined,
|
||||
hostname: target.diagnostic?.hostname?.trim() || undefined,
|
||||
};
|
||||
if (target.commandPlatform === 'windows') {
|
||||
return { identity, commands: [{ label: UNINSTALL_WINDOWS_LABEL, platform: 'windows' }] };
|
||||
}
|
||||
if (target.commandPlatform) {
|
||||
return {
|
||||
identity,
|
||||
commands: [{ label: UNINSTALL_SHELL_LABEL, platform: target.commandPlatform }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
identity,
|
||||
commands: [
|
||||
{ label: UNINSTALL_SHELL_LABEL, platform: 'linux' },
|
||||
{ label: UNINSTALL_WINDOWS_LABEL, platform: 'windows' },
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const summarizeInfrastructureAgentDoctorTargets = (
|
||||
targets: readonly InfrastructureAgentDoctorTarget[],
|
||||
) => ({
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ import {
|
|||
|
||||
export type InfrastructureOperationsStateOptions = InfrastructureInstallStateOptions;
|
||||
|
||||
// Uninstall commands only need the host identity flags, so callers without a
|
||||
// full inventory row (e.g. Agent Doctor's removed diagnostics) can hand one in.
|
||||
export type AgentUninstallIdentity = Pick<
|
||||
UnifiedAgentRow,
|
||||
'agentActionId' | 'agentId' | 'hostname'
|
||||
>;
|
||||
|
||||
export const useInfrastructureOperationsState = (
|
||||
options: InfrastructureOperationsStateOptions = {},
|
||||
) => {
|
||||
|
|
@ -69,9 +76,10 @@ export const useInfrastructureOperationsState = (
|
|||
return installState.currentToken();
|
||||
};
|
||||
|
||||
const getCanonicalUninstallAgentId = (row?: UnifiedAgentRow) =>
|
||||
const getCanonicalUninstallAgentId = (row?: AgentUninstallIdentity) =>
|
||||
row?.agentActionId?.trim() || row?.agentId?.trim() || '';
|
||||
const getCanonicalUninstallHostname = (row?: UnifiedAgentRow) => row?.hostname?.trim() || '';
|
||||
const getCanonicalUninstallHostname = (row?: AgentUninstallIdentity) =>
|
||||
row?.hostname?.trim() || '';
|
||||
const getCanonicalConnectionAgentId = (connection: Connection) => {
|
||||
if (connection.type !== 'agent') return '';
|
||||
const id = connection.id.trim();
|
||||
|
|
@ -87,7 +95,7 @@ export const useInfrastructureOperationsState = (
|
|||
const getConnectionUpgradePlatform = (connection: Connection): AgentPlatform =>
|
||||
resolveAgentCommandPlatform(connection.agentIdentity?.platform);
|
||||
|
||||
const getUninstallCommand = (row?: UnifiedAgentRow) => {
|
||||
const getUninstallCommand = (row?: AgentUninstallIdentity) => {
|
||||
const url = installState.selectedAgentUrl();
|
||||
const token = resolvedCommandToken();
|
||||
const insecure = getInsecureFlag(url);
|
||||
|
|
@ -102,7 +110,7 @@ export const useInfrastructureOperationsState = (
|
|||
);
|
||||
};
|
||||
|
||||
const getWindowsUninstallCommand = (row?: UnifiedAgentRow) => {
|
||||
const getWindowsUninstallCommand = (row?: AgentUninstallIdentity) => {
|
||||
const url = installState.selectedAgentUrl();
|
||||
const token = resolvedCommandToken();
|
||||
const transportEnv = getPowerShellTransportEnv();
|
||||
|
|
@ -123,7 +131,7 @@ export const useInfrastructureOperationsState = (
|
|||
return `${prefix}$env:PULSE_URL="${powerShellQuote(url)}"; $env:PULSE_UNINSTALL="true"; ${buildPowerShellInstallScriptBootstrap(url)}`;
|
||||
};
|
||||
|
||||
const getPlatformUninstallCommand = (platform: AgentPlatform, row?: UnifiedAgentRow) => {
|
||||
const getPlatformUninstallCommand = (platform: AgentPlatform, row?: AgentUninstallIdentity) => {
|
||||
if (platform === 'windows') {
|
||||
return getWindowsUninstallCommand(row);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue