From 35f851c6ad1696628435d4724280fc3670ea398d Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 16 Jun 2022 14:33:31 +0200 Subject: [PATCH 01/12] changed getAllAgentsByKuery to query all agents with pit and search_after --- .../fleet/common/types/models/agent.ts | 1 + .../fleet/server/services/agents/crud.ts | 127 +++++++++++++++++- .../fleet/server/services/agents/helpers.ts | 5 +- 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index f6c72ae9d2680..65c719947e52a 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -87,6 +87,7 @@ export interface Agent extends AgentBase { access_api_key?: string; status?: AgentStatus; packages: string[]; + sort?: Array; } export interface AgentSOAttributes extends AgentBase { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 258f0bf7d4898..5939b76aeefc0 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; import type { KueryNode } from '@kbn/es-query'; @@ -90,6 +91,108 @@ export async function getAgents(esClient: ElasticsearchClient, options: GetAgent return agents; } +export async function getAgentsByKueryPit( + esClient: ElasticsearchClient, + options: ListWithKuery & { + showInactive: boolean; + pitId: string; + searchAfter?: SortResults; + } +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + showUpgradeable, + searchAfter, + } = options; + const { pitId } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(kuery); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: toElasticsearchQuery(kueryNode) } : {}; + + const queryAgents = async (from: number, size: number) => { + return esClient.search({ + from, + size, + track_total_hits: true, + rest_total_hits_as_int: true, + body: { + ...body, + sort: [{ [sortField]: { order: sortOrder } }], + }, + pit: { + id: pitId, + keep_alive: '1m', + }, + ...(searchAfter ? { search_after: searchAfter, from: 0 } : {}), + }); + }; + + const res = await queryAgents((page - 1) * perPage, perPage); + + let agents = res.hits.hits.map(searchHitToAgent); + let total = res.hits.total as number; + + // filtering for a range on the version string will not work, + // nor does filtering on a flattened field (local_metadata), so filter here + if (showUpgradeable) { + // fixing a bug where upgradeable filter was not returning right results https://github.com/elastic/kibana/issues/117329 + // query all agents, then filter upgradeable, and return the requested page and correct total + // if there are more than SO_SEARCH_LIMIT agents, the logic falls back to same as before + if (total < SO_SEARCH_LIMIT) { + const response = await queryAgents(0, SO_SEARCH_LIMIT); + agents = response.hits.hits + .map(searchHitToAgent) + .filter((agent) => isAgentUpgradeable(agent, appContextService.getKibanaVersion())); + total = agents.length; + const start = (page - 1) * perPage; + agents = agents.slice(start, start + perPage); + } else { + agents = agents.filter((agent) => + isAgentUpgradeable(agent, appContextService.getKibanaVersion()) + ); + } + } + + return { + agents, + total, + page, + perPage, + }; +} + +export async function openAgentsPointInTime(esClient: ElasticsearchClient): Promise { + const pitKeepAlive = '10m'; + const pitRes = await esClient.openPointInTime({ + index: AGENTS_INDEX, + keep_alive: pitKeepAlive, + }); + return pitRes.id; +} + +export async function closeAgentsPointInTime(esClient: ElasticsearchClient, pitId: string) { + await esClient.closePointInTime({ id: pitId }); +} + export async function getAgentsByKuery( esClient: ElasticsearchClient, options: ListWithKuery & { @@ -168,6 +271,7 @@ export async function getAgentsByKuery( }; } +// TODO instead of query all at once, do bulk action in batches export async function getAllAgentsByKuery( esClient: ElasticsearchClient, options: Omit & { @@ -177,7 +281,28 @@ export async function getAllAgentsByKuery( agents: Agent[]; total: number; }> { - const res = await getAgentsByKuery(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); + const pitId = await openAgentsPointInTime(esClient); + + const res = await getAgentsByKueryPit(esClient, { + ...options, + page: 1, + perPage: SO_SEARCH_LIMIT, + pitId, + }); + + while (res.agents.length < res.total) { + const lastAgent = res.agents[res.agents.length - 1]; + const nextPage = await getAgentsByKueryPit(esClient, { + ...options, + page: 1, + perPage: SO_SEARCH_LIMIT, + pitId, + searchAfter: lastAgent.sort!, + }); + res.agents = [...res.agents, ...nextPage.agents]; + } + + await closeAgentsPointInTime(esClient, pitId); return { agents: res.agents, diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 08c0c7ba964dc..127223629c263 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { SearchHit } from '@kbn/core/types/elasticsearch'; import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; @@ -17,7 +17,7 @@ type FleetServerAgentESResponse = | estypes.SearchResponse['hits']['hits'][0] | SearchHit; -export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent { +export function searchHitToAgent(hit: FleetServerAgentESResponse & { sort?: SortResults }): Agent { // @ts-expect-error @elastic/elasticsearch MultiGetHit._source is optional const agent: Agent = { id: hit._id, @@ -26,6 +26,7 @@ export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent { access_api_key: undefined, status: undefined, packages: hit._source?.packages ?? [], + sort: hit.sort, }; agent.status = getAgentStatus(agent); From 5e9d1b09a5ddc2ce490b6455a27a8cb5de6146e8 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Tue, 21 Jun 2022 14:37:20 +0200 Subject: [PATCH 02/12] added internal api to test pit query --- .../plugins/fleet/common/constants/routes.ts | 1 + .../fleet/server/routes/agent/handlers.ts | 26 +++++++++++++++++++ .../fleet/server/routes/agent/index.ts | 13 ++++++++++ .../fleet/server/services/agents/crud.ts | 21 +++++++++------ .../fleet_api_integration/apis/agents/list.ts | 8 ++++++ .../es_archives/fleet/agents/data.json | 12 ++++++--- 6 files changed, 69 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index 7b185960dcb7b..0f6c981cc7222 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -119,6 +119,7 @@ export const AGENT_API_ROUTES = { UPGRADE_PATTERN: `${API_ROOT}/agents/{agentId}/upgrade`, BULK_UPGRADE_PATTERN: `${API_ROOT}/agents/bulk_upgrade`, CURRENT_UPGRADES_PATTERN: `${API_ROOT}/agents/current_upgrades`, + INTERNAL_LIST_PATTERN: `${INTERNAL_ROOT}/agents`, // for testing }; export const ENROLLMENT_API_KEY_ROUTES = { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index fd0a2ae4ab4bb..048a9021f4743 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -54,6 +54,32 @@ export const getAgentHandler: RequestHandler< } }; +export const getAllAgentsHandler: RequestHandler< + undefined, + TypeOf +> = async (context, request, response) => { + const coreContext = await context.core; + const esClient = coreContext.elasticsearch.client.asInternalUser; + + try { + const agents = await AgentService.getAgents(esClient, { + perPage: request.query.perPage, + kuery: request.query.kuery!, + }); + + const body: GetAgentsResponse = { + items: agents.slice(0, 10), + total: agents.length, + totalInactive: 0, + page: 1, + perPage: 0, + }; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + export const deleteAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index b0191f07e1a2a..92db64991ad49 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -35,6 +35,7 @@ import { putAgentsReassignHandler, postBulkAgentsReassignHandler, getAgentDataHandler, + getAllAgentsHandler, } from './handlers'; import { postNewAgentActionHandlerBuilder, @@ -93,6 +94,18 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT getAgentsHandler ); + // For testing + router.get( + { + path: AGENT_API_ROUTES.INTERNAL_LIST_PATTERN, + validate: GetAgentsRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + getAllAgentsHandler + ); + // Agent actions router.post( { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 5939b76aeefc0..da8d2672f4a6a 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -69,6 +69,7 @@ export type GetAgentsOptions = | { kuery: string; showInactive?: boolean; + perPage?: number; }; export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) { @@ -80,6 +81,7 @@ export async function getAgents(esClient: ElasticsearchClient, options: GetAgent await getAllAgentsByKuery(esClient, { kuery: options.kuery, showInactive: options.showInactive ?? false, + perPage: options.perPage, }) ).agents; } else { @@ -271,10 +273,9 @@ export async function getAgentsByKuery( }; } -// TODO instead of query all at once, do bulk action in batches export async function getAllAgentsByKuery( esClient: ElasticsearchClient, - options: Omit & { + options: Omit & { showInactive: boolean; } ): Promise<{ @@ -283,29 +284,33 @@ export async function getAllAgentsByKuery( }> { const pitId = await openAgentsPointInTime(esClient); + const perPage = options.perPage ?? SO_SEARCH_LIMIT; + const res = await getAgentsByKueryPit(esClient, { ...options, page: 1, - perPage: SO_SEARCH_LIMIT, + perPage, pitId, }); - while (res.agents.length < res.total) { - const lastAgent = res.agents[res.agents.length - 1]; + let allAgents = res.agents; + + while (allAgents.length < res.total) { + const lastAgent = allAgents[allAgents.length - 1]; const nextPage = await getAgentsByKueryPit(esClient, { ...options, page: 1, - perPage: SO_SEARCH_LIMIT, + perPage, pitId, searchAfter: lastAgent.sort!, }); - res.agents = [...res.agents, ...nextPage.agents]; + allAgents = allAgents.concat(nextPage.agents); } await closeAgentsPointInTime(esClient, pitId); return { - agents: res.agents, + agents: allAgents, total: res.total, }; } diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 17316ce4086ae..1863253e273d8 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -71,5 +71,13 @@ export default function ({ getService }: FtrProviderContext) { const agent = apiResponse.items[0]; expect(agent.access_api_key_id).to.eql('api-key-2'); }); + + it('should query with pit', async () => { + const { body: apiResponse } = await supertest + .get(`/internal/fleet/agents?perPage=2`) + .expect(200); + + expect(apiResponse.total).to.eql(4); + }); }); } diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 048ab6bf0853c..2011e2bb221b2 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -9,7 +9,8 @@ "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, - "user_provided_metadata": {} + "user_provided_metadata": {}, + "enrolled_at": "2022-06-21T12:14:25Z" } } } @@ -25,7 +26,8 @@ "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, - "user_provided_metadata": {} + "user_provided_metadata": {}, + "enrolled_at": "2022-06-21T12:15:25Z" } } } @@ -41,7 +43,8 @@ "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, - "user_provided_metadata": {} + "user_provided_metadata": {}, + "enrolled_at": "2022-06-21T12:16:25Z" } } } @@ -57,7 +60,8 @@ "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, - "user_provided_metadata": {} + "user_provided_metadata": {}, + "enrolled_at": "2022-06-21T12:17:25Z" } } } From af3a2d487dc94ad2d9f2f1f5a0aadde9d2b32cd4 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Wed, 22 Jun 2022 14:28:55 +0200 Subject: [PATCH 03/12] changed reassign to work on batches of 10k --- .../fleet/server/services/agents/crud.ts | 48 +++++++++++ .../fleet/server/services/agents/reassign.ts | 79 +++++++++++++++---- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index da8d2672f4a6a..8ac5b84157000 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -273,6 +273,54 @@ export async function getAgentsByKuery( }; } +export async function processAgentsInBatches( + esClient: ElasticsearchClient, + options: Omit & { + showInactive: boolean; + }, + processAgents: ( + agents: Agent[], + includeSuccess: boolean + ) => Promise<{ items: BulkActionResult[] }> +): Promise<{ items: BulkActionResult[] }> { + const pitId = await openAgentsPointInTime(esClient); + + const perPage = options.perPage ?? SO_SEARCH_LIMIT; + + const res = await getAgentsByKueryPit(esClient, { + ...options, + page: 1, + perPage, + pitId, + }); + + let currentAgents = res.agents; + // include successful agents if total agents does not exceed 10k + const skipSuccess = res.total > SO_SEARCH_LIMIT; + + let results = await processAgents(currentAgents, skipSuccess); + let allAgentsProcessed = currentAgents.length; + + while (allAgentsProcessed < res.total) { + const lastAgent = currentAgents[currentAgents.length - 1]; + const nextPage = await getAgentsByKueryPit(esClient, { + ...options, + page: 1, + perPage, + pitId, + searchAfter: lastAgent.sort!, + }); + currentAgents = nextPage.agents; + const currentResults = await processAgents(currentAgents, skipSuccess); + results = { items: results.items.concat(currentResults.items) }; + allAgentsProcessed += currentAgents.length; + } + + await closeAgentsPointInTime(esClient, pitId); + + return results; +} + export async function getAllAgentsByKuery( esClient: ElasticsearchClient, options: Omit & { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index e077e7e47294b..10d1ba9c8d063 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -14,10 +14,10 @@ import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from import { getAgentDocuments, - getAgents, getAgentPolicyForAgent, updateAgent, bulkUpdateAgents, + processAgentsInBatches, } from './crud'; import type { GetAgentsOptions } from '.'; import { createAgentAction } from './actions'; @@ -107,10 +107,46 @@ export async function reassignAgents( } } } else if ('kuery' in options) { - givenAgents = await getAgents(esClient, options); + return await processAgentsInBatches( + esClient, + { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + perPage: options.perPage, + }, + async (agents: Agent[], skipSuccess: boolean) => + await reassignBatch( + soClient, + esClient, + newAgentPolicyId, + agents, + outgoingErrors, + undefined, + skipSuccess + ) + ); } - const givenOrder = - 'agentIds' in options ? options.agentIds : givenAgents.map((agent) => agent.id); + + return await reassignBatch( + soClient, + esClient, + newAgentPolicyId, + givenAgents, + outgoingErrors, + 'agentIds' in options ? options.agentIds : undefined + ); +} + +async function reassignBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + newAgentPolicyId: string, + givenAgents: Agent[], + outgoingErrors: Record, + agentIds?: string[], + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { + const errors: Record = { ...outgoingErrors }; const hostedPolicies = await getHostedPolicies(soClient, givenAgents); @@ -137,7 +173,7 @@ export async function reassignAgents( agents.push(result.value); } else { const id = givenAgents[index].id; - outgoingErrors[id] = result.reason; + errors[id] = result.reason; } return agents; }, []); @@ -153,17 +189,28 @@ export async function reassignAgents( })) ); - const orderedOut = givenOrder.map((agentId) => { - const hasError = agentId in outgoingErrors; - const result: BulkActionResult = { + let results; + + if (!skipSuccess) { + const givenOrder = agentIds ? agentIds : givenAgents.map((agent) => agent.id); + results = givenOrder.map((agentId) => { + const hasError = agentId in errors; + const result: BulkActionResult = { + id: agentId, + success: !hasError, + }; + if (hasError) { + result.error = errors[agentId]; + } + return result; + }); + } else { + results = Object.entries(errors).map(([agentId, error]) => ({ id: agentId, - success: !hasError, - }; - if (hasError) { - result.error = outgoingErrors[agentId]; - } - return result; - }); + success: false, + error, + })); + } const now = new Date().toISOString(); await createAgentAction(esClient, { @@ -172,5 +219,5 @@ export async function reassignAgents( type: 'POLICY_REASSIGN', }); - return { items: orderedOut }; + return { items: results }; } From 2ef62fbcad2f7d83ae08429fb0de391d33ec7822 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Wed, 22 Jun 2022 16:51:29 +0200 Subject: [PATCH 04/12] unenroll in batches --- .../fleet/server/services/agents/crud.ts | 28 ++++++++++++ .../fleet/server/services/agents/reassign.ts | 26 +---------- .../fleet/server/services/agents/unenroll.ts | 43 ++++++++++++------- .../fleet/server/services/agents/upgrade.ts | 26 ++++------- 4 files changed, 67 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 8ac5b84157000..98d3ca44f420e 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -321,6 +321,34 @@ export async function processAgentsInBatches( return results; } +export function errorsToResults( + agents: Agent[], + errors: Record, + agentIds?: string[], + skipSuccess?: boolean +): BulkActionResult[] { + if (!skipSuccess) { + const givenOrder = agentIds ? agentIds : agents.map((agent) => agent.id); + return givenOrder.map((agentId) => { + const hasError = agentId in errors; + const result: BulkActionResult = { + id: agentId, + success: !hasError, + }; + if (hasError) { + result.error = errors[agentId]; + } + return result; + }); + } else { + return Object.entries(errors).map(([agentId, error]) => ({ + id: agentId, + success: false, + error, + })); + } +} + export async function getAllAgentsByKuery( esClient: ElasticsearchClient, options: Omit & { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 10d1ba9c8d063..885989242f03b 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -18,6 +18,7 @@ import { updateAgent, bulkUpdateAgents, processAgentsInBatches, + errorsToResults, } from './crud'; import type { GetAgentsOptions } from '.'; import { createAgentAction } from './actions'; @@ -189,29 +190,6 @@ async function reassignBatch( })) ); - let results; - - if (!skipSuccess) { - const givenOrder = agentIds ? agentIds : givenAgents.map((agent) => agent.id); - results = givenOrder.map((agentId) => { - const hasError = agentId in errors; - const result: BulkActionResult = { - id: agentId, - success: !hasError, - }; - if (hasError) { - result.error = errors[agentId]; - } - return result; - }); - } else { - results = Object.entries(errors).map(([agentId, error]) => ({ - id: agentId, - success: false, - error, - })); - } - const now = new Date().toISOString(); await createAgentAction(esClient, { agents: agentsToUpdate.map((agent) => agent.id), @@ -219,5 +197,5 @@ async function reassignBatch( type: 'POLICY_REASSIGN', }); - return { items: results }; + return { items: errorsToResults(givenAgents, errors, agentIds, skipSuccess) }; } diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 893fb5f20c76a..43afdfed9134e 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -13,12 +13,14 @@ import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; +import { errorsToResults } from './crud'; import { getAgentById, getAgents, updateAgent, getAgentPolicyForAgent, bulkUpdateAgents, + processAgentsInBatches, } from './crud'; import { getHostedPolicies, isHostedAgent } from './hosted_agent'; @@ -71,9 +73,32 @@ export async function unenrollAgents( revoke?: boolean; } ): Promise<{ items: BulkActionResult[] }> { - // start with all agents specified - const givenAgents = await getAgents(esClient, options); + if ('agentIds' in options) { + const givenAgents = await getAgents(esClient, options); + return await unenrollBatch(soClient, esClient, givenAgents, options); + } + return await processAgentsInBatches( + esClient, + { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + perPage: options.perPage, + }, + async (agents: Agent[], skipSuccess?: boolean) => + await unenrollBatch(soClient, esClient, agents, options, skipSuccess) + ); +} +async function unenrollBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + givenAgents: Agent[], + options: { + force?: boolean; + revoke?: boolean; + }, + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { // Filter to those not already unenrolled, or unenrolling const agentsEnrolled = givenAgents.filter((agent) => { if (options.revoke) { @@ -124,20 +149,8 @@ export async function unenrollAgents( agentsToUpdate.map(({ id }) => ({ agentId: id, data: updateData })) ); - const getResultForAgent = (agent: Agent) => { - const hasError = agent.id in outgoingErrors; - const result: BulkActionResult = { - id: agent.id, - success: !hasError, - }; - if (hasError) { - result.error = outgoingErrors[agent.id]; - } - return result; - }; - return { - items: givenAgents.map(getResultForAgent), + items: errorsToResults(givenAgents, outgoingErrors, undefined, skipSuccess), }; } diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index f5db132815fb7..0facc16e4b3b3 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -9,7 +9,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import moment from 'moment'; import pMap from 'p-map'; -import type { Agent, BulkActionResult, FleetServerAgentAction, CurrentUpgrade } from '../../types'; +import type { Agent, FleetServerAgentAction, CurrentUpgrade } from '../../types'; import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError, @@ -21,6 +21,7 @@ import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../commo import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; +import { errorsToResults } from './crud'; import { getAgentDocuments, getAgents, @@ -183,22 +184,13 @@ export async function sendUpgradeAgentsActions( })) ); - const givenOrder = - 'agentIds' in options ? options.agentIds : agentsToCheckUpgradeable.map((agent) => agent.id); - - const orderedOut = givenOrder.map((agentId) => { - const hasError = agentId in outgoingErrors; - const result: BulkActionResult = { - id: agentId, - success: !hasError, - }; - if (hasError) { - result.error = outgoingErrors[agentId]; - } - return result; - }); - - return { items: orderedOut }; + return { + items: errorsToResults( + givenAgents, + outgoingErrors, + 'agentIds' in options ? options.agentIds : undefined + ), + }; } /** From b86e379544d416deddd1244b5e765a6ce798399b Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Wed, 22 Jun 2022 17:05:27 +0200 Subject: [PATCH 05/12] upgrade in batches --- .../fleet/server/services/agents/upgrade.ts | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 0facc16e4b3b3..ce0b61a209487 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -9,7 +9,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import moment from 'moment'; import pMap from 'p-map'; -import type { Agent, FleetServerAgentAction, CurrentUpgrade } from '../../types'; +import type { Agent, BulkActionResult, FleetServerAgentAction, CurrentUpgrade } from '../../types'; import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError, @@ -21,14 +21,8 @@ import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../commo import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; -import { errorsToResults } from './crud'; -import { - getAgentDocuments, - getAgents, - updateAgent, - bulkUpdateAgents, - getAgentPolicyForAgent, -} from './crud'; +import { errorsToResults, processAgentsInBatches } from './crud'; +import { getAgentDocuments, updateAgent, bulkUpdateAgents, getAgentPolicyForAgent } from './crud'; import { searchHitToAgent } from './helpers'; import { getHostedPolicies, isHostedAgent } from './hosted_agent'; @@ -105,9 +99,37 @@ export async function sendUpgradeAgentsActions( } } } else if ('kuery' in options) { - givenAgents = await getAgents(esClient, options); + return await processAgentsInBatches( + esClient, + { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + perPage: options.perPage, + }, + async (agents: Agent[], skipSuccess: boolean) => + await upgradeBatch(soClient, esClient, agents, outgoingErrors, options, skipSuccess) + ); } + return await upgradeBatch(soClient, esClient, givenAgents, outgoingErrors, options); +} + +async function upgradeBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + givenAgents: Agent[], + outgoingErrors: Record, + options: ({ agents: Agent[] } | GetAgentsOptions) & { + version: string; + sourceUri?: string | undefined; + force?: boolean; + upgradeDurationSeconds?: number; + startTime?: string; + }, + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { + const errors: Record = { ...outgoingErrors }; + const hostedPolicies = await getHostedPolicies(soClient, givenAgents); // results from getAgents with options.kuery '' (or even 'active:false') may include hosted agents @@ -187,8 +209,9 @@ export async function sendUpgradeAgentsActions( return { items: errorsToResults( givenAgents, - outgoingErrors, - 'agentIds' in options ? options.agentIds : undefined + errors, + 'agentIds' in options ? options.agentIds : undefined, + skipSuccess ), }; } From 495bd71b600f56898f29a97b395822b2e8b33d3a Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 23 Jun 2022 11:08:01 +0200 Subject: [PATCH 06/12] fixed upgrade --- x-pack/plugins/fleet/server/services/agents/upgrade.ts | 2 +- x-pack/test/fleet_api_integration/apis/agents/upgrade.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index ce0b61a209487..1b87c086821d0 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -164,7 +164,7 @@ async function upgradeBatch( agents.push(result.value); } else { const id = givenAgents[index].id; - outgoingErrors[id] = result.reason; + errors[id] = result.reason; } return agents; }, []); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 599488baf6707..5323c73d48074 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -21,7 +21,7 @@ export default function (providerContext: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('Agents upgrade', () => { + describe('fleet_agents_upgrade', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/fleet/agents'); From f8c5b6ccc07d18278a9a7572706b335d08631300 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 23 Jun 2022 13:51:16 +0200 Subject: [PATCH 07/12] added tests --- .../fleet/server/routes/agent/handlers.ts | 2 +- .../server/routes/agent/unenroll_handler.ts | 1 + .../server/routes/agent/upgrade_handler.ts | 2 + .../fleet/server/services/agents/crud.test.ts | 121 +++++++++++++++--- .../fleet/server/types/rest_spec/agent.ts | 3 + .../apis/agents/reassign.ts | 25 ++++ .../apis/agents/unenroll.ts | 22 ++++ .../apis/agents/upgrade.ts | 48 ++++++- 8 files changed, 203 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 048a9021f4743..a5db7535edb61 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -221,7 +221,7 @@ export const postBulkAgentsReassignHandler: RequestHandler< const results = await AgentService.reassignAgents( soClient, esClient, - agentOptions, + { ...agentOptions, perPage: request.body.perPage }, request.body.policy_id ); 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 f9d27ff71ed9d..003195762f4b3 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -65,6 +65,7 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< ...agentOptions, revoke: request.body?.revoke, force: request.body?.force, + perPage: request.body?.perPage, }); const body = results.items.reduce((acc, so) => { acc[so.id] = { 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 17ac6772ee623..8d6032f3d3dd3 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -97,6 +97,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< force, rollout_duration_seconds: upgradeDurationSeconds, start_time: startTime, + perPage, } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -122,6 +123,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< force, upgradeDurationSeconds, startTime, + perPage, }; const results = await AgentService.sendUpgradeAgentsActions(soClient, esClient, upgradeOptions); const body = results.items.reduce((acc, so) => { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.test.ts b/x-pack/plugins/fleet/server/services/agents/crud.test.ts index 5a1b47a7b359a..86b62aacfde2e 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.test.ts @@ -9,7 +9,7 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type { Agent } from '../../types'; -import { getAgentsByKuery } from './crud'; +import { errorsToResults, getAgentsByKuery, processAgentsInBatches } from './crud'; jest.mock('../../../common', () => ({ ...jest.requireActual('../../../common'), @@ -19,26 +19,28 @@ jest.mock('../../../common', () => ({ describe('Agents CRUD test', () => { let esClientMock: ElasticsearchClient; let searchMock: jest.Mock; - describe('getAgentsByKuery', () => { - beforeEach(() => { - searchMock = jest.fn(); - esClientMock = { - search: searchMock, - } as unknown as ElasticsearchClient; - }); - function getEsResponse(ids: string[], total: number) { - return { - hits: { - total, - hits: ids.map((id: string) => ({ - _id: id, - _source: {}, - })), - }, - }; - } + beforeEach(() => { + searchMock = jest.fn(); + esClientMock = { + search: searchMock, + openPointInTime: jest.fn().mockResolvedValue({ id: '1' }), + closePointInTime: jest.fn(), + } as unknown as ElasticsearchClient; + }); + function getEsResponse(ids: string[], total: number) { + return { + hits: { + total, + hits: ids.map((id: string) => ({ + _id: id, + _source: {}, + })), + }, + }; + } + describe('getAgentsByKuery', () => { it('should return upgradeable on first page', async () => { searchMock .mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2', '3', '4', '5'], 7))) @@ -192,4 +194,85 @@ describe('Agents CRUD test', () => { }); }); }); + + describe('processAgentsInBatches', () => { + const mockProcessAgents = (agents: Agent[]) => + Promise.resolve({ items: agents.map((agent) => ({ id: agent.id, success: true })) }); + it('should return results for multiple batches', async () => { + searchMock + .mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2'], 3))) + .mockImplementationOnce(() => Promise.resolve(getEsResponse(['3'], 3))); + + const response = await processAgentsInBatches( + esClientMock, + { + kuery: 'active:true', + perPage: 2, + showInactive: false, + }, + mockProcessAgents + ); + expect(response).toEqual({ + items: [ + { id: '1', success: true }, + { id: '2', success: true }, + { id: '3', success: true }, + ], + }); + }); + + it('should return results for one batch', async () => { + searchMock.mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2', '3'], 3))); + + const response = await processAgentsInBatches( + esClientMock, + { + kuery: 'active:true', + showInactive: false, + }, + mockProcessAgents + ); + expect(response).toEqual({ + items: [ + { id: '1', success: true }, + { id: '2', success: true }, + { id: '3', success: true }, + ], + }); + }); + }); + + describe('errorsToResults', () => { + it('should transform errors to results', () => { + const results = errorsToResults([{ id: '1' } as Agent, { id: '2' } as Agent], { + '1': new Error('error'), + }); + expect(results).toEqual([ + { id: '1', success: false, error: new Error('error') }, + { id: '2', success: true }, + ]); + }); + + it('should transform errors to results with skip success', () => { + const results = errorsToResults( + [{ id: '1' } as Agent, { id: '2' } as Agent], + { '1': new Error('error') }, + undefined, + true + ); + expect(results).toEqual([{ id: '1', success: false, error: new Error('error') }]); + }); + + it('should transform errors to results preserve order', () => { + const results = errorsToResults( + [{ id: '1' } as Agent, { id: '2' } as Agent], + { '1': new Error('error') }, + ['2', '1'] + ); + expect(results).toEqual([ + { id: '2', success: true }, + { id: '1', success: false, error: new Error('error') }, + ]); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index f008822437d64..1945ef0695b15 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -59,6 +59,7 @@ export const PostBulkAgentUnenrollRequestSchema = { agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), force: schema.maybe(schema.boolean()), revoke: schema.maybe(schema.boolean()), + perPage: schema.maybe(schema.number()), }), }; @@ -97,6 +98,7 @@ export const PostBulkAgentUpgradeRequestSchema = { }, }) ), + perPage: schema.maybe(schema.number()), }), }; @@ -113,6 +115,7 @@ export const PostBulkAgentReassignRequestSchema = { body: schema.object({ policy_id: schema.string(), agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + perPage: schema.maybe(schema.number()), }), }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 52251a5f40133..acefdf134c6be 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -198,6 +198,31 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('should bulk reassign multiple agents by kuery in batches', async () => { + const { body: unenrolledBody } = await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'active: true', + policy_id: 'policy2', + perPage: 2, + }) + .expect(200); + + expect(unenrolledBody).to.eql({ + agent1: { success: true }, + agent2: { success: true }, + agent3: { success: true }, + agent4: { success: true }, + }); + + const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.items.forEach((agent: any) => { + expect(agent.policy_id).to.eql('policy2'); + }); + }); + it('should throw an error for invalid policy id for bulk reassign', async () => { await supertest .post(`/api/fleet/agents/bulk_reassign`) 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 14dd5871a0317..aef4ae6c270c5 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -197,5 +197,27 @@ export default function (providerContext: FtrProviderContext) { const { body } = await supertest.get(`/api/fleet/agents`); expect(body.total).to.eql(0); }); + + it('/agents/bulk_unenroll should allow to unenroll multiple agents by kuery in batches', async () => { + const { body: unenrolledBody } = await supertest + .post(`/api/fleet/agents/bulk_unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'active: true', + revoke: true, + perPage: 2, + }) + .expect(200); + + expect(unenrolledBody).to.eql({ + agent1: { success: true }, + agent2: { success: true }, + agent3: { success: true }, + agent4: { success: true }, + }); + + const { body } = await supertest.get(`/api/fleet/agents`); + expect(body.total).to.eql(0); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 5323c73d48074..b0ca19e4c2b06 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -21,7 +21,7 @@ export default function (providerContext: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('fleet_agents_upgrade', () => { + describe('fleet_upgrade_agent', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/fleet/agents'); @@ -483,6 +483,52 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); + it('should bulk upgrade multiple agents by kuery in batches', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: false, version: '0.0.0' }, + }, + }, + upgrade_started_at: undefined, + }, + }, + }); + + const { body: unenrolledBody } = await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'active:true', + version: fleetServerVersion, + perPage: 2, + }) + .expect(200); + + expect(unenrolledBody).to.eql({ + agent4: { success: false, error: 'agent4 is not upgradeable' }, + agent3: { success: false, error: 'agent3 is not upgradeable' }, + agent2: { success: false, error: 'agent2 is not upgradeable' }, + agent1: { success: true }, + agentWithFS: { success: false, error: 'agentWithFS is not upgradeable' }, + }); + }); + it('should not upgrade an unenrolling agent during bulk_upgrade', async () => { await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ revoke: true, From 4a94e51d7e0e04f7e77c724abc8f1ced74883676 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 23 Jun 2022 16:04:24 +0200 Subject: [PATCH 08/12] cleanup --- .../plugins/fleet/common/constants/routes.ts | 1 - .../fleet/server/routes/agent/handlers.ts | 26 ------------------- .../fleet/server/routes/agent/index.ts | 13 ---------- .../fleet/server/services/agents/crud.ts | 1 - .../fleet_api_integration/apis/agents/list.ts | 8 ------ 5 files changed, 49 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index fb8e84885ec8c..33bf1b6f6b5b7 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -121,7 +121,6 @@ export const AGENT_API_ROUTES = { UPGRADE_PATTERN: `${API_ROOT}/agents/{agentId}/upgrade`, BULK_UPGRADE_PATTERN: `${API_ROOT}/agents/bulk_upgrade`, CURRENT_UPGRADES_PATTERN: `${API_ROOT}/agents/current_upgrades`, - INTERNAL_LIST_PATTERN: `${INTERNAL_ROOT}/agents`, // for testing }; export const ENROLLMENT_API_KEY_ROUTES = { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index a5db7535edb61..5ff6f9de1c60a 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -54,32 +54,6 @@ export const getAgentHandler: RequestHandler< } }; -export const getAllAgentsHandler: RequestHandler< - undefined, - TypeOf -> = async (context, request, response) => { - const coreContext = await context.core; - const esClient = coreContext.elasticsearch.client.asInternalUser; - - try { - const agents = await AgentService.getAgents(esClient, { - perPage: request.query.perPage, - kuery: request.query.kuery!, - }); - - const body: GetAgentsResponse = { - items: agents.slice(0, 10), - total: agents.length, - totalInactive: 0, - page: 1, - perPage: 0, - }; - return response.ok({ body }); - } catch (error) { - return defaultIngestErrorHandler({ error, response }); - } -}; - export const deleteAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 92db64991ad49..b0191f07e1a2a 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -35,7 +35,6 @@ import { putAgentsReassignHandler, postBulkAgentsReassignHandler, getAgentDataHandler, - getAllAgentsHandler, } from './handlers'; import { postNewAgentActionHandlerBuilder, @@ -94,18 +93,6 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT getAgentsHandler ); - // For testing - router.get( - { - path: AGENT_API_ROUTES.INTERNAL_LIST_PATTERN, - validate: GetAgentsRequestSchema, - fleetAuthz: { - fleet: { all: true }, - }, - }, - getAllAgentsHandler - ); - // Agent actions router.post( { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 98d3ca44f420e..059377fb99b7f 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -156,7 +156,6 @@ export async function getAgentsByKueryPit( // filtering for a range on the version string will not work, // nor does filtering on a flattened field (local_metadata), so filter here if (showUpgradeable) { - // fixing a bug where upgradeable filter was not returning right results https://github.com/elastic/kibana/issues/117329 // query all agents, then filter upgradeable, and return the requested page and correct total // if there are more than SO_SEARCH_LIMIT agents, the logic falls back to same as before if (total < SO_SEARCH_LIMIT) { diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 1863253e273d8..17316ce4086ae 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -71,13 +71,5 @@ export default function ({ getService }: FtrProviderContext) { const agent = apiResponse.items[0]; expect(agent.access_api_key_id).to.eql('api-key-2'); }); - - it('should query with pit', async () => { - const { body: apiResponse } = await supertest - .get(`/internal/fleet/agents?perPage=2`) - .expect(200); - - expect(apiResponse.total).to.eql(4); - }); }); } From 6f57a91144797aa58dbf70b844a2d54dc91a119b Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 23 Jun 2022 16:08:58 +0200 Subject: [PATCH 09/12] revert changes in getAllAgentsByKuery --- .../fleet/server/services/agents/crud.ts | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 059377fb99b7f..2e65f6e57c86c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -81,7 +81,6 @@ export async function getAgents(esClient: ElasticsearchClient, options: GetAgent await getAllAgentsByKuery(esClient, { kuery: options.kuery, showInactive: options.showInactive ?? false, - perPage: options.perPage, }) ).agents; } else { @@ -350,42 +349,17 @@ export function errorsToResults( export async function getAllAgentsByKuery( esClient: ElasticsearchClient, - options: Omit & { + options: Omit & { showInactive: boolean; } ): Promise<{ agents: Agent[]; total: number; }> { - const pitId = await openAgentsPointInTime(esClient); - - const perPage = options.perPage ?? SO_SEARCH_LIMIT; - - const res = await getAgentsByKueryPit(esClient, { - ...options, - page: 1, - perPage, - pitId, - }); - - let allAgents = res.agents; - - while (allAgents.length < res.total) { - const lastAgent = allAgents[allAgents.length - 1]; - const nextPage = await getAgentsByKueryPit(esClient, { - ...options, - page: 1, - perPage, - pitId, - searchAfter: lastAgent.sort!, - }); - allAgents = allAgents.concat(nextPage.agents); - } - - await closeAgentsPointInTime(esClient, pitId); + const res = await getAgentsByKuery(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); return { - agents: allAgents, + agents: res.agents, total: res.total, }; } From 8b647247c5e1aaad66e702bcf814e378d28c285a Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 23 Jun 2022 16:16:01 +0200 Subject: [PATCH 10/12] renamed perPage to batchSize in bulk actions --- x-pack/plugins/fleet/server/routes/agent/handlers.ts | 2 +- .../plugins/fleet/server/routes/agent/unenroll_handler.ts | 2 +- x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts | 4 ++-- x-pack/plugins/fleet/server/services/agents/crud.ts | 5 +++-- x-pack/plugins/fleet/server/services/agents/reassign.ts | 4 ++-- x-pack/plugins/fleet/server/services/agents/unenroll.ts | 3 ++- x-pack/plugins/fleet/server/services/agents/upgrade.ts | 3 ++- x-pack/plugins/fleet/server/types/rest_spec/agent.ts | 6 +++--- x-pack/test/fleet_api_integration/apis/agents/reassign.ts | 2 +- x-pack/test/fleet_api_integration/apis/agents/unenroll.ts | 2 +- x-pack/test/fleet_api_integration/apis/agents/upgrade.ts | 2 +- 11 files changed, 19 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 5ff6f9de1c60a..8fd4c2db6c929 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -195,7 +195,7 @@ export const postBulkAgentsReassignHandler: RequestHandler< const results = await AgentService.reassignAgents( soClient, esClient, - { ...agentOptions, perPage: request.body.perPage }, + { ...agentOptions, batchSize: request.body.batchSize }, request.body.policy_id ); 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 003195762f4b3..257112dc6872d 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -65,7 +65,7 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< ...agentOptions, revoke: request.body?.revoke, force: request.body?.force, - perPage: request.body?.perPage, + batchSize: request.body?.batchSize, }); const body = results.items.reduce((acc, so) => { acc[so.id] = { 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 8d6032f3d3dd3..a389f749c9b11 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -97,7 +97,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< force, rollout_duration_seconds: upgradeDurationSeconds, start_time: startTime, - perPage, + batchSize, } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -123,7 +123,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< force, upgradeDurationSeconds, startTime, - perPage, + batchSize, }; const results = await AgentService.sendUpgradeAgentsActions(soClient, esClient, upgradeOptions); const body = results.items.reduce((acc, so) => { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 2e65f6e57c86c..e5c936fb9c56a 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -273,8 +273,9 @@ export async function getAgentsByKuery( export async function processAgentsInBatches( esClient: ElasticsearchClient, - options: Omit & { + options: Omit & { showInactive: boolean; + batchSize?: number; }, processAgents: ( agents: Agent[], @@ -283,7 +284,7 @@ export async function processAgentsInBatches( ): Promise<{ items: BulkActionResult[] }> { const pitId = await openAgentsPointInTime(esClient); - const perPage = options.perPage ?? SO_SEARCH_LIMIT; + const perPage = options.batchSize ?? SO_SEARCH_LIMIT; const res = await getAgentsByKueryPit(esClient, { ...options, diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 885989242f03b..1746d324d626f 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -79,7 +79,7 @@ function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetG export async function reassignAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: ({ agents: Agent[] } | GetAgentsOptions) & { force?: boolean }, + options: ({ agents: Agent[] } | GetAgentsOptions) & { force?: boolean; batchSize?: number }, newAgentPolicyId: string ): Promise<{ items: BulkActionResult[] }> { const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); @@ -113,7 +113,7 @@ export async function reassignAgents( { kuery: options.kuery, showInactive: options.showInactive ?? false, - perPage: options.perPage, + batchSize: options.batchSize, }, async (agents: Agent[], skipSuccess: boolean) => await reassignBatch( diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 43afdfed9134e..7d00f34377ed1 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -71,6 +71,7 @@ export async function unenrollAgents( options: GetAgentsOptions & { force?: boolean; revoke?: boolean; + batchSize?: number; } ): Promise<{ items: BulkActionResult[] }> { if ('agentIds' in options) { @@ -82,7 +83,7 @@ export async function unenrollAgents( { kuery: options.kuery, showInactive: options.showInactive ?? false, - perPage: options.perPage, + batchSize: options.batchSize, }, async (agents: Agent[], skipSuccess?: boolean) => await unenrollBatch(soClient, esClient, agents, options, skipSuccess) diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 1b87c086821d0..5f9e6bc420b9a 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -80,6 +80,7 @@ export async function sendUpgradeAgentsActions( force?: boolean; upgradeDurationSeconds?: number; startTime?: string; + batchSize?: number; } ) { // Full set of agents @@ -104,7 +105,7 @@ export async function sendUpgradeAgentsActions( { kuery: options.kuery, showInactive: options.showInactive ?? false, - perPage: options.perPage, + batchSize: options.batchSize, }, async (agents: Agent[], skipSuccess: boolean) => await upgradeBatch(soClient, esClient, agents, outgoingErrors, options, skipSuccess) diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 1945ef0695b15..405c71e9a42b3 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -59,7 +59,7 @@ export const PostBulkAgentUnenrollRequestSchema = { agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), force: schema.maybe(schema.boolean()), revoke: schema.maybe(schema.boolean()), - perPage: schema.maybe(schema.number()), + batchSize: schema.maybe(schema.number()), }), }; @@ -98,7 +98,7 @@ export const PostBulkAgentUpgradeRequestSchema = { }, }) ), - perPage: schema.maybe(schema.number()), + batchSize: schema.maybe(schema.number()), }), }; @@ -115,7 +115,7 @@ export const PostBulkAgentReassignRequestSchema = { body: schema.object({ policy_id: schema.string(), agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), - perPage: schema.maybe(schema.number()), + batchSize: schema.maybe(schema.number()), }), }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index acefdf134c6be..7c97ff5b79bf9 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -205,7 +205,7 @@ export default function (providerContext: FtrProviderContext) { .send({ agents: 'active: true', policy_id: 'policy2', - perPage: 2, + batchSize: 2, }) .expect(200); 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 aef4ae6c270c5..93d0a58b848df 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -205,7 +205,7 @@ export default function (providerContext: FtrProviderContext) { .send({ agents: 'active: true', revoke: true, - perPage: 2, + batchSize: 2, }) .expect(200); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index b0ca19e4c2b06..c52efca4e86af 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -516,7 +516,7 @@ export default function (providerContext: FtrProviderContext) { .send({ agents: 'active:true', version: fleetServerVersion, - perPage: 2, + batchSize: 2, }) .expect(200); From b154c3e3fcf644c544e92f62a7433b835f5422e5 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Thu, 23 Jun 2022 16:17:24 +0200 Subject: [PATCH 11/12] fixed test --- x-pack/plugins/fleet/server/services/agents/crud.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/agents/crud.test.ts b/x-pack/plugins/fleet/server/services/agents/crud.test.ts index 86b62aacfde2e..db0c5d6e55b75 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.test.ts @@ -207,7 +207,7 @@ describe('Agents CRUD test', () => { esClientMock, { kuery: 'active:true', - perPage: 2, + batchSize: 2, showInactive: false, }, mockProcessAgents From 7f9bc3b0b32a4bf18e995ed4132aaf0158ef2384 Mon Sep 17 00:00:00 2001 From: Julia Bardi Date: Fri, 24 Jun 2022 08:45:45 +0200 Subject: [PATCH 12/12] try catch around close pit --- x-pack/plugins/fleet/server/services/agents/crud.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index e5c936fb9c56a..2c62973e516e5 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -190,7 +190,13 @@ export async function openAgentsPointInTime(esClient: ElasticsearchClient): Prom } export async function closeAgentsPointInTime(esClient: ElasticsearchClient, pitId: string) { - await esClient.closePointInTime({ id: pitId }); + try { + await esClient.closePointInTime({ id: pitId }); + } catch (error) { + appContextService + .getLogger() + .warn(`Error closing point in time with id: ${pitId}. Error: ${error.message}`); + } } export async function getAgentsByKuery(