[8.8] [Fleet] Modify query for Agents bulk actions to exclude unselectable agents (#157386) (#157742)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Fleet] Modify query for Agents bulk actions to exclude unselectable
agents (#157386)](https://github.com/elastic/kibana/pull/157386)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Cristina
Amico","email":"criamico@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-05-15T14:45:02Z","message":"[Fleet]
Modify query for Agents bulk actions to exclude unselectable agents
(#157386)\n\nCloses
https://github.com/elastic/kibana/issues/152180\r\n\r\n##
Summary\r\n\r\nModify the query for Agents bulk actions to exclude those
agents who are\r\nunselectable.\r\n\r\n- In BulkActions component, fetch
the agent policies with `is_managed =\r\ntrue`\r\n- Then find the agents
that have those policies and are not unenrolled\r\n- Exclude the
previous agent ids from the bulk selection query\r\n\r\nDoing these two
queries first ensures that we have the correct count of\r\nagents to
exclude so we can subtract it to the total.\r\n\r\n### Testing\r\n\r\n-
Update an agent policy to be \"managed\". I did it with the fleet
server\r\npolicy to simulate the behaviour on cloud:\r\n```\r\nPUT
kbn:/api/fleet/agent_policies/agent-policy-id\r\n{\r\n \"name\":
\"Managed agent policy\",\r\n \"description\": \"\",\r\n \"namespace\":
\"default\",\r\n \"monitoring_enabled\": [\r\n \"logs\",\r\n
\"metrics\"\r\n ],\r\n \"is_managed\": true\r\n}\r\n```\r\n\r\n- Enroll
some agents, it's better if they are more than 20 so you can\r\nhave
them on different pages. I
used\r\n[Horde](https://github.com/elastic/horde) for this step.\r\n-
Select all of them, then click on `Select everything on all pages`\r\n-
Click on Actions and check that the selected number of agents
doesn't\r\ninclude your managed ones\r\n- Select an action and check
that the managed actions are not included\r\nin the
action\r\n\r\n\r\ncb4cefd6-ca0e-4a72-93c8-19223a491230\r\n\r\n\r\n###
Checklist\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"14f5c470b942e28f38236f0e6134e918ece45fea","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Fleet","backport:prev-minor","v8.9.0"],"number":157386,"url":"https://github.com/elastic/kibana/pull/157386","mergeCommit":{"message":"[Fleet]
Modify query for Agents bulk actions to exclude unselectable agents
(#157386)\n\nCloses
https://github.com/elastic/kibana/issues/152180\r\n\r\n##
Summary\r\n\r\nModify the query for Agents bulk actions to exclude those
agents who are\r\nunselectable.\r\n\r\n- In BulkActions component, fetch
the agent policies with `is_managed =\r\ntrue`\r\n- Then find the agents
that have those policies and are not unenrolled\r\n- Exclude the
previous agent ids from the bulk selection query\r\n\r\nDoing these two
queries first ensures that we have the correct count of\r\nagents to
exclude so we can subtract it to the total.\r\n\r\n### Testing\r\n\r\n-
Update an agent policy to be \"managed\". I did it with the fleet
server\r\npolicy to simulate the behaviour on cloud:\r\n```\r\nPUT
kbn:/api/fleet/agent_policies/agent-policy-id\r\n{\r\n \"name\":
\"Managed agent policy\",\r\n \"description\": \"\",\r\n \"namespace\":
\"default\",\r\n \"monitoring_enabled\": [\r\n \"logs\",\r\n
\"metrics\"\r\n ],\r\n \"is_managed\": true\r\n}\r\n```\r\n\r\n- Enroll
some agents, it's better if they are more than 20 so you can\r\nhave
them on different pages. I
used\r\n[Horde](https://github.com/elastic/horde) for this step.\r\n-
Select all of them, then click on `Select everything on all pages`\r\n-
Click on Actions and check that the selected number of agents
doesn't\r\ninclude your managed ones\r\n- Select an action and check
that the managed actions are not included\r\nin the
action\r\n\r\n\r\ncb4cefd6-ca0e-4a72-93c8-19223a491230\r\n\r\n\r\n###
Checklist\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"14f5c470b942e28f38236f0e6134e918ece45fea"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/157386","number":157386,"mergeCommit":{"message":"[Fleet]
Modify query for Agents bulk actions to exclude unselectable agents
(#157386)\n\nCloses
https://github.com/elastic/kibana/issues/152180\r\n\r\n##
Summary\r\n\r\nModify the query for Agents bulk actions to exclude those
agents who are\r\nunselectable.\r\n\r\n- In BulkActions component, fetch
the agent policies with `is_managed =\r\ntrue`\r\n- Then find the agents
that have those policies and are not unenrolled\r\n- Exclude the
previous agent ids from the bulk selection query\r\n\r\nDoing these two
queries first ensures that we have the correct count of\r\nagents to
exclude so we can subtract it to the total.\r\n\r\n### Testing\r\n\r\n-
Update an agent policy to be \"managed\". I did it with the fleet
server\r\npolicy to simulate the behaviour on cloud:\r\n```\r\nPUT
kbn:/api/fleet/agent_policies/agent-policy-id\r\n{\r\n \"name\":
\"Managed agent policy\",\r\n \"description\": \"\",\r\n \"namespace\":
\"default\",\r\n \"monitoring_enabled\": [\r\n \"logs\",\r\n
\"metrics\"\r\n ],\r\n \"is_managed\": true\r\n}\r\n```\r\n\r\n- Enroll
some agents, it's better if they are more than 20 so you can\r\nhave
them on different pages. I
used\r\n[Horde](https://github.com/elastic/horde) for this step.\r\n-
Select all of them, then click on `Select everything on all pages`\r\n-
Click on Actions and check that the selected number of agents
doesn't\r\ninclude your managed ones\r\n- Select an action and check
that the managed actions are not included\r\nin the
action\r\n\r\n\r\ncb4cefd6-ca0e-4a72-93c8-19223a491230\r\n\r\n\r\n###
Checklist\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"14f5c470b942e28f38236f0e6134e918ece45fea"}}]}]
BACKPORT-->

Co-authored-by: Cristina Amico <criamico@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-05-15 12:35:47 -04:00 committed by GitHub
parent 24c9d1bf4f
commit 76b43a71df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 335 additions and 13 deletions

View file

@ -0,0 +1,252 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { fireEvent, act } from '@testing-library/react';
import type { Agent } from '../../../../types';
import { createFleetTestRendererMock } from '../../../../../../mock';
import { ExperimentalFeaturesService } from '../../../../services';
import { sendGetAgents, sendGetAgentPolicies } from '../../../../hooks';
import { AgentBulkActions } from './bulk_actions';
jest.mock('../../../../../../services/experimental_features');
const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService);
jest.mock('../../../../hooks', () => ({
...jest.requireActual('../../../../hooks'),
sendGetAgents: jest.fn(),
sendGetAgentPolicies: jest.fn(),
}));
const mockedSendGetAgents = sendGetAgents as jest.Mock;
const mockedSendGetAgentPolicies = sendGetAgentPolicies as jest.Mock;
describe('AgentBulkActions', () => {
beforeAll(() => {
mockedExperimentalFeaturesService.get.mockReturnValue({
diagnosticFileUploadEnabled: false,
} as any);
});
function render(props: any) {
const renderer = createFleetTestRendererMock();
return renderer.render(<AgentBulkActions {...props} />);
}
describe('When in manual mode', () => {
it('should show only disabled actions if no agents are active', async () => {
const selectedAgents: Agent[] = [{ id: 'agent1' }, { id: 'agent2' }] as Agent[];
const props = {
totalAgents: 10,
totalInactiveAgents: 10,
selectionMode: 'manual',
currentQuery: '',
selectedAgents,
visibleAgents: [],
refreshAgents: () => undefined,
allTags: [],
agentPolicies: [],
};
const results = render(props);
const bulkActionsButton = results.getByTestId('agentBulkActionsButton');
await act(async () => {
fireEvent.click(bulkActionsButton);
});
expect(results.getByText('Add / remove tags').closest('button')!).toBeDisabled();
expect(results.getByText('Assign to new policy').closest('button')!).toBeDisabled();
expect(results.getByText('Unenroll 2 agents').closest('button')!).toBeDisabled();
expect(results.getByText('Upgrade 2 agents').closest('button')!).toBeDisabled();
expect(results.getByText('Schedule upgrade for 2 agents').closest('button')!).toBeDisabled();
expect(results.queryByText('Request diagnostics for 2 agents')).toBeNull();
});
it('should show available actions for 2 selected agents if they are active', async () => {
const selectedAgents: Agent[] = [
{ id: 'agent1', tags: ['oldTag'], active: true },
{ id: 'agent2', active: true },
] as Agent[];
const props = {
totalAgents: 10,
totalInactiveAgents: 0,
selectionMode: 'manual',
currentQuery: '',
selectedAgents,
visibleAgents: [],
refreshAgents: () => undefined,
allTags: [],
agentPolicies: [],
};
const results = render(props);
const bulkActionsButton = results.getByTestId('agentBulkActionsButton');
await act(async () => {
fireEvent.click(bulkActionsButton);
});
expect(results.getByText('Add / remove tags').closest('button')!).toBeEnabled();
expect(results.getByText('Assign to new policy').closest('button')!).toBeEnabled();
expect(results.getByText('Unenroll 2 agents').closest('button')!).toBeEnabled();
expect(results.getByText('Upgrade 2 agents').closest('button')!).toBeEnabled();
expect(results.getByText('Schedule upgrade for 2 agents').closest('button')!).toBeDisabled();
});
it('should add actions if mockedExperimentalFeaturesService is enabled', async () => {
mockedExperimentalFeaturesService.get.mockReturnValue({
diagnosticFileUploadEnabled: true,
} as any);
const selectedAgents: Agent[] = [
{ id: 'agent1', tags: ['oldTag'], active: true },
{ id: 'agent2', active: true },
] as Agent[];
const props = {
totalAgents: 10,
totalInactiveAgents: 0,
selectionMode: 'manual',
currentQuery: '',
selectedAgents,
visibleAgents: [],
refreshAgents: () => undefined,
allTags: [],
agentPolicies: [],
unselectableAgents: [],
};
const results = render(props);
const bulkActionsButton = results.getByTestId('agentBulkActionsButton');
await act(async () => {
fireEvent.click(bulkActionsButton);
});
expect(
results.getByText('Request diagnostics for 2 agents').closest('button')!
).toBeEnabled();
});
});
describe('When in query mode', () => {
it('should show correct actions for the active agents', async () => {
mockedSendGetAgentPolicies.mockResolvedValue({
data: {
items: [
{
name: 'Managed agent policy',
namespace: 'default',
description: '',
monitoring_enabled: ['logs', 'metrics'],
is_managed: true,
id: 'test-managed-policy',
},
],
},
});
mockedSendGetAgents.mockResolvedValueOnce({
data: {
items: [],
total: 0,
totalInactive: 0,
},
});
const selectedAgents: Agent[] = [];
const props = {
totalAgents: 10,
totalInactiveAgents: 0,
selectionMode: 'query',
currentQuery: '(Base query)',
selectedAgents,
visibleAgents: [],
refreshAgents: () => undefined,
allTags: [],
agentPolicies: [],
};
const results = render(props);
const bulkActionsButton = results.getByTestId('agentBulkActionsButton');
await act(async () => {
fireEvent.click(bulkActionsButton);
});
expect(results.getByText('Add / remove tags').closest('button')!).toBeEnabled();
expect(results.getByText('Assign to new policy').closest('button')!).toBeEnabled();
expect(results.getByText('Unenroll 10 agents').closest('button')!).toBeEnabled();
expect(results.getByText('Upgrade 10 agents').closest('button')!).toBeEnabled();
expect(results.getByText('Schedule upgrade for 10 agents').closest('button')!).toBeDisabled();
expect(
results.getByText('Request diagnostics for 10 agents').closest('button')!
).toBeEnabled();
});
it('should show correct actions for the active agents and exclude the managed agents from the count', async () => {
const selectedAgents: Agent[] = [];
mockedSendGetAgentPolicies.mockResolvedValue({
data: {
items: [
{
name: 'Managed agent policy',
namespace: 'default',
description: '',
monitoring_enabled: ['logs', 'metrics'],
is_managed: true,
id: 'test-managed-policy',
},
],
},
});
mockedSendGetAgents.mockResolvedValueOnce({
data: {
items: ['agentId1', 'agentId2'],
total: 2,
totalInactive: 0,
},
});
const props = {
totalAgents: 10,
totalInactiveAgents: 0,
selectionMode: 'query',
currentQuery: '(Base query)',
selectedAgents,
visibleAgents: [],
refreshAgents: () => undefined,
allTags: [],
agentPolicies: [],
};
const results = render(props);
const bulkActionsButton = results.getByTestId('agentBulkActionsButton');
await act(async () => {
fireEvent.click(bulkActionsButton);
});
expect(results.getByText('Add / remove tags').closest('button')!).toBeEnabled();
expect(results.getByText('Assign to new policy').closest('button')!).toBeEnabled();
expect(results.getByText('Unenroll 8 agents').closest('button')!).toBeEnabled();
expect(results.getByText('Upgrade 8 agents').closest('button')!).toBeEnabled();
expect(results.getByText('Schedule upgrade for 8 agents').closest('button')!).toBeDisabled();
expect(
results.getByText('Request diagnostics for 8 agents').closest('button')!
).toBeEnabled();
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -23,8 +23,13 @@ import {
AgentUnenrollAgentModal,
AgentUpgradeAgentModal,
} from '../../components';
import { useLicense } from '../../../../hooks';
import { LICENSE_FOR_SCHEDULE_UPGRADE } from '../../../../../../../common/constants';
import { useLicense, sendGetAgents, sendGetAgentPolicies } from '../../../../hooks';
import {
LICENSE_FOR_SCHEDULE_UPGRADE,
AGENTS_PREFIX,
SO_SEARCH_LIMIT,
AGENT_POLICY_SAVED_OBJECT_TYPE,
} from '../../../../../../../common/constants';
import { ExperimentalFeaturesService } from '../../../../services';
import { getCommonTags } from '../utils';
@ -72,6 +77,64 @@ export const AgentBulkActions: React.FunctionComponent<Props> = ({
const [isTagAddVisible, setIsTagAddVisible] = useState<boolean>(false);
const [isRequestDiagnosticsModalOpen, setIsRequestDiagnosticsModalOpen] =
useState<boolean>(false);
const [managedAgents, setManagedAgents] = useState<string[]>([]);
// get all the managed policies
const fetchManagedAgents = useCallback(async () => {
if (selectionMode === 'query') {
const managedPoliciesKuery = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed:true`;
const agentPoliciesResponse = await sendGetAgentPolicies({
kuery: managedPoliciesKuery,
perPage: SO_SEARCH_LIMIT,
full: false,
});
if (agentPoliciesResponse.error) {
throw new Error(agentPoliciesResponse.error.message);
}
const managedPolicies = agentPoliciesResponse.data?.items ?? [];
// find all the agents that have those policies and are not unenrolled
const policiesKuery = managedPolicies
.map((policy) => `policy_id:"${policy.id}"`)
.join(' or ');
const kuery = `NOT (status:unenrolled) and ${policiesKuery}`;
const response = await sendGetAgents({
kuery,
perPage: SO_SEARCH_LIMIT,
showInactive: true,
});
if (response.error) {
throw new Error(response.error.message);
}
return response.data?.items ?? [];
}
return [];
}, [selectionMode]);
useEffect(() => {
async function fetchDataAsync() {
const allManagedAgents = await fetchManagedAgents();
setManagedAgents(allManagedAgents?.map((agent) => agent.id));
}
fetchDataAsync();
}, [fetchManagedAgents]);
// update the query removing the "managed" agents
const selectionQuery = useMemo(() => {
if (managedAgents) {
const excludedKuery = `${AGENTS_PREFIX}.agent.id : (${managedAgents
.map((id) => `"${id}"`)
.join(' or ')})`;
return `${currentQuery} AND NOT (${excludedKuery})`;
} else {
return currentQuery;
}
}, [currentQuery, managedAgents]);
// Check if user is working with only inactive agents
const atLeastOneActiveAgentSelected =
@ -79,8 +142,12 @@ export const AgentBulkActions: React.FunctionComponent<Props> = ({
? !!selectedAgents.find((agent) => agent.active)
: totalAgents > totalInactiveAgents;
const totalActiveAgents = totalAgents - totalInactiveAgents;
const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents;
const agents = selectionMode === 'manual' ? selectedAgents : currentQuery;
const agentCount =
selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents - managedAgents?.length;
const agents = selectionMode === 'manual' ? selectedAgents : selectionQuery;
const [tagsPopoverButton, setTagsPopoverButton] = useState<HTMLElement>();
const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get();

View file

@ -162,6 +162,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
return selectedStatus.some((status) => status === 'inactive' || status === 'unenrolled');
}, [selectedStatus]);
// filters kuery
const kuery = useMemo(() => {
return getKuery({
search,
@ -344,14 +345,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
}, {} as { [k: string]: AgentPolicy });
}, [agentPolicies]);
const isAgentSelectable = (agent: Agent) => {
if (!agent.active) return false;
if (!agent.policy_id) return true;
const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
const isHosted = agentPolicy?.is_managed === true;
return !isHosted;
};
const isAgentSelectable = useCallback(
(agent: Agent) => {
if (!agent.active) return false;
if (!agent.policy_id) return true;
const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
const isHosted = agentPolicy?.is_managed === true;
return !isHosted;
},
[agentPoliciesIndexedById]
);
const onSelectionChange = (newAgents: Agent[]) => {
setSelectedAgents(newAgents);