Skip to content

Commit 044a94a

Browse files
author
John Schulz
authored
[Fleet] Add test/fix for invalid/missing ids in bulk agent reassign (#94632)
## Problem While working on changes for bulk reassign #90437, I found that the server has a runtime error and returns a 500 if given an invalid or missing id. <details><summary>server error stack trace</summary> ``` │ proc [kibana] server log [12:21:48.953] [error][fleet][plugins] TypeError: Cannot read property 'policy_revision_idx' of undefined │ proc [kibana] at map (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/helpers.ts:15:34) │ proc [kibana] at Array.map (<anonymous>) │ proc [kibana] at getAgents (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/crud.ts:191:32) │ proc [kibana] at runMicrotasks (<anonymous>) │ proc [kibana] at processTicksAndRejections (internal/process/task_queues.js:93:5) │ proc [kibana] at Object.reassignAgents (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/reassign.ts:91:9) │ proc [kibana] at postBulkAgentsReassignHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/agent/handlers.ts:314:21) │ proc [kibana] at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:272:30) │ proc [kibana] at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:227:11) │ proc [kibana] at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28) │ proc [kibana] at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20) │ proc [kibana] at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20) │ proc [kibana] at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32) │ proc [kibana] at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9) ``` </details> <details><summary>see test added in this PR fail on master</summary> ``` 1) Fleet Endpoints reassign agent(s) bulk reassign agents should allow to reassign multiple agents by id -- some invalid: Error: expected 200 "OK", got 500 "Internal Server Error" ``` </details> ## Root cause Debugging runtime error in `searchHitToAgent` found some TS type mismatches for the ES values being returned. Perhaps from one or more of the recent changes to ES client & Fleet Server. Based on `test:jest` and `test:ftr`, it appears the possible types are `GetResponse` or `SearchResponse`, instead of only an `ESSearchHit`. https://github.com/elastic/kibana/pull/94632/files#diff-254d0f427979efc3b442f78762302eb28fb9c8857df68ea04f8d411e052f939cL11 While a `.search` result will include return matched values, a `.get` or `.mget` will return a row for each input and a `found: boolean`. e.g. `{ _id: "does-not-exist", found: false }`. The error occurs when [`searchHitToAgent`](https://github.com/jfsiii/kibana/blob/1702cf98f018c41ec0a080d829a12403168ac242/x-pack/plugins/fleet/server/services/agents/helpers.ts#L11) is run on a get miss instead of a search hit. ## PR Changes * Added a test to ensure it doesn't fail if invalid or missing IDs are given * Moved the `bulk_reassign` tests to their own test section * Filter out any missing results before calling `searchHitToAgent`, to match current behavior * Consolidate repeated arguments into and code for getting agents into single [function](https://github.com/elastic/kibana/pull/94632/files#diff-f7377ed9ad56eaa8ea188b64e957e771ccc7a7652fd1eaf44251c25b930f8448R70-R87): and [TS type](https://github.com/elastic/kibana/pull/94632/files#diff-f7377ed9ad56eaa8ea188b64e957e771ccc7a7652fd1eaf44251c25b930f8448R61-R68) * Rename some agent service functions to be more explicit (IMO) but behavior maintained. Same API names exported. This moves toward the "one result (success or error) per given id" approach for #90437 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
1 parent c497239 commit 044a94a

File tree

17 files changed

+247
-236
lines changed

17 files changed

+247
-236
lines changed

x-pack/plugins/fleet/server/plugin.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ import {
8080
import {
8181
getAgentStatusById,
8282
authenticateAgentWithAccessToken,
83-
listAgents,
84-
getAgent,
83+
getAgentsByKuery,
84+
getAgentById,
8585
} from './services/agents';
8686
import { agentCheckinState } from './services/agents/checkin/state';
8787
import { registerFleetUsageCollector } from './collectors/register';
@@ -322,8 +322,8 @@ export class FleetPlugin
322322
},
323323
},
324324
agentService: {
325-
getAgent,
326-
listAgents,
325+
getAgent: getAgentById,
326+
listAgents: getAgentsByKuery,
327327
getAgentStatusById,
328328
authenticateAgentWithAccessToken,
329329
},

x-pack/plugins/fleet/server/routes/agent/handlers.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ export const getAgentHandler: RequestHandler<
4444
const esClient = context.core.elasticsearch.client.asCurrentUser;
4545

4646
try {
47-
const agent = await AgentService.getAgent(esClient, request.params.agentId);
48-
47+
const agent = await AgentService.getAgentById(esClient, request.params.agentId);
4948
const body: GetOneAgentResponse = {
5049
item: {
5150
...agent,
@@ -134,8 +133,7 @@ export const updateAgentHandler: RequestHandler<
134133
await AgentService.updateAgent(esClient, request.params.agentId, {
135134
user_provided_metadata: request.body.user_provided_metadata,
136135
});
137-
const agent = await AgentService.getAgent(esClient, request.params.agentId);
138-
136+
const agent = await AgentService.getAgentById(esClient, request.params.agentId);
139137
const body = {
140138
item: {
141139
...agent,
@@ -245,7 +243,7 @@ export const getAgentsHandler: RequestHandler<
245243
const esClient = context.core.elasticsearch.client.asCurrentUser;
246244

247245
try {
248-
const { agents, total, page, perPage } = await AgentService.listAgents(esClient, {
246+
const { agents, total, page, perPage } = await AgentService.getAgentsByKuery(esClient, {
249247
page: request.query.page,
250248
perPage: request.query.perPage,
251249
showInactive: request.query.showInactive,
@@ -310,6 +308,7 @@ export const postBulkAgentsReassignHandler: RequestHandler<
310308

311309
const soClient = context.core.savedObjects.client;
312310
const esClient = context.core.elasticsearch.client.asInternalUser;
311+
313312
try {
314313
const results = await AgentService.reassignAgents(
315314
soClient,

x-pack/plugins/fleet/server/routes/agent/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export const registerAPIRoutes = (router: IRouter, config: FleetConfigType) => {
125125
options: { tags: [`access:${PLUGIN_ID}-all`] },
126126
},
127127
postNewAgentActionHandlerBuilder({
128-
getAgent: AgentService.getAgent,
128+
getAgent: AgentService.getAgentById,
129129
createAgentAction: AgentService.createAgentAction,
130130
})
131131
);

x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as AgentService from '../../services/agents';
1515
import { appContextService } from '../../services';
1616
import { defaultIngestErrorHandler } from '../../errors';
1717
import { isAgentUpgradeable } from '../../../common/services';
18-
import { getAgent } from '../../services/agents';
18+
import { getAgentById } from '../../services/agents';
1919

2020
export const postAgentUpgradeHandler: RequestHandler<
2121
TypeOf<typeof PostAgentUpgradeRequestSchema.params>,
@@ -36,7 +36,7 @@ export const postAgentUpgradeHandler: RequestHandler<
3636
},
3737
});
3838
}
39-
const agent = await getAgent(esClient, request.params.agentId);
39+
const agent = await getAgentById(esClient, request.params.agentId);
4040
if (agent.unenrollment_started_at || agent.unenrolled_at) {
4141
return response.customError({
4242
statusCode: 400,

x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import bluebird from 'bluebird';
1111

1212
import { fullAgentPolicyToYaml } from '../../../common/services';
1313
import { appContextService, agentPolicyService, packagePolicyService } from '../../services';
14-
import { listAgents } from '../../services/agents';
14+
import { getAgentsByKuery } from '../../services/agents';
1515
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
1616
import type {
1717
GetAgentPoliciesRequestSchema,
@@ -58,7 +58,7 @@ export const getAgentPoliciesHandler: RequestHandler<
5858
await bluebird.map(
5959
items,
6060
(agentPolicy: GetAgentPoliciesResponseItem) =>
61-
listAgents(esClient, {
61+
getAgentsByKuery(esClient, {
6262
showInactive: false,
6363
perPage: 0,
6464
page: 1,

x-pack/plugins/fleet/server/services/agent_policy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
} from '../errors';
4444
import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config';
4545

46-
import { createAgentPolicyAction, listAgents } from './agents';
46+
import { createAgentPolicyAction, getAgentsByKuery } from './agents';
4747
import { packagePolicyService } from './package_policy';
4848
import { outputService } from './output';
4949
import { agentPolicyUpdateEventHandler } from './agent_policy_update';
@@ -520,7 +520,7 @@ class AgentPolicyService {
520520
throw new Error('The default agent policy cannot be deleted');
521521
}
522522

523-
const { total } = await listAgents(esClient, {
523+
const { total } = await getAgentsByKuery(esClient, {
524524
showInactive: false,
525525
perPage: 0,
526526
page: 1,

x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
getAgentPolicyActionByIds,
4040
} from '../actions';
4141
import { appContextService } from '../../app_context';
42-
import { getAgent, updateAgent } from '../crud';
42+
import { getAgentById, updateAgent } from '../crud';
4343

4444
import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils';
4545

@@ -266,7 +266,7 @@ export function agentCheckinStateNewActionsFactory() {
266266
(action) => action.type === 'INTERNAL_POLICY_REASSIGN'
267267
);
268268
if (hasConfigReassign) {
269-
return from(getAgent(esClient, agent.id)).pipe(
269+
return from(getAgentById(esClient, agent.id)).pipe(
270270
concatMap((refreshedAgent) => {
271271
if (!refreshedAgent.policy_id) {
272272
throw new Error('Agent does not have a policy assigned');

x-pack/plugins/fleet/server/services/agents/crud.ts

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
*/
77

88
import Boom from '@hapi/boom';
9-
import type { SearchResponse } from 'elasticsearch';
9+
import type { SearchResponse, MGetResponse, GetResponse } from 'elasticsearch';
1010
import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server';
1111

1212
import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types';
1313
import { appContextService, agentPolicyService } from '../../services';
1414
import type { FleetServerAgent } from '../../../common';
1515
import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common';
1616
import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants';
17-
import type { ESSearchHit } from '../../../../../../typings/elasticsearch';
1817
import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object';
1918
import type { KueryNode } from '../../../../../../src/plugins/data/server';
2019
import { esKuery } from '../../../../../../src/plugins/data/server';
@@ -59,7 +58,35 @@ export function removeSOAttributes(kuery: string) {
5958
return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, '');
6059
}
6160

62-
export async function listAgents(
61+
export type GetAgentsOptions =
62+
| {
63+
agentIds: string[];
64+
}
65+
| {
66+
kuery: string;
67+
showInactive?: boolean;
68+
};
69+
70+
export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) {
71+
let initialResults = [];
72+
73+
if ('agentIds' in options) {
74+
initialResults = await getAgentsById(esClient, options.agentIds);
75+
} else if ('kuery' in options) {
76+
initialResults = (
77+
await getAllAgentsByKuery(esClient, {
78+
kuery: options.kuery,
79+
showInactive: options.showInactive ?? false,
80+
})
81+
).agents;
82+
} else {
83+
throw new IngestManagerError('Cannot get agents');
84+
}
85+
86+
return initialResults;
87+
}
88+
89+
export async function getAgentsByKuery(
6390
esClient: ElasticsearchClient,
6491
options: ListWithKuery & {
6592
showInactive: boolean;
@@ -91,8 +118,7 @@ export async function listAgents(
91118

92119
const kueryNode = _joinFilters(filters);
93120
const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {};
94-
95-
const res = await esClient.search({
121+
const res = await esClient.search<SearchResponse<FleetServerAgent>>({
96122
index: AGENTS_INDEX,
97123
from: (page - 1) * perPage,
98124
size: perPage,
@@ -101,27 +127,24 @@ export async function listAgents(
101127
body,
102128
});
103129

104-
let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent);
105-
let total = res.body.hits.total.value;
106-
130+
let agents = res.body.hits.hits.map(searchHitToAgent);
107131
// filtering for a range on the version string will not work,
108132
// nor does filtering on a flattened field (local_metadata), so filter here
109133
if (showUpgradeable) {
110-
agentResults = agentResults.filter((agent) =>
134+
agents = agents.filter((agent) =>
111135
isAgentUpgradeable(agent, appContextService.getKibanaVersion())
112136
);
113-
total = agentResults.length;
114137
}
115138

116139
return {
117-
agents: res.body.hits.hits.map(searchHitToAgent),
118-
total,
140+
agents,
141+
total: agents.length,
119142
page,
120143
perPage,
121144
};
122145
}
123146

124-
export async function listAllAgents(
147+
export async function getAllAgentsByKuery(
125148
esClient: ElasticsearchClient,
126149
options: Omit<ListWithKuery, 'page' | 'perPage'> & {
127150
showInactive: boolean;
@@ -130,7 +153,7 @@ export async function listAllAgents(
130153
agents: Agent[];
131154
total: number;
132155
}> {
133-
const res = await listAgents(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT });
156+
const res = await getAgentsByKuery(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT });
134157

135158
return {
136159
agents: res.agents,
@@ -161,34 +184,51 @@ export async function countInactiveAgents(
161184
return res.body.hits.total.value;
162185
}
163186

164-
export async function getAgent(esClient: ElasticsearchClient, agentId: string) {
187+
export async function getAgentById(esClient: ElasticsearchClient, agentId: string) {
188+
const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`);
165189
try {
166-
const agentHit = await esClient.get<ESSearchHit<FleetServerAgent>>({
190+
const agentHit = await esClient.get<GetResponse<FleetServerAgent>>({
167191
index: AGENTS_INDEX,
168192
id: agentId,
169193
});
194+
195+
if (agentHit.body.found === false) {
196+
throw agentNotFoundError;
197+
}
170198
const agent = searchHitToAgent(agentHit.body);
171199

172200
return agent;
173201
} catch (err) {
174202
if (isESClientError(err) && err.meta.statusCode === 404) {
175-
throw new AgentNotFoundError(`Agent ${agentId} not found`);
203+
throw agentNotFoundError;
176204
}
177205
throw err;
178206
}
179207
}
180208

181-
export async function getAgents(
209+
async function getAgentDocuments(
182210
esClient: ElasticsearchClient,
183211
agentIds: string[]
184-
): Promise<Agent[]> {
185-
const body = { docs: agentIds.map((_id) => ({ _id })) };
186-
187-
const res = await esClient.mget({
188-
body,
212+
): Promise<Array<GetResponse<FleetServerAgent>>> {
213+
const res = await esClient.mget<MGetResponse<FleetServerAgent>>({
189214
index: AGENTS_INDEX,
215+
body: { docs: agentIds.map((_id) => ({ _id })) },
190216
});
191-
const agents = res.body.docs.map(searchHitToAgent);
217+
218+
return res.body.docs || [];
219+
}
220+
221+
export async function getAgentsById(
222+
esClient: ElasticsearchClient,
223+
agentIds: string[],
224+
options: { includeMissing?: boolean } = { includeMissing: false }
225+
): Promise<Agent[]> {
226+
const allDocs = await getAgentDocuments(esClient, agentIds);
227+
const agentDocs = options.includeMissing
228+
? allDocs
229+
: allDocs.filter((res) => res._id && res._source);
230+
const agents = agentDocs.map((doc) => searchHitToAgent(doc));
231+
192232
return agents;
193233
}
194234

@@ -201,7 +241,7 @@ export async function getAgentByAccessAPIKeyId(
201241
q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`,
202242
});
203243

204-
const [agent] = res.body.hits.hits.map(searchHitToAgent);
244+
const agent = searchHitToAgent(res.body.hits.hits[0]);
205245

206246
if (!agent) {
207247
throw new AgentNotFoundError('Agent not found');
@@ -288,7 +328,7 @@ export async function getAgentPolicyForAgent(
288328
esClient: ElasticsearchClient,
289329
agentId: string
290330
) {
291-
const agent = await getAgent(esClient, agentId);
331+
const agent = await getAgentById(esClient, agentId);
292332
if (!agent.policy_id) {
293333
return;
294334
}

x-pack/plugins/fleet/server/services/agents/helpers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
* 2.0.
66
*/
77

8-
import type { ESSearchHit } from '../../../../../../typings/elasticsearch';
8+
import type { GetResponse, SearchResponse } from 'elasticsearch';
9+
910
import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types';
1011

11-
export function searchHitToAgent(hit: ESSearchHit<FleetServerAgent>): Agent {
12+
type FleetServerAgentESResponse =
13+
| GetResponse<FleetServerAgent>
14+
| SearchResponse<FleetServerAgent>['hits']['hits'][0];
15+
16+
export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent {
1217
return {
1318
id: hit._id,
1419
...hit._source,

x-pack/plugins/fleet/server/services/agents/reassign.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function createClientsMock() {
121121
case unmanagedAgentPolicySO2.id:
122122
return unmanagedAgentPolicySO2;
123123
default:
124-
throw new Error('Not found');
124+
throw new Error(`${id} not found`);
125125
}
126126
});
127127
soClientMock.bulkGet.mockImplementation(async (options) => {
@@ -147,7 +147,7 @@ function createClientsMock() {
147147
case agentInUnmanagedDoc._id:
148148
return { body: agentInUnmanagedDoc };
149149
default:
150-
throw new Error('Not found');
150+
throw new Error(`${id} not found`);
151151
}
152152
});
153153
// @ts-expect-error

0 commit comments

Comments
 (0)