-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fleet] Add test/fix for invalid/missing ids in bulk agent reassign #94632
Changes from all commits
cdd501f
f02ef35
3d365fb
6963a6b
c80ab59
546175d
86fd8f7
8c69d5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,15 +6,14 @@ | |
*/ | ||
|
||
import Boom from '@hapi/boom'; | ||
import type { SearchResponse } from 'elasticsearch'; | ||
import type { SearchResponse, MGetResponse, GetResponse } from 'elasticsearch'; | ||
import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; | ||
|
||
import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; | ||
import { appContextService, agentPolicyService } from '../../services'; | ||
import type { FleetServerAgent } from '../../../common'; | ||
import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; | ||
import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; | ||
import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; | ||
import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; | ||
import type { KueryNode } from '../../../../../../src/plugins/data/server'; | ||
import { esKuery } from '../../../../../../src/plugins/data/server'; | ||
|
@@ -59,7 +58,35 @@ export function removeSOAttributes(kuery: string) { | |
return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); | ||
} | ||
|
||
export async function listAgents( | ||
export type GetAgentsOptions = | ||
| { | ||
agentIds: string[]; | ||
} | ||
| { | ||
kuery: string; | ||
showInactive?: boolean; | ||
}; | ||
|
||
export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) { | ||
let initialResults = []; | ||
|
||
if ('agentIds' in options) { | ||
initialResults = await getAgentsById(esClient, options.agentIds); | ||
} else if ('kuery' in options) { | ||
initialResults = ( | ||
await getAllAgentsByKuery(esClient, { | ||
kuery: options.kuery, | ||
showInactive: options.showInactive ?? false, | ||
}) | ||
).agents; | ||
} else { | ||
throw new IngestManagerError('Cannot get agents'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can surely improve this. Not sure what it should say. Something about required options? |
||
} | ||
|
||
return initialResults; | ||
} | ||
|
||
export async function getAgentsByKuery( | ||
esClient: ElasticsearchClient, | ||
options: ListWithKuery & { | ||
showInactive: boolean; | ||
|
@@ -91,8 +118,7 @@ export async function listAgents( | |
|
||
const kueryNode = _joinFilters(filters); | ||
const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; | ||
|
||
const res = await esClient.search({ | ||
const res = await esClient.search<SearchResponse<FleetServerAgent>>({ | ||
index: AGENTS_INDEX, | ||
from: (page - 1) * perPage, | ||
size: perPage, | ||
|
@@ -101,27 +127,24 @@ export async function listAgents( | |
body, | ||
}); | ||
|
||
let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); | ||
let total = res.body.hits.total.value; | ||
|
||
let agents = res.body.hits.hits.map(searchHitToAgent); | ||
// 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) { | ||
agentResults = agentResults.filter((agent) => | ||
agents = agents.filter((agent) => | ||
isAgentUpgradeable(agent, appContextService.getKibanaVersion()) | ||
); | ||
total = agentResults.length; | ||
} | ||
|
||
return { | ||
agents: res.body.hits.hits.map(searchHitToAgent), | ||
total, | ||
agents, | ||
total: agents.length, | ||
page, | ||
perPage, | ||
}; | ||
} | ||
|
||
export async function listAllAgents( | ||
export async function getAllAgentsByKuery( | ||
esClient: ElasticsearchClient, | ||
options: Omit<ListWithKuery, 'page' | 'perPage'> & { | ||
showInactive: boolean; | ||
|
@@ -130,7 +153,7 @@ export async function listAllAgents( | |
agents: Agent[]; | ||
total: number; | ||
}> { | ||
const res = await listAgents(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); | ||
const res = await getAgentsByKuery(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); | ||
|
||
return { | ||
agents: res.agents, | ||
|
@@ -161,34 +184,51 @@ export async function countInactiveAgents( | |
return res.body.hits.total.value; | ||
} | ||
|
||
export async function getAgent(esClient: ElasticsearchClient, agentId: string) { | ||
export async function getAgentById(esClient: ElasticsearchClient, agentId: string) { | ||
const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found it weird to create an error each time we call this function, I would duplicate this were we throw the error There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it was preferable to waste a few microseconds creating a value we might not use to ensure the exit/error values remained in sync. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can ensure we have the same error using a function like |
||
try { | ||
const agentHit = await esClient.get<ESSearchHit<FleetServerAgent>>({ | ||
const agentHit = await esClient.get<GetResponse<FleetServerAgent>>({ | ||
index: AGENTS_INDEX, | ||
id: agentId, | ||
}); | ||
|
||
if (agentHit.body.found === false) { | ||
throw agentNotFoundError; | ||
} | ||
const agent = searchHitToAgent(agentHit.body); | ||
|
||
return agent; | ||
} catch (err) { | ||
if (isESClientError(err) && err.meta.statusCode === 404) { | ||
throw new AgentNotFoundError(`Agent ${agentId} not found`); | ||
throw agentNotFoundError; | ||
} | ||
throw err; | ||
} | ||
} | ||
|
||
export async function getAgents( | ||
async function getAgentDocuments( | ||
esClient: ElasticsearchClient, | ||
agentIds: string[] | ||
): Promise<Agent[]> { | ||
const body = { docs: agentIds.map((_id) => ({ _id })) }; | ||
|
||
const res = await esClient.mget({ | ||
body, | ||
): Promise<Array<GetResponse<FleetServerAgent>>> { | ||
const res = await esClient.mget<MGetResponse<FleetServerAgent>>({ | ||
index: AGENTS_INDEX, | ||
body: { docs: agentIds.map((_id) => ({ _id })) }, | ||
}); | ||
const agents = res.body.docs.map(searchHitToAgent); | ||
|
||
return res.body.docs || []; | ||
} | ||
|
||
export async function getAgentsById( | ||
esClient: ElasticsearchClient, | ||
agentIds: string[], | ||
options: { includeMissing?: boolean } = { includeMissing: false } | ||
): Promise<Agent[]> { | ||
const allDocs = await getAgentDocuments(esClient, agentIds); | ||
const agentDocs = options.includeMissing | ||
? allDocs | ||
: allDocs.filter((res) => res._id && res._source); | ||
const agents = agentDocs.map((doc) => searchHitToAgent(doc)); | ||
|
||
return agents; | ||
} | ||
|
||
|
@@ -201,7 +241,7 @@ export async function getAgentByAccessAPIKeyId( | |
q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, | ||
}); | ||
|
||
const [agent] = res.body.hits.hits.map(searchHitToAgent); | ||
const agent = searchHitToAgent(res.body.hits.hits[0]); | ||
|
||
if (!agent) { | ||
throw new AgentNotFoundError('Agent not found'); | ||
|
@@ -288,7 +328,7 @@ export async function getAgentPolicyForAgent( | |
esClient: ElasticsearchClient, | ||
agentId: string | ||
) { | ||
const agent = await getAgent(esClient, agentId); | ||
const agent = await getAgentById(esClient, agentId); | ||
if (!agent.policy_id) { | ||
return; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if the fact that we do not use the same function name internally and externally will cause some communication issue in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair question. We can update these as well, I just held off while we're iterating.
Would you like me to update them now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this can be done in a follow up PR, as it's probably going to take some time to be reviewed as it concern multiple team