diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index f6a8437ef9dd9..4616e92925b3a 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -121,8 +121,13 @@ export interface PostBulkAgentUnenrollRequest { }; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PostBulkAgentUnenrollResponse {} +export type PostBulkAgentUnenrollResponse = Record< + Agent['id'], + { + success: boolean; + error?: string; + } +>; export interface PostAgentUpgradeRequest { params: { diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts index 558a9a8afbb0b..1505955215515 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -60,11 +60,17 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< : { kuery: request.body.agents }; try { - await AgentService.unenrollAgents(soClient, esClient, { + const results = await AgentService.unenrollAgents(soClient, esClient, { ...agentOptions, force: request.body?.force, }); - const body: PostBulkAgentUnenrollResponse = {}; + const body = results.items.reduce((acc, so) => { + acc[so.id] = { + success: !so.error, + error: so.error?.message, + }; + return acc; + }, {}); return response.ok({ body }); } catch (error) { diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 0288bcdbe220f..52f62037f61e6 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -103,7 +103,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< const body = results.items.reduce((acc, so) => { acc[so.id] = { success: !so.error, - error: so.error ? so.error.message || so.error.toString() : undefined, + error: so.error?.message, }; return acc; }, {}); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index b89b2b6d351b8..ecf18430da668 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -10,7 +10,6 @@ import type { estypes } from '@elastic/elasticsearch'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import type { AgentSOAttributes, Agent, BulkActionResult, ListWithKuery } from '../../types'; - import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 8cf7396eaa8de..ff243eff11570 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -7,6 +7,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { Agent, BulkActionResult } from '../../types'; import * as APIKeyService from '../api_keys'; import { AgentUnenrollmentError } from '../../errors'; @@ -57,26 +58,35 @@ export async function unenrollAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, options: GetAgentsOptions & { force?: boolean } -) { +): Promise<{ items: BulkActionResult[] }> { // start with all agents specified - const agents = await getAgents(esClient, options); + const givenAgents = await getAgents(esClient, options); + const outgoingErrors: Record = {}; // Filter to those not already unenrolled, or unenrolling - const agentsEnrolled = agents.filter((agent) => { + const agentsEnrolled = givenAgents.filter((agent) => { if (options.force) { return !agent.unenrolled_at; } return !agent.unenrollment_started_at && !agent.unenrolled_at; }); // And which are allowed to unenroll - const settled = await Promise.allSettled( + const agentResults = await Promise.allSettled( agentsEnrolled.map((agent) => unenrollAgentIsAllowed(soClient, esClient, agent.id).then((_) => agent) ) ); - const agentsToUpdate = agentsEnrolled.filter((_, index) => settled[index].status === 'fulfilled'); - const now = new Date().toISOString(); + const agentsToUpdate = agentResults.reduce((agents, result, index) => { + if (result.status === 'fulfilled') { + agents.push(result.value); + } else { + const id = givenAgents[index].id; + outgoingErrors[id] = result.reason; + } + return agents; + }, []); + const now = new Date().toISOString(); if (options.force) { // Get all API keys that need to be invalidated const apiKeys = agentsToUpdate.reduce((keys, agent) => { @@ -94,17 +104,6 @@ export async function unenrollAgents( if (apiKeys.length) { await APIKeyService.invalidateAPIKeys(soClient, apiKeys); } - // Update the necessary agents - return bulkUpdateAgents( - esClient, - agentsToUpdate.map((agent) => ({ - agentId: agent.id, - data: { - active: false, - unenrolled_at: now, - }, - })) - ); } else { // Create unenroll action for each agent await bulkCreateAgentActions( @@ -116,18 +115,32 @@ export async function unenrollAgents( type: 'UNENROLL', })) ); - - // Update the necessary agents - return bulkUpdateAgents( - esClient, - agentsToUpdate.map((agent) => ({ - agentId: agent.id, - data: { - unenrollment_started_at: now, - }, - })) - ); } + + // Update the necessary agents + const updateData = options.force + ? { unenrolled_at: now, active: false } + : { unenrollment_started_at: now }; + + await bulkUpdateAgents( + esClient, + agentsToUpdate.map(({ id }) => ({ agentId: id, data: updateData })) + ); + + const out = { + items: givenAgents.map((agent, index) => { + const hasError = agent.id in outgoingErrors; + const result: BulkActionResult = { + id: agent.id, + success: !hasError, + }; + if (hasError) { + result.error = outgoingErrors[agent.id]; + } + return result; + }), + }; + return out; } export async function forceUnenrollAgent( diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index 09a0d3c927e4c..ab765eae18ca5 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -119,7 +119,7 @@ export default function (providerContext: FtrProviderContext) { .expect(200); // try to unenroll - await supertest + const { body: unenrolledBody } = await supertest .post(`/api/fleet/agents/bulk_unenroll`) .set('kbn-xsrf', 'xxx') .send({ @@ -128,6 +128,16 @@ export default function (providerContext: FtrProviderContext) { // http request succeeds .expect(200); + expect(unenrolledBody).to.eql({ + agent2: { + success: false, + error: 'Cannot unenroll agent2 from a managed agent policy policy1', + }, + agent3: { + success: false, + error: 'Cannot unenroll agent3 from a managed agent policy policy1', + }, + }); // but agents are still enrolled const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), @@ -148,17 +158,23 @@ export default function (providerContext: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ name: 'Test policy', namespace: 'default', is_managed: false }) .expect(200); - await supertest + const { body: unenrolledBody } = await supertest .post(`/api/fleet/agents/bulk_unenroll`) .set('kbn-xsrf', 'xxx') .send({ agents: ['agent2', 'agent3'], - }) - .expect(200); + }); + + expect(unenrolledBody).to.eql({ + agent2: { success: true }, + agent3: { success: true }, + }); + const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), supertest.get(`/api/fleet/agents/agent3`), ]); + expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('string'); expect(agent2data.body.item.active).to.eql(true); expect(typeof agent3data.body.item.unenrollment_started_at).to.be('string');