mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Add per-policy inactivity timeout + use runtime fields for agent status (#147552)
## Summary Part of #143455 Previously agents would be unenrolled after a given time by the fleet server. Instead, they'll be considered `Inactive`. Agents in an `Inactive` state are hidden from the UI by default, but their API keys remain active. This allows these agents to check in again at any time without requesting new API keys.`inactivity_timeout` defaults to 10 minutes or can be configured on a per policy basis. Agents that are manually unenrolled will go into the new `Unenrolled` status.  These changes mean that we now need to get agent policies before knowing the agents status, we have used a runtime field to calculate the status at search time, this allows us to easily filter and aggregate on the status. ### Performance For 120 agents (20 of each main status): - filter call with filters: 90ms - agent status summary call: 83ms For 12k agents (2k of each main status): - filter call with filters: 455ms - agent status summary call: 500ms For 120k agents (20k of each main status): - filter call with filters: 2.2s - agent status summary call: 2.1s ### Manual Testing the create agent script can be used to test this at scale e.g create 10k agents of each of the given statuses: ```bash cd x-pack/plugins/fleet node scripts/create_agents --count 10000 --kibana http://localhost:5601/myprefix--status offline,online,inactive,error,updating,unenrolled --inactivityTimeout 360 --delete ``` ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
efb7cdd49e
commit
a9166da678
63 changed files with 1435 additions and 1097 deletions
|
@ -1178,6 +1178,9 @@
|
|||
"inactive": {
|
||||
"type": "integer"
|
||||
},
|
||||
"unenrolled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offline": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -735,6 +735,8 @@ paths:
|
|||
type: integer
|
||||
inactive:
|
||||
type: integer
|
||||
unenrolled:
|
||||
type: integer
|
||||
offline:
|
||||
type: integer
|
||||
online:
|
||||
|
|
|
@ -15,6 +15,8 @@ get:
|
|||
type: integer
|
||||
inactive:
|
||||
type: integer
|
||||
unenrolled:
|
||||
type: integer
|
||||
offline:
|
||||
type: integer
|
||||
online:
|
||||
|
|
|
@ -5,55 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AGENT_POLLING_THRESHOLD_MS } from '../constants';
|
||||
import type { Agent, AgentStatus, FleetServerAgent } from '../types';
|
||||
|
||||
const offlineTimeoutIntervalCount = 10; // 30s*10 = 5m timeout
|
||||
|
||||
export function getAgentStatus(agent: Agent | FleetServerAgent): AgentStatus {
|
||||
const { last_checkin: lastCheckIn } = agent;
|
||||
|
||||
if (!agent.active) {
|
||||
return 'inactive';
|
||||
}
|
||||
|
||||
if (!agent.last_checkin) {
|
||||
return 'enrolling';
|
||||
}
|
||||
|
||||
const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
|
||||
const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn;
|
||||
const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS);
|
||||
|
||||
if (intervalsSinceLastCheckIn >= offlineTimeoutIntervalCount) {
|
||||
return 'offline';
|
||||
}
|
||||
|
||||
if (agent.unenrollment_started_at && !agent.unenrolled_at) {
|
||||
return 'unenrolling';
|
||||
}
|
||||
|
||||
if (agent.last_checkin_status?.toLowerCase() === 'error') {
|
||||
return 'error';
|
||||
}
|
||||
if (agent.last_checkin_status?.toLowerCase() === 'degraded') {
|
||||
return 'degraded';
|
||||
}
|
||||
|
||||
const policyRevision =
|
||||
'policy_revision' in agent
|
||||
? agent.policy_revision
|
||||
: 'policy_revision_idx' in agent
|
||||
? agent.policy_revision_idx
|
||||
: undefined;
|
||||
|
||||
if (!policyRevision || (agent.upgrade_started_at && !agent.upgraded_at)) {
|
||||
return 'updating';
|
||||
}
|
||||
|
||||
return 'online';
|
||||
}
|
||||
|
||||
export function getPreviousAgentStatusForOfflineAgents(
|
||||
agent: Agent | FleetServerAgent
|
||||
): AgentStatus | undefined {
|
||||
|
@ -80,55 +33,26 @@ export function getPreviousAgentStatusForOfflineAgents(
|
|||
}
|
||||
}
|
||||
|
||||
export function buildKueryForEnrollingAgents(path: string = ''): string {
|
||||
return `not (${path}last_checkin:*)`;
|
||||
export function buildKueryForUnenrolledAgents(): string {
|
||||
return 'status:unenrolled';
|
||||
}
|
||||
|
||||
export function buildKueryForUnenrollingAgents(path: string = ''): string {
|
||||
return `${path}unenrollment_started_at:*`;
|
||||
export function buildKueryForOnlineAgents(): string {
|
||||
return 'status:online';
|
||||
}
|
||||
|
||||
export function buildKueryForOnlineAgents(path: string = ''): string {
|
||||
return `${path}last_checkin:* ${addExclusiveKueryFilter(
|
||||
[buildKueryForOfflineAgents, buildKueryForUpdatingAgents, buildKueryForErrorAgents],
|
||||
path
|
||||
)}`;
|
||||
export function buildKueryForErrorAgents(): string {
|
||||
return '(status:error or status:degraded)';
|
||||
}
|
||||
|
||||
export function buildKueryForErrorAgents(path: string = ''): string {
|
||||
return `(${path}last_checkin_status:error or ${path}last_checkin_status:degraded or ${path}last_checkin_status:DEGRADED or ${path}last_checkin_status:ERROR) ${addExclusiveKueryFilter(
|
||||
[buildKueryForOfflineAgents, buildKueryForUnenrollingAgents],
|
||||
path
|
||||
)}`;
|
||||
export function buildKueryForOfflineAgents(): string {
|
||||
return 'status:offline';
|
||||
}
|
||||
|
||||
export function buildKueryForOfflineAgents(path: string = ''): string {
|
||||
return `${path}last_checkin < now-${
|
||||
(offlineTimeoutIntervalCount * AGENT_POLLING_THRESHOLD_MS) / 1000
|
||||
}s`;
|
||||
export function buildKueryForUpdatingAgents(): string {
|
||||
return '(status:updating or status:unenrolling or status:enrolling)';
|
||||
}
|
||||
|
||||
export function buildKueryForUpgradingAgents(path: string = ''): string {
|
||||
return `(${path}upgrade_started_at:*) and not (${path}upgraded_at:*)`;
|
||||
}
|
||||
|
||||
export function buildKueryForUpdatingAgents(path: string = ''): string {
|
||||
return `((${buildKueryForUpgradingAgents(path)}) or (${buildKueryForEnrollingAgents(
|
||||
path
|
||||
)}) or (${buildKueryForUnenrollingAgents(
|
||||
path
|
||||
)}) or (not ${path}policy_revision_idx:*)) ${addExclusiveKueryFilter(
|
||||
[buildKueryForOfflineAgents, buildKueryForErrorAgents],
|
||||
path
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function buildKueryForInactiveAgents(path: string = '') {
|
||||
return `${path}active:false`;
|
||||
}
|
||||
|
||||
function addExclusiveKueryFilter(kueryBuilders: Array<(path?: string) => string>, path?: string) {
|
||||
return ` AND not (${kueryBuilders
|
||||
.map((kueryBuilder) => `(${kueryBuilder(path)})`)
|
||||
.join(' or ')})`;
|
||||
export function buildKueryForInactiveAgents() {
|
||||
return 'status:inactive';
|
||||
}
|
||||
|
|
|
@ -24,10 +24,17 @@ export type AgentStatus =
|
|||
| 'inactive'
|
||||
| 'enrolling'
|
||||
| 'unenrolling'
|
||||
| 'unenrolled'
|
||||
| 'updating'
|
||||
| 'degraded';
|
||||
|
||||
export type SimplifiedAgentStatus = 'healthy' | 'unhealthy' | 'updating' | 'offline' | 'inactive';
|
||||
export type SimplifiedAgentStatus =
|
||||
| 'healthy'
|
||||
| 'unhealthy'
|
||||
| 'updating'
|
||||
| 'offline'
|
||||
| 'inactive'
|
||||
| 'unenrolled';
|
||||
|
||||
export type AgentActionType =
|
||||
| 'UNENROLL'
|
||||
|
|
|
@ -186,6 +186,8 @@ export interface GetAgentStatusResponse {
|
|||
offline: number;
|
||||
other: number;
|
||||
updating: number;
|
||||
inactive: number;
|
||||
unenrolled: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -166,9 +166,17 @@ describe('View agents list', () => {
|
|||
});
|
||||
|
||||
describe('Agent status filter', () => {
|
||||
const clearFilters = () => {
|
||||
cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click();
|
||||
cy.get('button').contains('Healthy').click();
|
||||
cy.get('button').contains('Unhealthy').click();
|
||||
cy.get('button').contains('Updating').click();
|
||||
cy.get('button').contains('Offline').click();
|
||||
cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click();
|
||||
};
|
||||
it('should filter on healthy (16 result)', () => {
|
||||
cy.visit('/app/fleet/agents');
|
||||
|
||||
clearFilters();
|
||||
cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click();
|
||||
|
||||
cy.get('button').contains('Healthy').click();
|
||||
|
@ -179,7 +187,7 @@ describe('View agents list', () => {
|
|||
|
||||
it('should filter on unhealthy (1 result)', () => {
|
||||
cy.visit('/app/fleet/agents');
|
||||
|
||||
clearFilters();
|
||||
cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click();
|
||||
|
||||
cy.get('button').contains('Unhealthy').click();
|
||||
|
@ -190,6 +198,7 @@ describe('View agents list', () => {
|
|||
|
||||
it('should filter on inactive (0 result)', () => {
|
||||
cy.visit('/app/fleet/agents');
|
||||
clearFilters();
|
||||
|
||||
cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click();
|
||||
|
||||
|
@ -200,6 +209,7 @@ describe('View agents list', () => {
|
|||
|
||||
it('should filter on healthy and unhealthy', () => {
|
||||
cy.visit('/app/fleet/agents');
|
||||
clearFilters();
|
||||
|
||||
cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click();
|
||||
|
||||
|
|
|
@ -63,6 +63,12 @@ const statusFilters = [
|
|||
defaultMessage: 'Inactive',
|
||||
}),
|
||||
},
|
||||
{
|
||||
status: 'unenrolled',
|
||||
label: i18n.translate('xpack.fleet.agentList.statusUnenrolledFilterText', {
|
||||
defaultMessage: 'Unenrolled',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const ClearAllTagsFilterItem = styled(EuiFilterSelectItem)`
|
||||
|
|
|
@ -20,7 +20,9 @@ export const AgentStatusBadges: React.FC<{
|
|||
agentStatus: { [k in SimplifiedAgentStatus]: number };
|
||||
}> = memo(({ agentStatus, showInactive }) => {
|
||||
const agentStatuses = useMemo(() => {
|
||||
return AGENT_STATUSES.filter((status) => (showInactive ? true : status !== 'inactive'));
|
||||
return AGENT_STATUSES.filter((status) =>
|
||||
showInactive ? true : status !== 'inactive' && status !== 'unenrolled'
|
||||
);
|
||||
}, [showInactive]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -87,7 +87,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
const [selectedAgentPolicies, setSelectedAgentPolicies] = useState<string[]>([]);
|
||||
|
||||
// Status for filtering
|
||||
const [selectedStatus, setSelectedStatus] = useState<string[]>([]);
|
||||
const [selectedStatus, setSelectedStatus] = useState<string[]>([
|
||||
'healthy',
|
||||
'unhealthy',
|
||||
'updating',
|
||||
'offline',
|
||||
]);
|
||||
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
|
@ -183,6 +188,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
return AgentStatusKueryHelper.buildKueryForUpdatingAgents();
|
||||
case 'inactive':
|
||||
return AgentStatusKueryHelper.buildKueryForInactiveAgents();
|
||||
case 'unenrolled':
|
||||
return AgentStatusKueryHelper.buildKueryForUnenrolledAgents();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -201,7 +208,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
}, [search, selectedAgentPolicies, selectedTags, selectedStatus]);
|
||||
|
||||
const showInactive = useMemo(() => {
|
||||
return selectedStatus.includes('inactive');
|
||||
return selectedStatus.some((status) => status === 'inactive' || status === 'unenrolled');
|
||||
}, [selectedStatus]);
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
|
@ -309,7 +316,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
unhealthy: agentsStatusResponse.data.results.error,
|
||||
offline: agentsStatusResponse.data.results.offline,
|
||||
updating: agentsStatusResponse.data.results.updating,
|
||||
inactive: agentsResponse.data.totalInactive,
|
||||
inactive: agentsStatusResponse.data.results.inactive,
|
||||
unenrolled: agentsStatusResponse.data.results.unenrolled,
|
||||
});
|
||||
|
||||
const newAllTags = agentTagsResponse.data.items;
|
||||
|
|
|
@ -36,6 +36,14 @@ const Status = {
|
|||
<FormattedMessage id="xpack.fleet.agentHealth.inactiveStatusText" defaultMessage="Inactive" />
|
||||
</EuiBadge>
|
||||
),
|
||||
Unenrolled: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentHealth.unenrolledStatusText"
|
||||
defaultMessage="Unenrolled"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
Unhealthy: (
|
||||
<EuiBadge color="warning">
|
||||
<FormattedMessage
|
||||
|
@ -64,6 +72,8 @@ function getStatusComponent(status: Agent['status']): React.ReactElement {
|
|||
case 'enrolling':
|
||||
case 'updating':
|
||||
return Status.Updating;
|
||||
case 'unenrolled':
|
||||
return Status.Unenrolled;
|
||||
default:
|
||||
return Status.Healthy;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ const colorToHexMap = {
|
|||
warning: visColors[5],
|
||||
danger: visColors[9],
|
||||
inactive: euiLightVars.euiColorDarkShade,
|
||||
lightest: euiLightVars.euiColorDisabled,
|
||||
};
|
||||
|
||||
export const AGENT_STATUSES: SimplifiedAgentStatus[] = [
|
||||
|
@ -29,6 +30,7 @@ export const AGENT_STATUSES: SimplifiedAgentStatus[] = [
|
|||
'updating',
|
||||
'offline',
|
||||
'inactive',
|
||||
'unenrolled',
|
||||
];
|
||||
|
||||
export function getColorForAgentStatus(agentStatus: SimplifiedAgentStatus): string {
|
||||
|
@ -43,6 +45,8 @@ export function getColorForAgentStatus(agentStatus: SimplifiedAgentStatus): stri
|
|||
return colorToHexMap.warning;
|
||||
case 'updating':
|
||||
return colorToHexMap.primary;
|
||||
case 'unenrolled':
|
||||
return colorToHexMap.lightest;
|
||||
default:
|
||||
throw new Error(`Unsupported Agent status ${agentStatus}`);
|
||||
}
|
||||
|
@ -62,6 +66,10 @@ export function getLabelForAgentStatus(agentStatus: SimplifiedAgentStatus): stri
|
|||
return i18n.translate('xpack.fleet.agentStatus.inactiveLabel', {
|
||||
defaultMessage: 'Inactive',
|
||||
});
|
||||
case 'unenrolled':
|
||||
return i18n.translate('xpack.fleet.agentStatus.unenrolledLabel', {
|
||||
defaultMessage: 'Unenrolled',
|
||||
});
|
||||
case 'unhealthy':
|
||||
return i18n.translate('xpack.fleet.agentStatus.unhealthyLabel', {
|
||||
defaultMessage: 'Unhealthy',
|
||||
|
|
|
@ -31,14 +31,18 @@ const DEFAULT_KIBANA_URL = 'http://localhost:5601';
|
|||
const DEFAULT_KIBANA_USERNAME = 'elastic';
|
||||
const DEFAULT_KIBANA_PASSWORD = 'changeme';
|
||||
|
||||
const DEFAULT_UNENROLL_TIMEOUT = 300; // 5 minutes
|
||||
const ES_URL = 'http://localhost:9200';
|
||||
const ES_SUPERUSER = 'fleet_superuser';
|
||||
const ES_PASSWORD = 'password';
|
||||
|
||||
const DEFAULT_AGENT_COUNT = 50000;
|
||||
|
||||
const INDEX_BULK_OP = '{ "index":{ } }\n';
|
||||
|
||||
const {
|
||||
delete: deleteAgentsFirst = false,
|
||||
inactivityTimeout: inactivityTimeoutArg,
|
||||
status: statusArg = 'online',
|
||||
count: countArg,
|
||||
kibana: kibanaUrl = DEFAULT_KIBANA_URL,
|
||||
|
@ -52,7 +56,10 @@ const {
|
|||
} = yargs(process.argv.slice(2)).argv;
|
||||
|
||||
const statusesArg = (statusArg as string).split(',') as AgentStatus[];
|
||||
const count = countArg ? Number(countArg).valueOf() : 50000;
|
||||
const inactivityTimeout = inactivityTimeoutArg
|
||||
? Number(inactivityTimeoutArg).valueOf()
|
||||
: DEFAULT_UNENROLL_TIMEOUT;
|
||||
const count = countArg ? Number(countArg).valueOf() : DEFAULT_AGENT_COUNT;
|
||||
const kbnAuth = 'Basic ' + Buffer.from(kbnUsername + ':' + kbnPassword).toString('base64');
|
||||
|
||||
const logger = new ToolingLog({
|
||||
|
@ -63,7 +70,7 @@ const logger = new ToolingLog({
|
|||
function setAgentStatus(agent: any, status: AgentStatus) {
|
||||
switch (status) {
|
||||
case 'inactive':
|
||||
agent.active = false;
|
||||
agent.last_checkin = new Date(new Date().getTime() - inactivityTimeout * 1000).toISOString();
|
||||
break;
|
||||
case 'enrolling':
|
||||
agent.last_checkin = null;
|
||||
|
@ -78,6 +85,10 @@ function setAgentStatus(agent: any, status: AgentStatus) {
|
|||
case 'unenrolling':
|
||||
agent.unenrollment_started_at = new Date().toISOString();
|
||||
break;
|
||||
case 'unenrolled':
|
||||
agent.unenrolled_at = new Date().toISOString();
|
||||
agent.active = false;
|
||||
break;
|
||||
case 'error':
|
||||
agent.last_checkin_status = 'ERROR';
|
||||
break;
|
||||
|
@ -141,7 +152,7 @@ function createAgentWithStatus({
|
|||
user_provided_metadata: {},
|
||||
enrolled_at: new Date().toISOString(),
|
||||
last_checkin: new Date().toISOString(),
|
||||
tags: ['script_create_agents'],
|
||||
tags: ['script_create_agents', status],
|
||||
};
|
||||
|
||||
return setAgentStatus(baseAgent, status);
|
||||
|
@ -248,6 +259,7 @@ async function createAgentPolicy(id: string) {
|
|||
namespace: 'default',
|
||||
description: '',
|
||||
monitoring_enabled: ['logs'],
|
||||
inactivity_timeout: inactivityTimeout,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: kbnAuth,
|
||||
|
@ -304,7 +316,7 @@ export async function run() {
|
|||
|
||||
logger.info('Creating agent policy');
|
||||
|
||||
const agentPolicyId = uuid();
|
||||
const agentPolicyId = 'script-create-agent-' + uuid();
|
||||
const agentPolicy = await createAgentPolicy(agentPolicyId);
|
||||
logger.info(`Created agent policy ${agentPolicy.item.id}`);
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ export interface AgentUsage {
|
|||
healthy: number;
|
||||
unhealthy: number;
|
||||
offline: number;
|
||||
inactive: number;
|
||||
unenrolled: number;
|
||||
total_all_statuses: number;
|
||||
updating: number;
|
||||
}
|
||||
|
@ -31,19 +33,23 @@ export const getAgentUsage = async (
|
|||
healthy: 0,
|
||||
unhealthy: 0,
|
||||
offline: 0,
|
||||
inactive: 0,
|
||||
unenrolled: 0,
|
||||
total_all_statuses: 0,
|
||||
updating: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const { total, inactive, online, error, offline, updating } =
|
||||
await AgentService.getAgentStatusForAgentPolicy(esClient);
|
||||
const { total, inactive, online, error, offline, updating, unenrolled } =
|
||||
await AgentService.getAgentStatusForAgentPolicy(esClient, soClient);
|
||||
return {
|
||||
total_enrolled: total,
|
||||
healthy: online,
|
||||
unhealthy: error,
|
||||
offline,
|
||||
total_all_statuses: total + inactive,
|
||||
inactive,
|
||||
unenrolled,
|
||||
total_all_statuses: total + unenrolled,
|
||||
updating,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -70,6 +70,7 @@ export const getFleetServerUsage = async (
|
|||
|
||||
const { total, inactive, online, error, updating, offline } = await getAgentStatusForAgentPolicy(
|
||||
esClient,
|
||||
soClient,
|
||||
undefined,
|
||||
Array.from(policyIds)
|
||||
.map((policyId) => `(policy_id:"${policyId}")`)
|
||||
|
|
|
@ -34,7 +34,7 @@ export const getAllFleetServerAgents = async (
|
|||
|
||||
let agentsResponse;
|
||||
try {
|
||||
agentsResponse = await getAgentsByKuery(esClient, {
|
||||
agentsResponse = await getAgentsByKuery(esClient, soClient, {
|
||||
showInactive: false,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
kuery: `${AGENTS_PREFIX}.policy_id:${agentPoliciesIds.map((id) => `"${id}"`).join(' or ')}`,
|
||||
|
|
|
@ -137,6 +137,18 @@ export function registerFleetUsageCollector(
|
|||
description: 'The total number of enrolled agents currently offline',
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description: 'The total number of of enrolled agents currently inactive',
|
||||
},
|
||||
},
|
||||
unenrolled: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description: 'The total number of agents currently unenrolled',
|
||||
},
|
||||
},
|
||||
total_all_statuses: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { PluginInitializerContext } from '@kbn/core/server';
|
|||
|
||||
import { FleetPlugin } from './plugin';
|
||||
|
||||
export { buildAgentStatusRuntimeField } from './services/agents/build_status_runtime_field';
|
||||
export type {
|
||||
AgentService,
|
||||
AgentClient,
|
||||
|
|
|
@ -147,7 +147,7 @@ describe('fleet usage telemetry', () => {
|
|||
},
|
||||
{
|
||||
create: {
|
||||
_id: 'inactive',
|
||||
_id: 'unenrolled',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -251,6 +251,8 @@ describe('fleet usage telemetry', () => {
|
|||
total_enrolled: 2,
|
||||
healthy: 0,
|
||||
unhealthy: 0,
|
||||
inactive: 0,
|
||||
unenrolled: 1,
|
||||
offline: 2,
|
||||
total_all_statuses: 3,
|
||||
updating: 0,
|
||||
|
|
|
@ -346,7 +346,7 @@ export class FleetPlugin
|
|||
const coreContext = await context.core;
|
||||
const authz = await getAuthzFromRequest(request);
|
||||
const esClient = coreContext.elasticsearch.client;
|
||||
|
||||
const soClient = coreContext.savedObjects.getClient();
|
||||
const routeRequiredAuthz = getRouteRequiredAuthz(request.route.method, request.route.path);
|
||||
const routeAuthz = routeRequiredAuthz
|
||||
? calculateRouteAuthz(authz, routeRequiredAuthz)
|
||||
|
@ -359,7 +359,7 @@ export class FleetPlugin
|
|||
|
||||
return {
|
||||
get agentClient() {
|
||||
const agentService = plugin.setupAgentService(esClient.asInternalUser);
|
||||
const agentService = plugin.setupAgentService(esClient.asInternalUser, soClient);
|
||||
|
||||
return {
|
||||
asCurrentUser: agentService.asScoped(request),
|
||||
|
@ -502,6 +502,7 @@ export class FleetPlugin
|
|||
}
|
||||
})();
|
||||
|
||||
const internalSoClient = new SavedObjectsClient(core.savedObjects.createInternalRepository());
|
||||
return {
|
||||
authz: {
|
||||
fromRequest: getAuthzFromRequest,
|
||||
|
@ -510,9 +511,12 @@ export class FleetPlugin
|
|||
esIndexPatternService: new ESIndexPatternSavedObjectService(),
|
||||
packageService: this.setupPackageService(
|
||||
core.elasticsearch.client.asInternalUser,
|
||||
new SavedObjectsClient(core.savedObjects.createInternalRepository())
|
||||
internalSoClient
|
||||
),
|
||||
agentService: this.setupAgentService(
|
||||
core.elasticsearch.client.asInternalUser,
|
||||
internalSoClient
|
||||
),
|
||||
agentService: this.setupAgentService(core.elasticsearch.client.asInternalUser),
|
||||
agentPolicyService: {
|
||||
get: agentPolicyService.get,
|
||||
list: agentPolicyService.list,
|
||||
|
@ -536,12 +540,15 @@ export class FleetPlugin
|
|||
this.fleetStatus$.complete();
|
||||
}
|
||||
|
||||
private setupAgentService(internalEsClient: ElasticsearchClient): AgentService {
|
||||
private setupAgentService(
|
||||
internalEsClient: ElasticsearchClient,
|
||||
internalSoClient: SavedObjectsClientContract
|
||||
): AgentService {
|
||||
if (this.agentService) {
|
||||
return this.agentService;
|
||||
}
|
||||
|
||||
this.agentService = new AgentServiceImpl(internalEsClient);
|
||||
this.agentService = new AgentServiceImpl(internalEsClient, internalSoClient);
|
||||
return this.agentService;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,9 +27,11 @@ export const postNewAgentActionHandlerBuilder = function (
|
|||
> {
|
||||
return async (context, request, response) => {
|
||||
try {
|
||||
const esClient = (await context.core).elasticsearch.client.asInternalUser;
|
||||
const core = await context.core;
|
||||
const esClient = core.elasticsearch.client.asInternalUser;
|
||||
const soClient = core.savedObjects.client;
|
||||
|
||||
const agent = await actionsService.getAgent(esClient, request.params.agentId);
|
||||
const agent = await actionsService.getAgent(esClient, soClient, request.params.agentId);
|
||||
|
||||
const newAgentAction = request.body.action;
|
||||
|
||||
|
|
|
@ -52,10 +52,11 @@ export const getAgentHandler: RequestHandler<
|
|||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
|
||||
try {
|
||||
const body: GetOneAgentResponse = {
|
||||
item: await AgentService.getAgentById(esClient, request.params.agentId),
|
||||
item: await AgentService.getAgentById(esClient, soClient, request.params.agentId),
|
||||
};
|
||||
|
||||
return response.ok({ body });
|
||||
|
@ -103,6 +104,7 @@ export const updateAgentHandler: RequestHandler<
|
|||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
|
||||
const partialAgent: any = {};
|
||||
if (request.body.user_provided_metadata) {
|
||||
|
@ -115,7 +117,7 @@ export const updateAgentHandler: RequestHandler<
|
|||
try {
|
||||
await AgentService.updateAgent(esClient, request.params.agentId, partialAgent);
|
||||
const body = {
|
||||
item: await AgentService.getAgentById(esClient, request.params.agentId),
|
||||
item: await AgentService.getAgentById(esClient, soClient, request.params.agentId),
|
||||
};
|
||||
|
||||
return response.ok({ body });
|
||||
|
@ -163,9 +165,16 @@ export const getAgentsHandler: RequestHandler<
|
|||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
|
||||
try {
|
||||
const { agents, total, page, perPage } = await AgentService.getAgentsByKuery(esClient, {
|
||||
const {
|
||||
agents,
|
||||
total,
|
||||
page,
|
||||
perPage,
|
||||
totalInactive = 0,
|
||||
} = await AgentService.getAgentsByKuery(esClient, soClient, {
|
||||
page: request.query.page,
|
||||
perPage: request.query.perPage,
|
||||
showInactive: request.query.showInactive,
|
||||
|
@ -173,12 +182,8 @@ export const getAgentsHandler: RequestHandler<
|
|||
kuery: request.query.kuery,
|
||||
sortField: request.query.sortField,
|
||||
sortOrder: request.query.sortOrder,
|
||||
getTotalInactive: true,
|
||||
});
|
||||
const totalInactive = request.query.showInactive
|
||||
? await AgentService.countInactiveAgents(esClient, {
|
||||
kuery: request.query.kuery,
|
||||
})
|
||||
: 0;
|
||||
|
||||
const body: GetAgentsResponse = {
|
||||
list: agents, // deprecated
|
||||
|
@ -271,9 +276,11 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler<
|
|||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
try {
|
||||
const results = await AgentService.getAgentStatusForAgentPolicy(
|
||||
esClient,
|
||||
soClient,
|
||||
request.query.policyId,
|
||||
request.query.kuery
|
||||
);
|
||||
|
|
|
@ -46,7 +46,7 @@ export const postAgentUpgradeHandler: RequestHandler<
|
|||
});
|
||||
}
|
||||
try {
|
||||
const agent = await getAgentById(esClient, request.params.agentId);
|
||||
const agent = await getAgentById(esClient, soClient, request.params.agentId);
|
||||
|
||||
const fleetServerAgents = await getAllFleetServerAgents(soClient, esClient);
|
||||
const agentIsFleetServer = fleetServerAgents.some(
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import type { RequestHandler, ResponseHeaders, ElasticsearchClient } from '@kbn/core/server';
|
||||
import type {
|
||||
RequestHandler,
|
||||
ResponseHeaders,
|
||||
ElasticsearchClient,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import pMap from 'p-map';
|
||||
import { safeDump } from 'js-yaml';
|
||||
|
||||
|
@ -46,12 +51,13 @@ import { createAgentPolicyWithPackages } from '../../services/agent_policy_creat
|
|||
|
||||
async function populateAssignedAgentsCount(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentPolicies: AgentPolicy[]
|
||||
) {
|
||||
await pMap(
|
||||
agentPolicies,
|
||||
(agentPolicy: GetAgentPoliciesResponseItem) =>
|
||||
getAgentsByKuery(esClient, {
|
||||
getAgentsByKuery(esClient, soClient, {
|
||||
showInactive: false,
|
||||
perPage: 0,
|
||||
page: 1,
|
||||
|
@ -83,7 +89,7 @@ export const getAgentPoliciesHandler: FleetRequestHandler<
|
|||
perPage,
|
||||
};
|
||||
|
||||
await populateAssignedAgentsCount(esClient, items);
|
||||
await populateAssignedAgentsCount(esClient, soClient, items);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
|
@ -110,7 +116,7 @@ export const bulkGetAgentPoliciesHandler: FleetRequestHandler<
|
|||
items,
|
||||
};
|
||||
|
||||
await populateAssignedAgentsCount(esClient, items);
|
||||
await populateAssignedAgentsCount(esClient, soClient, items);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
|
|
|
@ -569,4 +569,59 @@ describe('agent policy', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInactivityTimeouts', () => {
|
||||
const createPolicySO = (id: string, inactivityTimeout: number) => ({
|
||||
id,
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: { inactivity_timeout: inactivityTimeout },
|
||||
references: [],
|
||||
score: 1,
|
||||
});
|
||||
|
||||
const createMockSoClientThatReturns = (policies: Array<ReturnType<typeof createPolicySO>>) => {
|
||||
const mockSoClient = savedObjectsClientMock.create();
|
||||
mockSoClient.find.mockResolvedValue({
|
||||
saved_objects: policies,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: policies.length,
|
||||
});
|
||||
return mockSoClient;
|
||||
};
|
||||
|
||||
it('should return empty array if no policies with inactivity timeouts', async () => {
|
||||
const mockSoClient = createMockSoClientThatReturns([]);
|
||||
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([]);
|
||||
});
|
||||
it('should return single inactivity timeout', async () => {
|
||||
const mockSoClient = createMockSoClientThatReturns([createPolicySO('policy1', 1000)]);
|
||||
|
||||
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([
|
||||
{ inactivityTimeout: 1000, policyIds: ['policy1'] },
|
||||
]);
|
||||
});
|
||||
it('should return group policies with same inactivity timeout', async () => {
|
||||
const mockSoClient = createMockSoClientThatReturns([
|
||||
createPolicySO('policy1', 1000),
|
||||
createPolicySO('policy2', 1000),
|
||||
]);
|
||||
|
||||
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([
|
||||
{ inactivityTimeout: 1000, policyIds: ['policy1', 'policy2'] },
|
||||
]);
|
||||
});
|
||||
it('should return handle single and grouped policies', async () => {
|
||||
const mockSoClient = createMockSoClientThatReturns([
|
||||
createPolicySO('policy1', 1000),
|
||||
createPolicySO('policy2', 1000),
|
||||
createPolicySO('policy3', 2000),
|
||||
]);
|
||||
|
||||
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([
|
||||
{ inactivityTimeout: 1000, policyIds: ['policy1', 'policy2'] },
|
||||
{ inactivityTimeout: 2000, policyIds: ['policy3'] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { omit, isEqual, keyBy } from 'lodash';
|
||||
import { omit, isEqual, keyBy, groupBy } from 'lodash';
|
||||
import uuidv5 from 'uuid/v5';
|
||||
import { safeDump } from 'js-yaml';
|
||||
import pMap from 'p-map';
|
||||
|
@ -658,7 +658,7 @@ class AgentPolicyService {
|
|||
throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`);
|
||||
}
|
||||
|
||||
const { total } = await getAgentsByKuery(esClient, {
|
||||
const { total } = await getAgentsByKuery(esClient, soClient, {
|
||||
showInactive: false,
|
||||
perPage: 0,
|
||||
page: 1,
|
||||
|
@ -1031,6 +1031,25 @@ class AgentPolicyService {
|
|||
|
||||
return res;
|
||||
}
|
||||
|
||||
public async getInactivityTimeouts(
|
||||
soClient: SavedObjectsClientContract
|
||||
): Promise<Array<{ policyIds: string[]; inactivityTimeout: number }>> {
|
||||
const findRes = await soClient.find<AgentPolicySOAttributes>({
|
||||
type: SAVED_OBJECT_TYPE,
|
||||
page: 1,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
filter: `${SAVED_OBJECT_TYPE}.attributes.inactivity_timeout: *`,
|
||||
fields: [`inactivity_timeout`],
|
||||
});
|
||||
|
||||
const groupedResults = groupBy(findRes.saved_objects, (so) => so.attributes.inactivity_timeout);
|
||||
|
||||
return Object.entries(groupedResults).map(([inactivityTimeout, policies]) => ({
|
||||
inactivityTimeout: parseInt(inactivityTimeout, 10),
|
||||
policyIds: policies.map((policy) => policy.id),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const agentPolicyService = new AgentPolicyService();
|
||||
|
|
|
@ -14,31 +14,47 @@ import type { AgentPolicy } from '../../types';
|
|||
export function createClientMock() {
|
||||
const agentInHostedDoc = {
|
||||
_id: 'agent-in-hosted-policy',
|
||||
_index: 'index',
|
||||
_source: {
|
||||
policy_id: 'hosted-agent-policy',
|
||||
local_metadata: { elastic: { agent: { version: '8.4.0', upgradeable: true } } },
|
||||
},
|
||||
fields: {
|
||||
status: ['online'],
|
||||
},
|
||||
};
|
||||
const agentInHostedDoc2 = {
|
||||
_id: 'agent-in-hosted-policy2',
|
||||
_index: 'index',
|
||||
_source: {
|
||||
policy_id: 'hosted-agent-policy',
|
||||
local_metadata: { elastic: { agent: { version: '8.4.0', upgradeable: true } } },
|
||||
},
|
||||
fields: {
|
||||
status: ['online'],
|
||||
},
|
||||
};
|
||||
const agentInRegularDoc = {
|
||||
_id: 'agent-in-regular-policy',
|
||||
_index: 'index',
|
||||
_source: {
|
||||
policy_id: 'regular-agent-policy',
|
||||
local_metadata: { elastic: { agent: { version: '8.4.0', upgradeable: true } } },
|
||||
},
|
||||
fields: {
|
||||
status: ['online'],
|
||||
},
|
||||
};
|
||||
const agentInRegularDoc2 = {
|
||||
_id: 'agent-in-regular-policy2',
|
||||
_index: 'index',
|
||||
_source: {
|
||||
policy_id: 'regular-agent-policy',
|
||||
local_metadata: { elastic: { agent: { version: '8.4.0', upgradeable: true } } },
|
||||
},
|
||||
fields: {
|
||||
status: ['online'],
|
||||
},
|
||||
};
|
||||
const regularAgentPolicySO = {
|
||||
id: 'regular-agent-policy',
|
||||
|
@ -74,6 +90,13 @@ export function createClientMock() {
|
|||
};
|
||||
});
|
||||
|
||||
soClientMock.find.mockResolvedValue({
|
||||
saved_objects: [],
|
||||
total: 0,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
// @ts-expect-error
|
||||
esClientMock.get.mockResponseImplementation(({ id }) => {
|
||||
|
@ -124,7 +147,24 @@ export function createClientMock() {
|
|||
};
|
||||
});
|
||||
|
||||
esClientMock.search.mockResolvedValue({ hits: { hits: [] } } as any);
|
||||
esClientMock.search.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
failed: 0,
|
||||
successful: 1,
|
||||
total: 1,
|
||||
},
|
||||
hits: {
|
||||
hits: [agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2],
|
||||
total: {
|
||||
value: 3,
|
||||
relation: 'eq',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
soClient: soClientMock,
|
||||
|
|
|
@ -191,7 +191,7 @@ export abstract class ActionRunner {
|
|||
const perPage = this.actionParams.batchSize ?? SO_SEARCH_LIMIT;
|
||||
|
||||
const getAgents = () =>
|
||||
getAgentsByKuery(this.esClient, {
|
||||
getAgentsByKuery(this.esClient, this.soClient, {
|
||||
kuery: this.actionParams.kuery,
|
||||
showInactive: this.actionParams.showInactive ?? false,
|
||||
page: 1,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { appContextService } from '../app_context';
|
||||
import type {
|
||||
|
@ -287,7 +287,11 @@ export async function cancelAgentAction(esClient: ElasticsearchClient, actionId:
|
|||
}
|
||||
|
||||
export interface ActionsService {
|
||||
getAgent: (esClient: ElasticsearchClient, agentId: string) => Promise<Agent>;
|
||||
getAgent: (
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentId: string
|
||||
) => Promise<Agent>;
|
||||
|
||||
cancelAgentAction: (esClient: ElasticsearchClient, actionId: string) => Promise<AgentAction>;
|
||||
|
||||
|
|
|
@ -9,8 +9,12 @@ jest.mock('../security');
|
|||
jest.mock('./crud');
|
||||
jest.mock('./status');
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { elasticsearchServiceMock, httpServerMock } from '@kbn/core/server/mocks';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
httpServerMock,
|
||||
savedObjectsClientMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
|
||||
import { FleetUnauthorizedError } from '../../errors';
|
||||
|
||||
|
@ -36,7 +40,8 @@ describe('AgentService', () => {
|
|||
describe('asScoped', () => {
|
||||
describe('without required privilege', () => {
|
||||
const agentClient = new AgentServiceImpl(
|
||||
elasticsearchServiceMock.createElasticsearchClient()
|
||||
elasticsearchServiceMock.createElasticsearchClient(),
|
||||
savedObjectsClientMock.create()
|
||||
).asScoped(httpServerMock.createKibanaRequest());
|
||||
|
||||
beforeEach(() =>
|
||||
|
@ -99,7 +104,8 @@ describe('AgentService', () => {
|
|||
|
||||
describe('with required privilege', () => {
|
||||
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
const agentClient = new AgentServiceImpl(mockEsClient).asScoped(
|
||||
const mockSoClient = savedObjectsClientMock.create();
|
||||
const agentClient = new AgentServiceImpl(mockEsClient, mockSoClient).asScoped(
|
||||
httpServerMock.createKibanaRequest()
|
||||
);
|
||||
|
||||
|
@ -128,20 +134,22 @@ describe('AgentService', () => {
|
|||
)
|
||||
);
|
||||
|
||||
expectApisToCallServicesSuccessfully(mockEsClient, agentClient);
|
||||
expectApisToCallServicesSuccessfully(mockEsClient, mockSoClient, agentClient);
|
||||
});
|
||||
});
|
||||
|
||||
describe('asInternalUser', () => {
|
||||
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
const agentClient = new AgentServiceImpl(mockEsClient).asInternalUser;
|
||||
const mockSoClient = savedObjectsClientMock.create();
|
||||
const agentClient = new AgentServiceImpl(mockEsClient, mockSoClient).asInternalUser;
|
||||
|
||||
expectApisToCallServicesSuccessfully(mockEsClient, agentClient);
|
||||
expectApisToCallServicesSuccessfully(mockEsClient, mockSoClient, agentClient);
|
||||
});
|
||||
});
|
||||
|
||||
function expectApisToCallServicesSuccessfully(
|
||||
mockEsClient: ElasticsearchClient,
|
||||
mockSoClient: jest.Mocked<SavedObjectsClientContract>,
|
||||
agentClient: AgentClient
|
||||
) {
|
||||
test('client.listAgents calls getAgentsByKuery and returns results', async () => {
|
||||
|
@ -149,7 +157,7 @@ function expectApisToCallServicesSuccessfully(
|
|||
await expect(agentClient.listAgents({ showInactive: true })).resolves.toEqual(
|
||||
'getAgentsByKuery success'
|
||||
);
|
||||
expect(mockGetAgentsByKuery).toHaveBeenCalledWith(mockEsClient, {
|
||||
expect(mockGetAgentsByKuery).toHaveBeenCalledWith(mockEsClient, mockSoClient, {
|
||||
showInactive: true,
|
||||
});
|
||||
});
|
||||
|
@ -157,7 +165,7 @@ function expectApisToCallServicesSuccessfully(
|
|||
test('client.getAgent calls getAgentById and returns results', async () => {
|
||||
mockGetAgentById.mockResolvedValue('getAgentById success');
|
||||
await expect(agentClient.getAgent('foo-id')).resolves.toEqual('getAgentById success');
|
||||
expect(mockGetAgentById).toHaveBeenCalledWith(mockEsClient, 'foo-id');
|
||||
expect(mockGetAgentById).toHaveBeenCalledWith(mockEsClient, mockSoClient, 'foo-id');
|
||||
});
|
||||
|
||||
test('client.getAgentStatusById calls getAgentStatusById and returns results', async () => {
|
||||
|
@ -165,7 +173,7 @@ function expectApisToCallServicesSuccessfully(
|
|||
await expect(agentClient.getAgentStatusById('foo-id')).resolves.toEqual(
|
||||
'getAgentStatusById success'
|
||||
);
|
||||
expect(mockGetAgentStatusById).toHaveBeenCalledWith(mockEsClient, 'foo-id');
|
||||
expect(mockGetAgentStatusById).toHaveBeenCalledWith(mockEsClient, mockSoClient, 'foo-id');
|
||||
});
|
||||
|
||||
test('client.getAgentStatusForAgentPolicy calls getAgentStatusForAgentPolicy and returns results', async () => {
|
||||
|
@ -175,6 +183,7 @@ function expectApisToCallServicesSuccessfully(
|
|||
);
|
||||
expect(mockGetAgentStatusForAgentPolicy).toHaveBeenCalledWith(
|
||||
mockEsClient,
|
||||
mockSoClient,
|
||||
'foo-id',
|
||||
'foo-filter'
|
||||
);
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server';
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
KibanaRequest,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import type { AgentStatus, ListWithKuery } from '../../types';
|
||||
import type { Agent, GetAgentStatusResponse } from '../../../common/types';
|
||||
|
@ -82,6 +86,7 @@ export interface AgentClient {
|
|||
class AgentClientImpl implements AgentClient {
|
||||
constructor(
|
||||
private readonly internalEsClient: ElasticsearchClient,
|
||||
private readonly soClient: SavedObjectsClientContract,
|
||||
private readonly preflightCheck?: () => void | Promise<void>
|
||||
) {}
|
||||
|
||||
|
@ -91,22 +96,27 @@ class AgentClientImpl implements AgentClient {
|
|||
}
|
||||
) {
|
||||
await this.#runPreflight();
|
||||
return getAgentsByKuery(this.internalEsClient, options);
|
||||
return getAgentsByKuery(this.internalEsClient, this.soClient, options);
|
||||
}
|
||||
|
||||
public async getAgent(agentId: string) {
|
||||
await this.#runPreflight();
|
||||
return getAgentById(this.internalEsClient, agentId);
|
||||
return getAgentById(this.internalEsClient, this.soClient, agentId);
|
||||
}
|
||||
|
||||
public async getAgentStatusById(agentId: string) {
|
||||
await this.#runPreflight();
|
||||
return getAgentStatusById(this.internalEsClient, agentId);
|
||||
return getAgentStatusById(this.internalEsClient, this.soClient, agentId);
|
||||
}
|
||||
|
||||
public async getAgentStatusForAgentPolicy(agentPolicyId?: string, filterKuery?: string) {
|
||||
await this.#runPreflight();
|
||||
return getAgentStatusForAgentPolicy(this.internalEsClient, agentPolicyId, filterKuery);
|
||||
return getAgentStatusForAgentPolicy(
|
||||
this.internalEsClient,
|
||||
this.soClient,
|
||||
agentPolicyId,
|
||||
filterKuery
|
||||
);
|
||||
}
|
||||
|
||||
#runPreflight = async () => {
|
||||
|
@ -120,7 +130,10 @@ class AgentClientImpl implements AgentClient {
|
|||
* @internal
|
||||
*/
|
||||
export class AgentServiceImpl implements AgentService {
|
||||
constructor(private readonly internalEsClient: ElasticsearchClient) {}
|
||||
constructor(
|
||||
private readonly internalEsClient: ElasticsearchClient,
|
||||
private readonly soClient: SavedObjectsClientContract
|
||||
) {}
|
||||
|
||||
public asScoped(req: KibanaRequest) {
|
||||
const preflightCheck = async () => {
|
||||
|
@ -132,10 +145,10 @@ export class AgentServiceImpl implements AgentService {
|
|||
}
|
||||
};
|
||||
|
||||
return new AgentClientImpl(this.internalEsClient, preflightCheck);
|
||||
return new AgentClientImpl(this.internalEsClient, this.soClient, preflightCheck);
|
||||
}
|
||||
|
||||
public get asInternalUser() {
|
||||
return new AgentClientImpl(this.internalEsClient);
|
||||
return new AgentClientImpl(this.internalEsClient, this.soClient);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* 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 type { InactivityTimeouts } from './build_status_runtime_field';
|
||||
import { _buildStatusRuntimeField } from './build_status_runtime_field';
|
||||
|
||||
describe('buildStatusRuntimeField', () => {
|
||||
const now = 1234567890123;
|
||||
beforeAll(() => {
|
||||
global.Date.now = jest.fn(() => now);
|
||||
});
|
||||
it('should build the correct runtime field if there are no inactivity timeouts', () => {
|
||||
const inactivityTimeouts: InactivityTimeouts = [];
|
||||
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
|
||||
expect(runtimeField).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"status": Object {
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "
|
||||
long lastCheckinMillis = doc['last_checkin'].size() > 0
|
||||
? doc['last_checkin'].value.toInstant().toEpochMilli()
|
||||
: -1;
|
||||
if (doc['active'].size() > 0 && doc['active'].value == false) {
|
||||
emit('unenrolled');
|
||||
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && false) {
|
||||
emit('inactive');
|
||||
} else if (
|
||||
lastCheckinMillis > 0
|
||||
&& lastCheckinMillis
|
||||
< (1234567590123L)
|
||||
) {
|
||||
emit('offline');
|
||||
} else if (
|
||||
doc['policy_revision_idx'].size() == 0 || (
|
||||
doc['upgrade_started_at'].size() > 0 &&
|
||||
doc['upgraded_at'].size() == 0
|
||||
)
|
||||
) {
|
||||
emit('updating');
|
||||
} else if (doc['last_checkin'].size() == 0) {
|
||||
emit('enrolling');
|
||||
} else if (doc['unenrollment_started_at'].size() > 0) {
|
||||
emit('unenrolling');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'error'
|
||||
) {
|
||||
emit('error');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
|
||||
) {
|
||||
emit('degraded');
|
||||
} else {
|
||||
emit('online');
|
||||
}",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should build the correct runtime field if there are no inactivity timeouts (prefix)', () => {
|
||||
const inactivityTimeouts: InactivityTimeouts = [];
|
||||
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts, 'my.prefix.');
|
||||
expect(runtimeField).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"status": Object {
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "
|
||||
long lastCheckinMillis = doc['my.prefix.last_checkin'].size() > 0
|
||||
? doc['my.prefix.last_checkin'].value.toInstant().toEpochMilli()
|
||||
: -1;
|
||||
if (doc['my.prefix.active'].size() > 0 && doc['my.prefix.active'].value == false) {
|
||||
emit('unenrolled');
|
||||
} else if (lastCheckinMillis > 0 && doc['my.prefix.policy_id'].size() > 0 && false) {
|
||||
emit('inactive');
|
||||
} else if (
|
||||
lastCheckinMillis > 0
|
||||
&& lastCheckinMillis
|
||||
< (1234567590123L)
|
||||
) {
|
||||
emit('offline');
|
||||
} else if (
|
||||
doc['my.prefix.policy_revision_idx'].size() == 0 || (
|
||||
doc['my.prefix.upgrade_started_at'].size() > 0 &&
|
||||
doc['my.prefix.upgraded_at'].size() == 0
|
||||
)
|
||||
) {
|
||||
emit('updating');
|
||||
} else if (doc['my.prefix.last_checkin'].size() == 0) {
|
||||
emit('enrolling');
|
||||
} else if (doc['my.prefix.unenrollment_started_at'].size() > 0) {
|
||||
emit('unenrolling');
|
||||
} else if (
|
||||
doc['my.prefix.last_checkin_status'].size() > 0 &&
|
||||
doc['my.prefix.last_checkin_status'].value.toLowerCase() == 'error'
|
||||
) {
|
||||
emit('error');
|
||||
} else if (
|
||||
doc['my.prefix.last_checkin_status'].size() > 0 &&
|
||||
doc['my.prefix.last_checkin_status'].value.toLowerCase() == 'degraded'
|
||||
) {
|
||||
emit('degraded');
|
||||
} else {
|
||||
emit('online');
|
||||
}",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should build the correct runtime field if there is one inactivity timeout', () => {
|
||||
const inactivityTimeouts: InactivityTimeouts = [
|
||||
{
|
||||
inactivityTimeout: 300,
|
||||
policyIds: ['policy-1'],
|
||||
},
|
||||
];
|
||||
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
|
||||
expect(runtimeField).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"status": Object {
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "
|
||||
long lastCheckinMillis = doc['last_checkin'].size() > 0
|
||||
? doc['last_checkin'].value.toInstant().toEpochMilli()
|
||||
: -1;
|
||||
if (doc['active'].size() > 0 && doc['active'].value == false) {
|
||||
emit('unenrolled');
|
||||
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && (doc['policy_id'].value == 'policy-1') && lastCheckinMillis < 1234567590123L) {
|
||||
emit('inactive');
|
||||
} else if (
|
||||
lastCheckinMillis > 0
|
||||
&& lastCheckinMillis
|
||||
< (1234567590123L)
|
||||
) {
|
||||
emit('offline');
|
||||
} else if (
|
||||
doc['policy_revision_idx'].size() == 0 || (
|
||||
doc['upgrade_started_at'].size() > 0 &&
|
||||
doc['upgraded_at'].size() == 0
|
||||
)
|
||||
) {
|
||||
emit('updating');
|
||||
} else if (doc['last_checkin'].size() == 0) {
|
||||
emit('enrolling');
|
||||
} else if (doc['unenrollment_started_at'].size() > 0) {
|
||||
emit('unenrolling');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'error'
|
||||
) {
|
||||
emit('error');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
|
||||
) {
|
||||
emit('degraded');
|
||||
} else {
|
||||
emit('online');
|
||||
}",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should build the correct runtime field if there are multiple inactivity timeouts with same timeout', () => {
|
||||
const inactivityTimeouts: InactivityTimeouts = [
|
||||
{
|
||||
inactivityTimeout: 300,
|
||||
policyIds: ['policy-1', 'policy-2'],
|
||||
},
|
||||
];
|
||||
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
|
||||
expect(runtimeField).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"status": Object {
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "
|
||||
long lastCheckinMillis = doc['last_checkin'].size() > 0
|
||||
? doc['last_checkin'].value.toInstant().toEpochMilli()
|
||||
: -1;
|
||||
if (doc['active'].size() > 0 && doc['active'].value == false) {
|
||||
emit('unenrolled');
|
||||
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && (doc['policy_id'].value == 'policy-1' || doc['policy_id'].value == 'policy-2') && lastCheckinMillis < 1234567590123L) {
|
||||
emit('inactive');
|
||||
} else if (
|
||||
lastCheckinMillis > 0
|
||||
&& lastCheckinMillis
|
||||
< (1234567590123L)
|
||||
) {
|
||||
emit('offline');
|
||||
} else if (
|
||||
doc['policy_revision_idx'].size() == 0 || (
|
||||
doc['upgrade_started_at'].size() > 0 &&
|
||||
doc['upgraded_at'].size() == 0
|
||||
)
|
||||
) {
|
||||
emit('updating');
|
||||
} else if (doc['last_checkin'].size() == 0) {
|
||||
emit('enrolling');
|
||||
} else if (doc['unenrollment_started_at'].size() > 0) {
|
||||
emit('unenrolling');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'error'
|
||||
) {
|
||||
emit('error');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
|
||||
) {
|
||||
emit('degraded');
|
||||
} else {
|
||||
emit('online');
|
||||
}",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should build the correct runtime field if there are multiple inactivity timeouts with different timeout', () => {
|
||||
const inactivityTimeouts: InactivityTimeouts = [
|
||||
{
|
||||
inactivityTimeout: 300,
|
||||
policyIds: ['policy-1', 'policy-2'],
|
||||
},
|
||||
{
|
||||
inactivityTimeout: 400,
|
||||
policyIds: ['policy-3'],
|
||||
},
|
||||
];
|
||||
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
|
||||
expect(runtimeField).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"status": Object {
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "
|
||||
long lastCheckinMillis = doc['last_checkin'].size() > 0
|
||||
? doc['last_checkin'].value.toInstant().toEpochMilli()
|
||||
: -1;
|
||||
if (doc['active'].size() > 0 && doc['active'].value == false) {
|
||||
emit('unenrolled');
|
||||
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && (doc['policy_id'].value == 'policy-1' || doc['policy_id'].value == 'policy-2') && lastCheckinMillis < 1234567590123L || (doc['policy_id'].value == 'policy-3') && lastCheckinMillis < 1234567490123L) {
|
||||
emit('inactive');
|
||||
} else if (
|
||||
lastCheckinMillis > 0
|
||||
&& lastCheckinMillis
|
||||
< (1234567590123L)
|
||||
) {
|
||||
emit('offline');
|
||||
} else if (
|
||||
doc['policy_revision_idx'].size() == 0 || (
|
||||
doc['upgrade_started_at'].size() > 0 &&
|
||||
doc['upgraded_at'].size() == 0
|
||||
)
|
||||
) {
|
||||
emit('updating');
|
||||
} else if (doc['last_checkin'].size() == 0) {
|
||||
emit('enrolling');
|
||||
} else if (doc['unenrollment_started_at'].size() > 0) {
|
||||
emit('unenrolling');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'error'
|
||||
) {
|
||||
emit('error');
|
||||
} else if (
|
||||
doc['last_checkin_status'].size() > 0 &&
|
||||
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
|
||||
) {
|
||||
emit('degraded');
|
||||
} else {
|
||||
emit('online');
|
||||
}",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should build the same runtime field if path ends with. or not', () => {
|
||||
const inactivityTimeouts: InactivityTimeouts = [];
|
||||
const runtimeFieldWithDot = _buildStatusRuntimeField(inactivityTimeouts, 'my.prefix.');
|
||||
const runtimeFieldNoDot = _buildStatusRuntimeField(inactivityTimeouts, 'my.prefix');
|
||||
expect(runtimeFieldWithDot).toEqual(runtimeFieldNoDot);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 type * as estypes from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { AGENT_POLLING_THRESHOLD_MS } from '../../constants';
|
||||
import { agentPolicyService } from '../agent_policy';
|
||||
const MISSED_INTERVALS_BEFORE_OFFLINE = 10;
|
||||
const MS_BEFORE_OFFLINE = MISSED_INTERVALS_BEFORE_OFFLINE * AGENT_POLLING_THRESHOLD_MS;
|
||||
|
||||
export type InactivityTimeouts = Awaited<
|
||||
ReturnType<typeof agentPolicyService['getInactivityTimeouts']>
|
||||
>;
|
||||
|
||||
const _buildInactiveClause = (
|
||||
now: number,
|
||||
inactivityTimeouts: InactivityTimeouts,
|
||||
field: (path: string) => string
|
||||
) => {
|
||||
const policyClauses = inactivityTimeouts
|
||||
.map(({ inactivityTimeout, policyIds }) => {
|
||||
const inactivityTimeoutMs = inactivityTimeout * 1000;
|
||||
const policyOrs = policyIds
|
||||
.map((policyId) => `${field('policy_id')}.value == '${policyId}'`)
|
||||
.join(' || ');
|
||||
|
||||
return `(${policyOrs}) && lastCheckinMillis < ${now - inactivityTimeoutMs}L`;
|
||||
})
|
||||
.join(' || ');
|
||||
|
||||
const agentIsInactive = policyClauses.length ? `${policyClauses}` : 'false'; // if no policies have inactivity timeouts, then no agents are inactive
|
||||
|
||||
return `lastCheckinMillis > 0 && ${field('policy_id')}.size() > 0 && ${agentIsInactive}`;
|
||||
};
|
||||
|
||||
function _buildSource(inactivityTimeouts: InactivityTimeouts, pathPrefix?: string) {
|
||||
const normalizedPrefix = pathPrefix ? `${pathPrefix}${pathPrefix.endsWith('.') ? '' : '.'}` : '';
|
||||
const field = (path: string) => `doc['${normalizedPrefix + path}']`;
|
||||
const now = Date.now();
|
||||
return `
|
||||
long lastCheckinMillis = ${field('last_checkin')}.size() > 0
|
||||
? ${field('last_checkin')}.value.toInstant().toEpochMilli()
|
||||
: -1;
|
||||
if (${field('active')}.size() > 0 && ${field('active')}.value == false) {
|
||||
emit('unenrolled');
|
||||
} else if (${_buildInactiveClause(now, inactivityTimeouts, field)}) {
|
||||
emit('inactive');
|
||||
} else if (
|
||||
lastCheckinMillis > 0
|
||||
&& lastCheckinMillis
|
||||
< (${now - MS_BEFORE_OFFLINE}L)
|
||||
) {
|
||||
emit('offline');
|
||||
} else if (
|
||||
${field('policy_revision_idx')}.size() == 0 || (
|
||||
${field('upgrade_started_at')}.size() > 0 &&
|
||||
${field('upgraded_at')}.size() == 0
|
||||
)
|
||||
) {
|
||||
emit('updating');
|
||||
} else if (${field('last_checkin')}.size() == 0) {
|
||||
emit('enrolling');
|
||||
} else if (${field('unenrollment_started_at')}.size() > 0) {
|
||||
emit('unenrolling');
|
||||
} else if (
|
||||
${field('last_checkin_status')}.size() > 0 &&
|
||||
${field('last_checkin_status')}.value.toLowerCase() == 'error'
|
||||
) {
|
||||
emit('error');
|
||||
} else if (
|
||||
${field('last_checkin_status')}.size() > 0 &&
|
||||
${field('last_checkin_status')}.value.toLowerCase() == 'degraded'
|
||||
) {
|
||||
emit('degraded');
|
||||
} else {
|
||||
emit('online');
|
||||
}`;
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export function _buildStatusRuntimeField(
|
||||
inactivityTimeouts: InactivityTimeouts,
|
||||
pathPrefix?: string
|
||||
): NonNullable<estypes.SearchRequest['runtime_mappings']> {
|
||||
const source = _buildSource(inactivityTimeouts, pathPrefix);
|
||||
return {
|
||||
status: {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Build the runtime field to return the agent status
|
||||
// pathPrefix is used to prefix the field path in the source
|
||||
// pathPrefix is used by the endpoint team currently to run
|
||||
// agent queries against the endpoint metadata index
|
||||
export async function buildAgentStatusRuntimeField(
|
||||
soClient: SavedObjectsClientContract,
|
||||
pathPrefix?: string
|
||||
) {
|
||||
const inactivityTimeouts = await agentPolicyService.getInactivityTimeouts(soClient);
|
||||
|
||||
return _buildStatusRuntimeField(inactivityTimeouts, pathPrefix);
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import type { Agent } from '../../types';
|
||||
|
||||
|
@ -16,11 +17,13 @@ jest.mock('../../../common/services/is_agent_upgradeable', () => ({
|
|||
}));
|
||||
|
||||
describe('Agents CRUD test', () => {
|
||||
const soClientMock = savedObjectsClientMock.create();
|
||||
let esClientMock: ElasticsearchClient;
|
||||
let searchMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock = jest.fn();
|
||||
soClientMock.find = jest.fn().mockResolvedValue({ saved_objects: [] });
|
||||
esClientMock = {
|
||||
search: searchMock,
|
||||
openPointInTime: jest.fn().mockResolvedValue({ id: '1' }),
|
||||
|
@ -35,6 +38,9 @@ describe('Agents CRUD test', () => {
|
|||
hits: ids.map((id: string) => ({
|
||||
_id: id,
|
||||
_source: {},
|
||||
fields: {
|
||||
status: ['inactive'],
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
@ -54,7 +60,7 @@ describe('Agents CRUD test', () => {
|
|||
expect(searchMock).toHaveBeenCalledWith({
|
||||
aggs: { tags: { terms: { field: 'tags', size: 10000 } } },
|
||||
body: {
|
||||
query: { bool: { minimum_should_match: 1, should: [{ match: { active: true } }] } },
|
||||
query: expect.any(Object),
|
||||
},
|
||||
index: '.fleet-agents',
|
||||
size: 0,
|
||||
|
@ -122,7 +128,7 @@ describe('Agents CRUD test', () => {
|
|||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(getEsResponse(['1', '2', '3', '4', '5', 'up', '7'], 7))
|
||||
);
|
||||
const result = await getAgentsByKuery(esClientMock, {
|
||||
const result = await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showUpgradeable: true,
|
||||
showInactive: false,
|
||||
page: 1,
|
||||
|
@ -151,7 +157,7 @@ describe('Agents CRUD test', () => {
|
|||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(getEsResponse(['1', '2', '3', 'up', '5', 'up2', '7'], 7))
|
||||
);
|
||||
const result = await getAgentsByKuery(esClientMock, {
|
||||
const result = await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showUpgradeable: true,
|
||||
showInactive: false,
|
||||
page: 1,
|
||||
|
@ -187,7 +193,7 @@ describe('Agents CRUD test', () => {
|
|||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(getEsResponse(['up1', 'up2', 'up3', 'up4', 'up5', 'up6', '7'], 7))
|
||||
);
|
||||
const result = await getAgentsByKuery(esClientMock, {
|
||||
const result = await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showUpgradeable: true,
|
||||
showInactive: false,
|
||||
page: 2,
|
||||
|
@ -214,7 +220,7 @@ describe('Agents CRUD test', () => {
|
|||
searchMock.mockImplementationOnce(() =>
|
||||
Promise.resolve(getEsResponse(['1', '2', '3', 'up', '5'], 10001))
|
||||
);
|
||||
const result = await getAgentsByKuery(esClientMock, {
|
||||
const result = await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showUpgradeable: true,
|
||||
showInactive: false,
|
||||
page: 1,
|
||||
|
@ -239,7 +245,7 @@ describe('Agents CRUD test', () => {
|
|||
|
||||
it('should return second page', async () => {
|
||||
searchMock.mockImplementationOnce(() => Promise.resolve(getEsResponse(['6', '7'], 7)));
|
||||
const result = await getAgentsByKuery(esClientMock, {
|
||||
const result = await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showUpgradeable: false,
|
||||
showInactive: false,
|
||||
page: 2,
|
||||
|
@ -271,11 +277,11 @@ describe('Agents CRUD test', () => {
|
|||
|
||||
it('should pass secondary sort for default sort', async () => {
|
||||
searchMock.mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2'], 2)));
|
||||
await getAgentsByKuery(esClientMock, {
|
||||
await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showInactive: false,
|
||||
});
|
||||
|
||||
expect(searchMock.mock.calls[searchMock.mock.calls.length - 1][0].body.sort).toEqual([
|
||||
expect(searchMock.mock.calls.at(-1)[0].sort).toEqual([
|
||||
{ enrolled_at: { order: 'desc' } },
|
||||
{ 'local_metadata.host.hostname.keyword': { order: 'asc' } },
|
||||
]);
|
||||
|
@ -283,13 +289,11 @@ describe('Agents CRUD test', () => {
|
|||
|
||||
it('should not pass secondary sort for non-default sort', async () => {
|
||||
searchMock.mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2'], 2)));
|
||||
await getAgentsByKuery(esClientMock, {
|
||||
await getAgentsByKuery(esClientMock, soClientMock, {
|
||||
showInactive: false,
|
||||
sortField: 'policy_id',
|
||||
});
|
||||
expect(searchMock.mock.calls[searchMock.mock.calls.length - 1][0].body.sort).toEqual([
|
||||
{ policy_id: { order: 'desc' } },
|
||||
]);
|
||||
expect(searchMock.mock.calls.at(-1)[0].sort).toEqual([{ policy_id: { order: 'desc' } }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,14 +16,15 @@ import { appContextService, agentPolicyService } from '..';
|
|||
import type { FleetServerAgent } from '../../../common/types';
|
||||
import { SO_SEARCH_LIMIT } from '../../../common/constants';
|
||||
import { isAgentUpgradeable } from '../../../common/services';
|
||||
import { AGENTS_PREFIX, AGENTS_INDEX } from '../../constants';
|
||||
import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object';
|
||||
import { AGENTS_INDEX } from '../../constants';
|
||||
import { FleetError, isESClientError, AgentNotFoundError } from '../../errors';
|
||||
|
||||
import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers';
|
||||
|
||||
const ACTIVE_AGENT_CONDITION = 'active:true';
|
||||
const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`;
|
||||
import { buildAgentStatusRuntimeField } from './build_status_runtime_field';
|
||||
|
||||
const INACTIVE_AGENT_CONDITION = `status:inactive OR status:unenrolled`;
|
||||
const ACTIVE_AGENT_CONDITION = `NOT (${INACTIVE_AGENT_CONDITION})`;
|
||||
|
||||
function _joinFilters(filters: Array<string | undefined | KueryNode>): KueryNode | undefined {
|
||||
try {
|
||||
|
@ -71,13 +72,19 @@ export type GetAgentsOptions =
|
|||
perPage?: number;
|
||||
};
|
||||
|
||||
export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) {
|
||||
export async function getAgents(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: GetAgentsOptions
|
||||
) {
|
||||
let agents: Agent[] = [];
|
||||
if ('agentIds' in options) {
|
||||
agents = await getAgentsById(esClient, options.agentIds);
|
||||
agents = (await getAgentsById(esClient, soClient, options.agentIds)).filter(
|
||||
(maybeAgent) => !('notFound' in maybeAgent)
|
||||
) as Agent[];
|
||||
} else if ('kuery' in options) {
|
||||
agents = (
|
||||
await getAllAgentsByKuery(esClient, {
|
||||
await getAllAgentsByKuery(esClient, soClient, {
|
||||
kuery: options.kuery,
|
||||
showInactive: options.showInactive ?? false,
|
||||
})
|
||||
|
@ -180,8 +187,10 @@ export function getElasticsearchQuery(
|
|||
|
||||
export async function getAgentsByKuery(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: ListWithKuery & {
|
||||
showInactive: boolean;
|
||||
getTotalInactive?: boolean;
|
||||
sortField?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
pitId?: string;
|
||||
|
@ -192,6 +201,7 @@ export async function getAgentsByKuery(
|
|||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
totalInactive?: number;
|
||||
}> {
|
||||
const {
|
||||
page = 1,
|
||||
|
@ -203,6 +213,7 @@ export async function getAgentsByKuery(
|
|||
showUpgradeable,
|
||||
searchAfter,
|
||||
pitId,
|
||||
getTotalInactive = false,
|
||||
} = options;
|
||||
const filters = [];
|
||||
|
||||
|
@ -215,22 +226,24 @@ export async function getAgentsByKuery(
|
|||
}
|
||||
|
||||
const kueryNode = _joinFilters(filters);
|
||||
const body = kueryNode ? { query: toElasticsearchQuery(kueryNode) } : {};
|
||||
|
||||
const runtimeFields = await buildAgentStatusRuntimeField(soClient);
|
||||
|
||||
const isDefaultSort = sortField === 'enrolled_at' && sortOrder === 'desc';
|
||||
// if using default sorting (enrolled_at), adding a secondary sort on hostname, so that the results are not changing randomly in case many agents were enrolled at the same time
|
||||
const secondarySort: estypes.Sort = isDefaultSort
|
||||
? [{ 'local_metadata.host.hostname.keyword': { order: 'asc' } }]
|
||||
: [];
|
||||
const queryAgents = async (from: number, size: number) =>
|
||||
esClient.search<FleetServerAgent, {}>({
|
||||
esClient.search<FleetServerAgent, { totalInactive?: { doc_count: number } }>({
|
||||
from,
|
||||
size,
|
||||
track_total_hits: true,
|
||||
rest_total_hits_as_int: true,
|
||||
body: {
|
||||
...body,
|
||||
sort: [{ [sortField]: { order: sortOrder } }, ...secondarySort],
|
||||
},
|
||||
runtime_mappings: runtimeFields,
|
||||
fields: Object.keys(runtimeFields),
|
||||
sort: [{ [sortField]: { order: sortOrder } }, ...secondarySort],
|
||||
post_filter: kueryNode ? toElasticsearchQuery(kueryNode) : undefined,
|
||||
...(pitId
|
||||
? {
|
||||
pit: {
|
||||
|
@ -243,11 +256,28 @@ export async function getAgentsByKuery(
|
|||
ignore_unavailable: true,
|
||||
}),
|
||||
...(pitId && searchAfter ? { search_after: searchAfter, from: 0 } : {}),
|
||||
...(getTotalInactive && {
|
||||
aggregations: {
|
||||
totalInactive: {
|
||||
filter: { bool: { must: { terms: { status: ['inactive', 'unenrolled'] } } } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
const res = await queryAgents((page - 1) * perPage, perPage);
|
||||
let res;
|
||||
try {
|
||||
res = await queryAgents((page - 1) * perPage, perPage);
|
||||
} catch (err) {
|
||||
appContextService.getLogger().error(`Error getting agents by kuery: ${JSON.stringify(err)}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
let agents = res.hits.hits.map(searchHitToAgent);
|
||||
let total = res.hits.total as number;
|
||||
let totalInactive = 0;
|
||||
if (getTotalInactive && res.aggregations) {
|
||||
totalInactive = res.aggregations?.totalInactive?.doc_count ?? 0;
|
||||
}
|
||||
// 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) {
|
||||
|
@ -274,11 +304,13 @@ export async function getAgentsByKuery(
|
|||
total,
|
||||
page,
|
||||
perPage,
|
||||
...(getTotalInactive && { totalInactive }),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllAgentsByKuery(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: Omit<ListWithKuery, 'page' | 'perPage'> & {
|
||||
showInactive: boolean;
|
||||
}
|
||||
|
@ -286,7 +318,11 @@ export async function getAllAgentsByKuery(
|
|||
agents: Agent[];
|
||||
total: number;
|
||||
}> {
|
||||
const res = await getAgentsByKuery(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT });
|
||||
const res = await getAgentsByKuery(esClient, soClient, {
|
||||
...options,
|
||||
page: 1,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
return {
|
||||
agents: res.agents,
|
||||
|
@ -294,104 +330,108 @@ export async function getAllAgentsByKuery(
|
|||
};
|
||||
}
|
||||
|
||||
export async function countInactiveAgents(
|
||||
export async function getAgentById(
|
||||
esClient: ElasticsearchClient,
|
||||
options: Pick<ListWithKuery, 'kuery'>
|
||||
): Promise<number> {
|
||||
const { kuery } = options;
|
||||
const filters = [INACTIVE_AGENT_CONDITION];
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentId: string
|
||||
) {
|
||||
const [agentHit] = await getAgentsById(esClient, soClient, [agentId]);
|
||||
|
||||
if (kuery && kuery !== '') {
|
||||
filters.push(normalizeKuery(AGENTS_PREFIX, kuery));
|
||||
if ('notFound' in agentHit) {
|
||||
throw new AgentNotFoundError(`Agent ${agentId} not found`);
|
||||
}
|
||||
|
||||
const kueryNode = _joinFilters(filters);
|
||||
const body = kueryNode ? { query: toElasticsearchQuery(kueryNode) } : {};
|
||||
|
||||
const res = await esClient.search({
|
||||
index: AGENTS_INDEX,
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
rest_total_hits_as_int: true,
|
||||
filter_path: 'hits.total',
|
||||
ignore_unavailable: true,
|
||||
body,
|
||||
});
|
||||
|
||||
return (res.hits.total as number) || 0;
|
||||
return agentHit;
|
||||
}
|
||||
|
||||
export async function getAgentById(esClient: ElasticsearchClient, agentId: string) {
|
||||
const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`);
|
||||
async function _filterAgents(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
query: estypes.QueryDslQueryContainer,
|
||||
options: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortField?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
} = {}
|
||||
): Promise<{
|
||||
agents: Agent[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}> {
|
||||
const { page = 1, perPage = 20, sortField = 'enrolled_at', sortOrder = 'desc' } = options;
|
||||
const runtimeFields = await buildAgentStatusRuntimeField(soClient);
|
||||
|
||||
let res;
|
||||
try {
|
||||
const agentHit = await esClient.get<FleetServerAgent>({
|
||||
res = await esClient.search<FleetServerAgent, {}>({
|
||||
from: (page - 1) * perPage,
|
||||
size: perPage,
|
||||
track_total_hits: true,
|
||||
rest_total_hits_as_int: true,
|
||||
runtime_mappings: runtimeFields,
|
||||
fields: Object.keys(runtimeFields),
|
||||
sort: [{ [sortField]: { order: sortOrder } }],
|
||||
query: { bool: { filter: query } },
|
||||
index: AGENTS_INDEX,
|
||||
id: agentId,
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
|
||||
if (agentHit.found === false) {
|
||||
throw agentNotFoundError;
|
||||
}
|
||||
|
||||
return searchHitToAgent(agentHit);
|
||||
} catch (err) {
|
||||
if (isESClientError(err) && err.meta.statusCode === 404) {
|
||||
throw agentNotFoundError;
|
||||
}
|
||||
appContextService.getLogger().error(`Error querying agents: ${JSON.stringify(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAgentDocument(
|
||||
maybeDocument: any
|
||||
): maybeDocument is estypes.MgetResponseItem<FleetServerAgent> {
|
||||
return '_id' in maybeDocument && '_source' in maybeDocument;
|
||||
}
|
||||
const agents = res.hits.hits.map(searchHitToAgent);
|
||||
const total = res.hits.total as number;
|
||||
|
||||
export type ESAgentDocumentResult = estypes.MgetResponseItem<FleetServerAgent>;
|
||||
|
||||
export async function getAgentDocuments(
|
||||
esClient: ElasticsearchClient,
|
||||
agentIds: string[]
|
||||
): Promise<ESAgentDocumentResult[]> {
|
||||
const res = await esClient.mget<FleetServerAgent>({
|
||||
index: AGENTS_INDEX,
|
||||
body: { docs: agentIds.map((_id) => ({ _id })) },
|
||||
});
|
||||
|
||||
return res.docs || [];
|
||||
return {
|
||||
agents,
|
||||
total,
|
||||
page,
|
||||
perPage,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAgentsById(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentIds: string[]
|
||||
): Promise<Agent[]> {
|
||||
const allDocs = await getAgentDocuments(esClient, agentIds);
|
||||
const agents = allDocs.reduce<Agent[]>((results, doc) => {
|
||||
if (isAgentDocument(doc)) {
|
||||
results.push(searchHitToAgent(doc));
|
||||
}
|
||||
): Promise<Array<Agent | { id: string; notFound: true }>> {
|
||||
if (!agentIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results;
|
||||
}, []);
|
||||
const idsQuery = {
|
||||
terms: {
|
||||
_id: agentIds,
|
||||
},
|
||||
};
|
||||
const { agents } = await _filterAgents(esClient, soClient, idsQuery, {
|
||||
perPage: agentIds.length,
|
||||
});
|
||||
|
||||
return agents;
|
||||
// return agents in the same order as agentIds
|
||||
return agentIds.map(
|
||||
(agentId) => agents.find((agent) => agent.id === agentId) || { id: agentId, notFound: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAgentByAccessAPIKeyId(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
accessAPIKeyId: string
|
||||
): Promise<Agent> {
|
||||
const res = await esClient.search<FleetServerAgent>({
|
||||
index: AGENTS_INDEX,
|
||||
ignore_unavailable: true,
|
||||
q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`,
|
||||
});
|
||||
const query = {
|
||||
term: {
|
||||
access_api_key_id: accessAPIKeyId,
|
||||
},
|
||||
};
|
||||
const { agents } = await _filterAgents(esClient, soClient, query);
|
||||
|
||||
const searchHit = res.hits.hits[0];
|
||||
const agent = searchHit && searchHitToAgent(searchHit);
|
||||
const agent = agents.length ? agents[0] : null;
|
||||
|
||||
if (!searchHit || !agent) {
|
||||
if (!agent) {
|
||||
throw new AgentNotFoundError('Agent not found');
|
||||
}
|
||||
if (agent.access_api_key_id !== accessAPIKeyId) {
|
||||
|
@ -472,12 +512,31 @@ export async function deleteAgent(esClient: ElasticsearchClient, agentId: string
|
|||
}
|
||||
}
|
||||
|
||||
async function _getAgentDocById(esClient: ElasticsearchClient, agentId: string) {
|
||||
try {
|
||||
const res = await esClient.get<FleetServerAgent>({
|
||||
id: agentId,
|
||||
index: AGENTS_INDEX,
|
||||
});
|
||||
|
||||
if (!res._source) {
|
||||
throw new AgentNotFoundError(`Agent ${agentId} not found`);
|
||||
}
|
||||
return res._source;
|
||||
} catch (err) {
|
||||
if (isESClientError(err) && err.meta.statusCode === 404) {
|
||||
throw new AgentNotFoundError(`Agent ${agentId} not found`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAgentPolicyForAgent(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
agentId: string
|
||||
) {
|
||||
const agent = await getAgentById(esClient, agentId);
|
||||
const agent = await _getAgentDocById(esClient, agentId);
|
||||
if (!agent.policy_id) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -9,15 +9,21 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
|||
import type { SortResults } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { SearchHit } from '@kbn/es-types';
|
||||
|
||||
import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types';
|
||||
import { getAgentStatus } from '../../../common/services/agent_status';
|
||||
import { appContextService } from '..';
|
||||
|
||||
import type { Agent, AgentSOAttributes, AgentStatus, FleetServerAgent } from '../../types';
|
||||
|
||||
type FleetServerAgentESResponse =
|
||||
| estypes.GetGetResult<FleetServerAgent>
|
||||
| estypes.SearchResponse<FleetServerAgent>['hits']['hits'][0]
|
||||
| SearchHit<FleetServerAgent>;
|
||||
|
||||
export function searchHitToAgent(hit: FleetServerAgentESResponse & { sort?: SortResults }): Agent {
|
||||
export function searchHitToAgent(
|
||||
hit: FleetServerAgentESResponse & {
|
||||
sort?: SortResults;
|
||||
fields?: { status?: AgentStatus[] };
|
||||
}
|
||||
): Agent {
|
||||
// @ts-expect-error @elastic/elasticsearch MultiGetHit._source is optional
|
||||
const agent: Agent = {
|
||||
id: hit._id,
|
||||
|
@ -29,7 +35,16 @@ export function searchHitToAgent(hit: FleetServerAgentESResponse & { sort?: Sort
|
|||
sort: hit.sort,
|
||||
};
|
||||
|
||||
agent.status = getAgentStatus(agent);
|
||||
if (!hit.fields?.status?.length) {
|
||||
appContextService
|
||||
.getLogger()
|
||||
.error(
|
||||
'Agent status runtime field is missing, unable to get agent status for agent ' + agent.id
|
||||
);
|
||||
} else {
|
||||
agent.status = hit.fields.status[0];
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
|
|
|
@ -73,6 +73,13 @@ describe('reassignAgents (plural)', () => {
|
|||
agentInHostedDoc2,
|
||||
regularAgentPolicySO2,
|
||||
} = createClientMock();
|
||||
|
||||
esClient.search.mockResponse({
|
||||
hits: {
|
||||
hits: [agentInRegularDoc, agentInHostedDoc, agentInHostedDoc2],
|
||||
},
|
||||
} as any);
|
||||
|
||||
const idsToReassign = [agentInRegularDoc._id, agentInHostedDoc._id, agentInHostedDoc2._id];
|
||||
await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, regularAgentPolicySO2.id);
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server';
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
|
@ -15,7 +14,7 @@ import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from
|
|||
import { SO_SEARCH_LIMIT } from '../../constants';
|
||||
|
||||
import {
|
||||
getAgentDocuments,
|
||||
getAgentsById,
|
||||
getAgentPolicyForAgent,
|
||||
updateAgent,
|
||||
getAgentsByKuery,
|
||||
|
@ -23,7 +22,6 @@ import {
|
|||
} from './crud';
|
||||
import type { GetAgentsOptions } from '.';
|
||||
import { createAgentAction } from './actions';
|
||||
import { searchHitToAgent } from './helpers';
|
||||
|
||||
import { ReassignActionRunner, reassignBatch } from './reassign_action_runner';
|
||||
|
||||
|
@ -78,9 +76,6 @@ export async function reassignAgentIsAllowed(
|
|||
return true;
|
||||
}
|
||||
|
||||
function isMgetDoc(doc?: estypes.MgetResponseItem<unknown>): doc is estypes.GetGetResult {
|
||||
return Boolean(doc && 'found' in doc);
|
||||
}
|
||||
export async function reassignAgents(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
|
@ -105,19 +100,19 @@ export async function reassignAgents(
|
|||
if ('agents' in options) {
|
||||
givenAgents = options.agents;
|
||||
} else if ('agentIds' in options) {
|
||||
const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds);
|
||||
for (const agentResult of givenAgentsResults) {
|
||||
if (isMgetDoc(agentResult) && agentResult.found === false) {
|
||||
outgoingErrors[agentResult._id] = new AgentReassignmentError(
|
||||
`Cannot find agent ${agentResult._id}`
|
||||
const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds);
|
||||
for (const maybeAgent of maybeAgents) {
|
||||
if ('notFound' in maybeAgent) {
|
||||
outgoingErrors[maybeAgent.id] = new AgentReassignmentError(
|
||||
`Cannot find agent ${maybeAgent.id}`
|
||||
);
|
||||
} else {
|
||||
givenAgents.push(searchHitToAgent(agentResult));
|
||||
givenAgents.push(maybeAgent);
|
||||
}
|
||||
}
|
||||
} else if ('kuery' in options) {
|
||||
const batchSize = options.batchSize ?? SO_SEARCH_LIMIT;
|
||||
const res = await getAgentsByKuery(esClient, {
|
||||
const res = await getAgentsByKuery(esClient, soClient, {
|
||||
kuery: options.kuery,
|
||||
showInactive: options.showInactive ?? false,
|
||||
page: 1,
|
||||
|
|
|
@ -38,19 +38,19 @@ export async function bulkRequestDiagnostics(
|
|||
}
|
||||
): Promise<{ actionId: string }> {
|
||||
if ('agentIds' in options) {
|
||||
const givenAgents = await getAgents(esClient, options);
|
||||
const givenAgents = await getAgents(esClient, soClient, options);
|
||||
return await requestDiagnosticsBatch(esClient, givenAgents, {});
|
||||
}
|
||||
|
||||
const batchSize = options.batchSize ?? SO_SEARCH_LIMIT;
|
||||
const res = await getAgentsByKuery(esClient, {
|
||||
const res = await getAgentsByKuery(esClient, soClient, {
|
||||
kuery: options.kuery,
|
||||
showInactive: false,
|
||||
page: 1,
|
||||
perPage: batchSize,
|
||||
});
|
||||
if (res.total <= batchSize) {
|
||||
const givenAgents = await getAgents(esClient, options);
|
||||
const givenAgents = await getAgents(esClient, soClient, options);
|
||||
return await requestDiagnosticsBatch(esClient, givenAgents, {});
|
||||
} else {
|
||||
return await new RequestDiagnosticsActionRunner(
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* 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 { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { AGENT_POLLING_THRESHOLD_MS } from '../../../common/constants';
|
||||
|
||||
import { getAgentStatusById } from './status';
|
||||
|
||||
describe('Agent status service', () => {
|
||||
it('should return inactive when agent is not active', async () => {
|
||||
const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
mockElasticsearchClient.get.mockResponse(
|
||||
// @ts-expect-error not full interface
|
||||
{
|
||||
_id: 'id',
|
||||
_source: {
|
||||
active: false,
|
||||
local_metadata: {},
|
||||
user_provided_metadata: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
const status = await getAgentStatusById(mockElasticsearchClient, 'id');
|
||||
expect(status).toEqual('inactive');
|
||||
});
|
||||
|
||||
it('should return online when agent is active', async () => {
|
||||
const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
mockElasticsearchClient.get.mockResponse(
|
||||
// @ts-expect-error not full interface
|
||||
{
|
||||
_id: 'id',
|
||||
_source: {
|
||||
active: true,
|
||||
policy_revision_idx: 1,
|
||||
last_checkin: new Date().toISOString(),
|
||||
local_metadata: {},
|
||||
user_provided_metadata: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
const status = await getAgentStatusById(mockElasticsearchClient, 'id');
|
||||
expect(status).toEqual('online');
|
||||
});
|
||||
|
||||
it('should return enrolling when agent is active but never checkin', async () => {
|
||||
const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
mockElasticsearchClient.get.mockResponse(
|
||||
// @ts-expect-error not full interface
|
||||
{
|
||||
_id: 'id',
|
||||
_source: {
|
||||
active: true,
|
||||
local_metadata: {},
|
||||
user_provided_metadata: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
const status = await getAgentStatusById(mockElasticsearchClient, 'id');
|
||||
expect(status).toEqual('enrolling');
|
||||
});
|
||||
|
||||
it('should return unenrolling when agent is unenrolling', async () => {
|
||||
const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
mockElasticsearchClient.get.mockResponse(
|
||||
// @ts-expect-error not full interface
|
||||
{
|
||||
_id: 'id',
|
||||
_source: {
|
||||
active: true,
|
||||
last_checkin: new Date().toISOString(),
|
||||
unenrollment_started_at: new Date().toISOString(),
|
||||
local_metadata: {},
|
||||
user_provided_metadata: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
const status = await getAgentStatusById(mockElasticsearchClient, 'id');
|
||||
expect(status).toEqual('unenrolling');
|
||||
});
|
||||
|
||||
it('should return offline when agent has not checked in for 10 intervals', async () => {
|
||||
const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
mockElasticsearchClient.get.mockResponse(
|
||||
// @ts-expect-error not full interface
|
||||
{
|
||||
_id: 'id',
|
||||
_source: {
|
||||
active: true,
|
||||
last_checkin: new Date(Date.now() - 10 * AGENT_POLLING_THRESHOLD_MS - 1000).toISOString(),
|
||||
policy_revision_idx: 2,
|
||||
local_metadata: {},
|
||||
user_provided_metadata: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
const status = await getAgentStatusById(mockElasticsearchClient, 'id');
|
||||
expect(status).toEqual('offline');
|
||||
});
|
||||
});
|
|
@ -5,120 +5,142 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import pMap from 'p-map';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import { toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
|
||||
import { AGENTS_PREFIX } from '../../constants';
|
||||
import type {
|
||||
AggregationsTermsAggregateBase,
|
||||
AggregationsTermsBucketBase,
|
||||
QueryDslQueryContainer,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { AGENTS_INDEX } from '../../constants';
|
||||
import type { AgentStatus } from '../../types';
|
||||
import { AgentStatusKueryHelper } from '../../../common/services';
|
||||
import { FleetUnauthorizedError } from '../../errors';
|
||||
|
||||
import { appContextService } from '../app_context';
|
||||
|
||||
import {
|
||||
closePointInTime,
|
||||
getAgentById,
|
||||
getAgentsByKuery,
|
||||
openPointInTime,
|
||||
removeSOAttributes,
|
||||
} from './crud';
|
||||
import { getAgentById, removeSOAttributes } from './crud';
|
||||
import { buildAgentStatusRuntimeField } from './build_status_runtime_field';
|
||||
|
||||
interface AggregationsStatusTermsBucketKeys extends AggregationsTermsBucketBase {
|
||||
key: AgentStatus;
|
||||
}
|
||||
|
||||
const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*,synthetics-*-*';
|
||||
const MAX_AGENT_DATA_PREVIEW_SIZE = 20;
|
||||
export async function getAgentStatusById(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentId: string
|
||||
): Promise<AgentStatus> {
|
||||
return (await getAgentById(esClient, agentId)).status!;
|
||||
}
|
||||
|
||||
export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus;
|
||||
|
||||
function joinKuerys(...kuerys: Array<string | undefined>) {
|
||||
return kuerys
|
||||
.filter((kuery) => kuery !== undefined)
|
||||
.reduce((acc: KueryNode | undefined, kuery: string | undefined): KueryNode | undefined => {
|
||||
if (kuery === undefined) {
|
||||
return acc;
|
||||
}
|
||||
const normalizedKuery: KueryNode = fromKueryExpression(removeSOAttributes(kuery || ''));
|
||||
|
||||
if (!acc) {
|
||||
return normalizedKuery;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
function: 'and',
|
||||
arguments: [acc, normalizedKuery],
|
||||
};
|
||||
}, undefined as KueryNode | undefined);
|
||||
return (await getAgentById(esClient, soClient, agentId)).status!;
|
||||
}
|
||||
|
||||
export async function getAgentStatusForAgentPolicy(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentPolicyId?: string,
|
||||
filterKuery?: string
|
||||
) {
|
||||
let pitId: string | undefined;
|
||||
const logger = appContextService.getLogger();
|
||||
const runtimeFields = await buildAgentStatusRuntimeField(soClient);
|
||||
|
||||
const clauses: QueryDslQueryContainer[] = [];
|
||||
|
||||
if (filterKuery) {
|
||||
const kueryAsElasticsearchQuery = toElasticsearchQuery(
|
||||
fromKueryExpression(removeSOAttributes(filterKuery))
|
||||
);
|
||||
clauses.push(kueryAsElasticsearchQuery);
|
||||
}
|
||||
|
||||
if (agentPolicyId) {
|
||||
clauses.push({
|
||||
term: {
|
||||
policy_id: agentPolicyId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const query =
|
||||
clauses.length > 0
|
||||
? {
|
||||
bool: {
|
||||
must: clauses,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const statuses: Record<AgentStatus, number> = {
|
||||
online: 0,
|
||||
error: 0,
|
||||
inactive: 0,
|
||||
offline: 0,
|
||||
updating: 0,
|
||||
unenrolled: 0,
|
||||
degraded: 0,
|
||||
enrolling: 0,
|
||||
unenrolling: 0,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
pitId = await openPointInTime(esClient);
|
||||
response = await esClient.search<
|
||||
null,
|
||||
{ status: AggregationsTermsAggregateBase<AggregationsStatusTermsBucketKeys> }
|
||||
>({
|
||||
index: AGENTS_INDEX,
|
||||
size: 0,
|
||||
query,
|
||||
fields: Object.keys(runtimeFields),
|
||||
runtime_mappings: runtimeFields,
|
||||
aggregations: {
|
||||
status: {
|
||||
terms: {
|
||||
field: 'status',
|
||||
size: Object.keys(statuses).length,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
appContextService
|
||||
.getLogger()
|
||||
.debug('Index .fleet-agents does not exist yet, skipping point in time.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const [all, allActive, online, error, offline, updating] = await pMap(
|
||||
[
|
||||
undefined, // All agents, including inactive
|
||||
undefined, // All active agents
|
||||
AgentStatusKueryHelper.buildKueryForOnlineAgents(),
|
||||
AgentStatusKueryHelper.buildKueryForErrorAgents(),
|
||||
AgentStatusKueryHelper.buildKueryForOfflineAgents(),
|
||||
AgentStatusKueryHelper.buildKueryForUpdatingAgents(),
|
||||
],
|
||||
(kuery, index) =>
|
||||
getAgentsByKuery(esClient, {
|
||||
showInactive: index === 0,
|
||||
perPage: 0,
|
||||
page: 1,
|
||||
pitId,
|
||||
kuery: joinKuerys(
|
||||
...[
|
||||
kuery,
|
||||
filterKuery,
|
||||
agentPolicyId ? `${AGENTS_PREFIX}.policy_id:"${agentPolicyId}"` : undefined,
|
||||
]
|
||||
),
|
||||
}),
|
||||
{
|
||||
concurrency: 1,
|
||||
}
|
||||
);
|
||||
|
||||
if (pitId) {
|
||||
await closePointInTime(esClient, pitId);
|
||||
logger.error(`Error getting agent statuses: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const result = {
|
||||
total: allActive.total,
|
||||
inactive: all.total - allActive.total,
|
||||
online: online.total,
|
||||
error: error.total,
|
||||
offline: offline.total,
|
||||
updating: updating.total,
|
||||
other: all.total - online.total - error.total - offline.total,
|
||||
const buckets = (response?.aggregations?.status?.buckets ||
|
||||
[]) as AggregationsStatusTermsBucketKeys[];
|
||||
|
||||
buckets.forEach((bucket) => {
|
||||
if (statuses[bucket.key] !== undefined) {
|
||||
statuses[bucket.key] = bucket.doc_count;
|
||||
}
|
||||
});
|
||||
|
||||
const combinedStatuses = {
|
||||
online: statuses.online,
|
||||
error: statuses.error + statuses.degraded,
|
||||
inactive: statuses.inactive,
|
||||
offline: statuses.offline,
|
||||
updating: statuses.updating + statuses.enrolling + statuses.unenrolling,
|
||||
unenrolled: statuses.unenrolled,
|
||||
};
|
||||
|
||||
return {
|
||||
...combinedStatuses,
|
||||
/* @deprecated no agents will have other status */
|
||||
other: 0,
|
||||
/* @deprecated Agent events do not exists anymore */
|
||||
events: 0,
|
||||
total:
|
||||
Object.values(statuses).reduce((acc, val) => acc + val, 0) -
|
||||
combinedStatuses.unenrolled -
|
||||
combinedStatuses.inactive,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
export async function getIncomingDataByAgentsId(
|
||||
esClient: ElasticsearchClient,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import { AGENT_ACTIONS_INDEX } from '../../../common';
|
||||
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../common';
|
||||
|
||||
import { HostedAgentPolicyRestrictionRelatedError } from '../../errors';
|
||||
import { invalidateAPIKeys } from '../api_keys';
|
||||
|
@ -144,24 +144,33 @@ describe('unenrollAgents (plural)', () => {
|
|||
it('force unenroll updates in progress unenroll actions', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
esClient.search.mockReset();
|
||||
esClient.search.mockImplementation((request) =>
|
||||
Promise.resolve(
|
||||
request?.index === AGENT_ACTIONS_INDEX
|
||||
? ({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
esClient.search.mockImplementation(async (request) => {
|
||||
if (request?.index === AGENT_ACTIONS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action',
|
||||
},
|
||||
},
|
||||
} as any)
|
||||
: { hits: { hits: [] } }
|
||||
)
|
||||
);
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
if (request?.index === AGENT_ACTIONS_RESULTS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { hits: { hits: [agentInRegularDoc, agentInRegularDoc2] } };
|
||||
});
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, {
|
||||
|
@ -178,30 +187,34 @@ describe('unenrollAgents (plural)', () => {
|
|||
it('force unenroll should not update completed unenroll actions', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
esClient.search.mockReset();
|
||||
esClient.search.mockImplementation((request) =>
|
||||
Promise.resolve(
|
||||
request?.index === AGENT_ACTIONS_INDEX
|
||||
? ({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action1',
|
||||
},
|
||||
},
|
||||
],
|
||||
esClient.search.mockImplementation(async (request) => {
|
||||
if (request?.index === AGENT_ACTIONS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action1',
|
||||
},
|
||||
},
|
||||
} as any)
|
||||
: {
|
||||
hits: {
|
||||
hits: [
|
||||
{ _source: { action_id: 'other-action1', agent_id: 'agent-in-regular-policy' } },
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
if (request?.index === AGENT_ACTIONS_RESULTS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{ _source: { action_id: 'other-action1', agent_id: 'agent-in-regular-policy' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { hits: { hits: [agentInRegularDoc, agentInRegularDoc2] } };
|
||||
});
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, {
|
||||
|
|
|
@ -53,7 +53,7 @@ export async function unenrollAgent(
|
|||
await unenrollAgentIsAllowed(soClient, esClient, agentId);
|
||||
}
|
||||
if (options?.revoke) {
|
||||
return forceUnenrollAgent(esClient, agentId);
|
||||
return forceUnenrollAgent(esClient, soClient, agentId);
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
await createAgentAction(esClient, {
|
||||
|
@ -76,19 +76,19 @@ export async function unenrollAgents(
|
|||
}
|
||||
): Promise<{ actionId: string }> {
|
||||
if ('agentIds' in options) {
|
||||
const givenAgents = await getAgents(esClient, options);
|
||||
const givenAgents = await getAgents(esClient, soClient, options);
|
||||
return await unenrollBatch(soClient, esClient, givenAgents, options);
|
||||
}
|
||||
|
||||
const batchSize = options.batchSize ?? SO_SEARCH_LIMIT;
|
||||
const res = await getAgentsByKuery(esClient, {
|
||||
const res = await getAgentsByKuery(esClient, soClient, {
|
||||
kuery: options.kuery,
|
||||
showInactive: options.showInactive ?? false,
|
||||
page: 1,
|
||||
perPage: batchSize,
|
||||
});
|
||||
if (res.total <= batchSize) {
|
||||
const givenAgents = await getAgents(esClient, options);
|
||||
const givenAgents = await getAgents(esClient, soClient, options);
|
||||
return await unenrollBatch(soClient, esClient, givenAgents, options);
|
||||
} else {
|
||||
return await new UnenrollActionRunner(
|
||||
|
@ -106,11 +106,12 @@ export async function unenrollAgents(
|
|||
|
||||
export async function forceUnenrollAgent(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentIdOrAgent: string | Agent
|
||||
) {
|
||||
const agent =
|
||||
typeof agentIdOrAgent === 'string'
|
||||
? await getAgentById(esClient, agentIdOrAgent)
|
||||
? await getAgentById(esClient, soClient, agentIdOrAgent)
|
||||
: agentIdOrAgent;
|
||||
|
||||
await invalidateAPIKeysForAgents([agent]);
|
||||
|
|
|
@ -20,7 +20,7 @@ export async function unenrollForAgentPolicyId(
|
|||
let hasMore = true;
|
||||
let page = 1;
|
||||
while (hasMore) {
|
||||
const { agents } = await getAgentsByKuery(esClient, {
|
||||
const { agents } = await getAgentsByKuery(esClient, soClient, {
|
||||
kuery: `${AGENTS_PREFIX}.policy_id:"${policyId}"`,
|
||||
page: page++,
|
||||
perPage: 1000,
|
||||
|
|
|
@ -28,6 +28,7 @@ jest.mock('../app_context', () => {
|
|||
jest.mock('../agent_policy', () => {
|
||||
return {
|
||||
agentPolicyService: {
|
||||
getInactivityTimeouts: jest.fn().mockResolvedValue([]),
|
||||
getByIDs: jest.fn().mockResolvedValue([{ id: 'hosted-agent-policy', is_managed: true }]),
|
||||
list: jest.fn().mockResolvedValue({ items: [] }),
|
||||
},
|
||||
|
@ -49,16 +50,21 @@ describe('update_agent_tags', () => {
|
|||
beforeEach(() => {
|
||||
esClient = elasticsearchServiceMock.createInternalClient();
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient.mget.mockResolvedValue({
|
||||
docs: [
|
||||
{
|
||||
_id: 'agent1',
|
||||
_source: {
|
||||
tags: ['one', 'two', 'three'],
|
||||
esClient.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: 'agent1',
|
||||
_source: {
|
||||
tags: ['one', 'two', 'three'],
|
||||
},
|
||||
fields: {
|
||||
status: 'online',
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
esClient.bulk.mockReset();
|
||||
esClient.bulk.mockResolvedValue({
|
||||
items: [],
|
||||
|
@ -257,18 +263,65 @@ describe('update_agent_tags', () => {
|
|||
);
|
||||
|
||||
const updateByQuery = esClient.updateByQuery.mock.calls[0][0] as any;
|
||||
expect(updateByQuery.query).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { minimum_should_match: 1, should: [{ match: { active: true } }] } },
|
||||
{
|
||||
bool: {
|
||||
must_not: { bool: { minimum_should_match: 1, should: [{ match: { tags: 'new' } }] } },
|
||||
expect(updateByQuery.query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"status": "inactive",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"status": "unenrolled",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"tags": "new",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add tags filter if only one tag to remove', async () => {
|
||||
|
|
|
@ -5,21 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import type { Agent } from '../../types';
|
||||
import { AgentReassignmentError } from '../../errors';
|
||||
|
||||
import { getAgentDocuments } from './crud';
|
||||
import { getAgentsById } from './crud';
|
||||
import type { GetAgentsOptions } from '.';
|
||||
import { searchHitToAgent } from './helpers';
|
||||
import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner';
|
||||
|
||||
function isMgetDoc(doc?: estypes.MgetResponseItem<unknown>): doc is estypes.GetGetResult {
|
||||
return Boolean(doc && 'found' in doc);
|
||||
}
|
||||
|
||||
export async function updateAgentTags(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
|
@ -31,14 +25,14 @@ export async function updateAgentTags(
|
|||
const givenAgents: Agent[] = [];
|
||||
|
||||
if ('agentIds' in options) {
|
||||
const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds);
|
||||
for (const agentResult of givenAgentsResults) {
|
||||
if (isMgetDoc(agentResult) && agentResult.found === false) {
|
||||
outgoingErrors[agentResult._id] = new AgentReassignmentError(
|
||||
`Cannot find agent ${agentResult._id}`
|
||||
const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds);
|
||||
for (const maybeAgent of maybeAgents) {
|
||||
if ('notFound' in maybeAgent) {
|
||||
outgoingErrors[maybeAgent.id] = new AgentReassignmentError(
|
||||
`Cannot find agent ${maybeAgent.id}`
|
||||
);
|
||||
} else {
|
||||
givenAgents.push(searchHitToAgent(agentResult));
|
||||
givenAgents.push(maybeAgent);
|
||||
}
|
||||
}
|
||||
} else if ('kuery' in options) {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import type { Agent } from '../../types';
|
||||
|
@ -15,14 +14,9 @@ import { createAgentAction } from './actions';
|
|||
import type { GetAgentsOptions } from './crud';
|
||||
import { openPointInTime } from './crud';
|
||||
import { getAgentsByKuery } from './crud';
|
||||
import { getAgentDocuments, updateAgent, getAgentPolicyForAgent } from './crud';
|
||||
import { searchHitToAgent } from './helpers';
|
||||
import { getAgentsById, updateAgent, getAgentPolicyForAgent } from './crud';
|
||||
import { UpgradeActionRunner, upgradeBatch } from './upgrade_action_runner';
|
||||
|
||||
function isMgetDoc(doc?: estypes.MgetResponseItem<unknown>): doc is estypes.GetGetResult {
|
||||
return Boolean(doc && 'found' in doc);
|
||||
}
|
||||
|
||||
export async function sendUpgradeAgentAction({
|
||||
soClient,
|
||||
esClient,
|
||||
|
@ -80,19 +74,19 @@ export async function sendUpgradeAgentsActions(
|
|||
if ('agents' in options) {
|
||||
givenAgents = options.agents;
|
||||
} else if ('agentIds' in options) {
|
||||
const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds);
|
||||
for (const agentResult of givenAgentsResults) {
|
||||
if (!isMgetDoc(agentResult) || agentResult.found === false) {
|
||||
outgoingErrors[agentResult._id] = new AgentReassignmentError(
|
||||
`Cannot find agent ${agentResult._id}`
|
||||
const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds);
|
||||
for (const maybeAgent of maybeAgents) {
|
||||
if ('notFound' in maybeAgent) {
|
||||
outgoingErrors[maybeAgent.id] = new AgentReassignmentError(
|
||||
`Cannot find agent ${maybeAgent.id}`
|
||||
);
|
||||
} else {
|
||||
givenAgents.push(searchHitToAgent(agentResult));
|
||||
givenAgents.push(maybeAgent);
|
||||
}
|
||||
}
|
||||
} else if ('kuery' in options) {
|
||||
const batchSize = options.batchSize ?? SO_SEARCH_LIMIT;
|
||||
const res = await getAgentsByKuery(esClient, {
|
||||
const res = await getAgentsByKuery(esClient, soClient, {
|
||||
kuery: options.kuery,
|
||||
showInactive: options.showInactive ?? false,
|
||||
page: 1,
|
||||
|
|
|
@ -154,7 +154,7 @@ async function _deleteExistingData(
|
|||
}
|
||||
|
||||
// unenroll all the agents enroled in this policies
|
||||
const { agents } = await getAgentsByKuery(esClient, {
|
||||
const { agents } = await getAgentsByKuery(esClient, soClient, {
|
||||
showInactive: false,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
kuery: existingPolicies.map((policy) => `policy_id:"${policy.id}"`).join(' or '),
|
||||
|
@ -163,7 +163,7 @@ async function _deleteExistingData(
|
|||
// Delete
|
||||
if (agents.length > 0) {
|
||||
logger.info(`Force unenrolling ${agents.length} agents`);
|
||||
await pMap(agents, (agent) => forceUnenrollAgent(esClient, agent.id), {
|
||||
await pMap(agents, (agent) => forceUnenrollAgent(esClient, soClient, agent.id), {
|
||||
concurrency: 20,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -196,8 +196,8 @@ export class FleetAgentGenerator extends BaseDataGenerator<Agent> {
|
|||
) {
|
||||
const esHit = this.generateEsHit(overrides);
|
||||
|
||||
// Basically: reverse engineer the Fleet `getAgentStatus()` utility:
|
||||
// https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/common/services/agent_status.ts#L13-L44
|
||||
// Basically: reverse engineer the Fleet agent status runtime field:
|
||||
// https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/server/services/agents/build_status_runtime_field.ts
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const fleetServerAgent = esHit._source!;
|
||||
|
@ -218,10 +218,9 @@ export class FleetAgentGenerator extends BaseDataGenerator<Agent> {
|
|||
fleetServerAgent.last_checkin_status = 'error';
|
||||
break;
|
||||
|
||||
// not able to generate agents with inactive status without a valid agent policy
|
||||
// with inactivity_timeout set
|
||||
case 'inactive':
|
||||
fleetServerAgent.active = false;
|
||||
break;
|
||||
|
||||
case 'offline':
|
||||
// current fleet timeout interface for offline is 5 minutes
|
||||
// https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/common/services/agent_status.ts#L11
|
||||
|
|
|
@ -58,7 +58,10 @@ export interface PolicyDetailsState {
|
|||
/** artifacts namespace inside policy details page */
|
||||
artifacts: PolicyArtifactsState;
|
||||
/** A summary of stats for the agents associated with a given Fleet Agent Policy */
|
||||
agentStatusSummary?: Omit<GetAgentStatusResponse['results'], 'updating'>;
|
||||
agentStatusSummary?: Omit<
|
||||
GetAgentStatusResponse['results'],
|
||||
'updating' | 'inactive' | 'unenrolled'
|
||||
>;
|
||||
/** Status of an update to the policy */
|
||||
updateStatus?: {
|
||||
success: boolean;
|
||||
|
|
|
@ -65,6 +65,7 @@ export function getMetadataListRequestHandler(
|
|||
const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService();
|
||||
const fleetServices = endpointAppContext.service.getInternalFleetServices();
|
||||
const esClient = (await context.core).elasticsearch.client.asInternalUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
||||
let doesUnitedIndexExist = false;
|
||||
let didUnitedIndexError = false;
|
||||
|
@ -107,6 +108,7 @@ export function getMetadataListRequestHandler(
|
|||
try {
|
||||
const { data, total } = await endpointMetadataService.getHostMetadataList(
|
||||
esClient,
|
||||
soClient,
|
||||
fleetServices,
|
||||
request.query
|
||||
);
|
||||
|
|
|
@ -219,7 +219,12 @@ describe('test endpoint routes', () => {
|
|||
kuery: 'not host.ip:10.140.73.246',
|
||||
},
|
||||
});
|
||||
|
||||
mockSavedObjectClient.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
});
|
||||
mockAgentClient.getAgentStatusById.mockResolvedValue('error');
|
||||
mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent);
|
||||
mockAgentPolicyService.getByIds = jest.fn().mockResolvedValueOnce([]);
|
||||
|
@ -267,187 +272,27 @@ describe('test endpoint routes', () => {
|
|||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ exists: { field: 'united.agent.upgrade_started_at' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [{ exists: { field: 'united.agent.upgraded_at' } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [{ exists: { field: 'united.agent.last_checkin' } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ exists: { field: 'united.agent.unenrollment_started_at' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{ exists: { field: 'united.agent.policy_revision_idx' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [{ match: { status: 'updating' } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ range: { 'united.agent.last_checkin': { lt: 'now-300s' } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status': 'degraded',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status': 'DEGRADED',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status': 'ERROR',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'united.agent.last_checkin': {
|
||||
lt: 'now-300s',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field:
|
||||
'united.agent.unenrollment_started_at',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
should: [{ match: { status: 'unenrolling' } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [{ match: { status: 'enrolling' } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -17,326 +17,26 @@ export const expectedCompleteUnitedIndexQuery = {
|
|||
],
|
||||
},
|
||||
},
|
||||
filter: [
|
||||
{ terms: { 'united.agent.policy_id': ['test-endpoint-policy-id'] } },
|
||||
{ exists: { field: 'united.endpoint.agent.id' } },
|
||||
{ exists: { field: 'united.agent.agent.id' } },
|
||||
{ term: { 'united.agent.active': { value: true } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [{ exists: { field: 'united.agent.last_checkin' } }],
|
||||
minimum_should_match: 1,
|
||||
terms: {
|
||||
'united.agent.policy_id': ['test-endpoint-policy-id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [{ range: { 'united.agent.last_checkin': { lt: 'now-300s' } } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: 'united.agent.upgrade_started_at',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{ exists: { field: 'united.agent.upgraded_at' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{ exists: { field: 'united.agent.last_checkin' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: { field: 'united.agent.unenrollment_started_at' },
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: { field: 'united.agent.policy_revision_idx' },
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'united.agent.last_checkin': { lt: 'now-300s' },
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status':
|
||||
'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status':
|
||||
'degraded',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status':
|
||||
'DEGRADED',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'united.agent.last_checkin_status':
|
||||
'ERROR',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'united.agent.last_checkin': {
|
||||
lt: 'now-300s',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field:
|
||||
'united.agent.unenrollment_started_at',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ match: { 'united.agent.last_checkin_status': 'error' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: { 'united.agent.last_checkin_status': 'degraded' },
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: { 'united.agent.last_checkin_status': 'DEGRADED' },
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ match: { 'united.agent.last_checkin_status': 'ERROR' } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'united.agent.last_checkin': { lt: 'now-300s' },
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: 'united.agent.unenrollment_started_at',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
exists: {
|
||||
field: 'united.endpoint.agent.id',
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: 'united.agent.agent.id',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'united.agent.active': {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -345,7 +45,25 @@ export const expectedCompleteUnitedIndexQuery = {
|
|||
},
|
||||
{
|
||||
bool: {
|
||||
should: [{ exists: { field: 'united.endpoint.host.os.name' } }],
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
status: 'online',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: 'united.endpoint.host.os.name',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants';
|
||||
import { get } from 'lodash';
|
||||
import { expectedCompleteUnitedIndexQuery } from './query_builders.fixtures';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
describe('query builder', () => {
|
||||
describe('MetadataListESQuery', () => {
|
||||
|
@ -194,7 +195,16 @@ describe('query builder', () => {
|
|||
|
||||
describe('buildUnitedIndexQuery', () => {
|
||||
it('correctly builds empty query', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
soClient.find.mockResolvedValue({
|
||||
saved_objects: [],
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
});
|
||||
|
||||
const query = await buildUnitedIndexQuery(
|
||||
soClient,
|
||||
{ page: 1, pageSize: 10, hostStatuses: [], kuery: '' },
|
||||
[]
|
||||
);
|
||||
|
@ -238,7 +248,16 @@ describe('query builder', () => {
|
|||
});
|
||||
|
||||
it('correctly builds query', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
soClient.find.mockResolvedValue({
|
||||
saved_objects: [],
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
});
|
||||
|
||||
const query = await buildUnitedIndexQuery(
|
||||
soClient,
|
||||
{
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { buildAgentStatusRuntimeField } from '@kbn/fleet-plugin/server';
|
||||
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import {
|
||||
ENDPOINT_DEFAULT_PAGE,
|
||||
ENDPOINT_DEFAULT_PAGE_SIZE,
|
||||
|
@ -205,6 +207,8 @@ interface BuildUnitedIndexQueryResponse {
|
|||
query: Record<string, unknown>;
|
||||
track_total_hits: boolean;
|
||||
sort: estypes.SortCombinations[];
|
||||
runtime_mappings: Record<string, unknown>;
|
||||
fields?: string[];
|
||||
};
|
||||
from: number;
|
||||
size: number;
|
||||
|
@ -212,6 +216,7 @@ interface BuildUnitedIndexQueryResponse {
|
|||
}
|
||||
|
||||
export async function buildUnitedIndexQuery(
|
||||
soClient: SavedObjectsClientContract,
|
||||
queryOptions: GetMetadataListRequestQuery,
|
||||
endpointPolicyIds: string[] = []
|
||||
): Promise<BuildUnitedIndexQueryResponse> {
|
||||
|
@ -273,11 +278,15 @@ export async function buildUnitedIndexQuery(
|
|||
};
|
||||
}
|
||||
|
||||
const runtimeMappings = await buildAgentStatusRuntimeField(soClient, 'united.agent.');
|
||||
const fields = Object.keys(runtimeMappings);
|
||||
return {
|
||||
body: {
|
||||
query,
|
||||
track_total_hits: true,
|
||||
sort: MetadataSortMethod,
|
||||
fields,
|
||||
runtime_mappings: runtimeMappings,
|
||||
},
|
||||
from: page * pageSize,
|
||||
size: pageSize,
|
||||
|
|
|
@ -92,45 +92,46 @@ describe('test filtering endpoint hosts by agent status', () => {
|
|||
it('correctly builds kuery for healthy status', () => {
|
||||
const status = ['healthy'];
|
||||
const kuery = buildStatusesKuery(status);
|
||||
expect(kuery).toMatchInlineSnapshot(
|
||||
`"(united.agent.last_checkin:* AND not ((united.agent.last_checkin < now-300s) or ((((united.agent.upgrade_started_at:*) and not (united.agent.upgraded_at:*)) or (not (united.agent.last_checkin:*)) or (united.agent.unenrollment_started_at:*) or (not united.agent.policy_revision_idx:*)) AND not ((united.agent.last_checkin < now-300s) or ((united.agent.last_checkin_status:error or united.agent.last_checkin_status:degraded or united.agent.last_checkin_status:DEGRADED or united.agent.last_checkin_status:ERROR) AND not ((united.agent.last_checkin < now-300s) or (united.agent.unenrollment_started_at:*))))) or ((united.agent.last_checkin_status:error or united.agent.last_checkin_status:degraded or united.agent.last_checkin_status:DEGRADED or united.agent.last_checkin_status:ERROR) AND not ((united.agent.last_checkin < now-300s) or (united.agent.unenrollment_started_at:*)))))"`
|
||||
);
|
||||
expect(kuery).toMatchInlineSnapshot(`"(status:online)"`);
|
||||
});
|
||||
|
||||
it('correctly builds kuery for offline status', () => {
|
||||
const status = ['offline'];
|
||||
const kuery = buildStatusesKuery(status);
|
||||
expect(kuery).toMatchInlineSnapshot(`"(united.agent.last_checkin < now-300s)"`);
|
||||
expect(kuery).toMatchInlineSnapshot(`"(status:offline)"`);
|
||||
});
|
||||
|
||||
it('correctly builds kuery for unhealthy status', () => {
|
||||
const status = ['unhealthy'];
|
||||
const kuery = buildStatusesKuery(status);
|
||||
expect(kuery).toMatchInlineSnapshot(
|
||||
`"((united.agent.last_checkin_status:error or united.agent.last_checkin_status:degraded or united.agent.last_checkin_status:DEGRADED or united.agent.last_checkin_status:ERROR) AND not ((united.agent.last_checkin < now-300s) or (united.agent.unenrollment_started_at:*)))"`
|
||||
);
|
||||
expect(kuery).toMatchInlineSnapshot(`"((status:error or status:degraded))"`);
|
||||
});
|
||||
|
||||
it('correctly builds kuery for updating status', () => {
|
||||
const status = ['updating'];
|
||||
const kuery = buildStatusesKuery(status);
|
||||
expect(kuery).toMatchInlineSnapshot(
|
||||
`"((((united.agent.upgrade_started_at:*) and not (united.agent.upgraded_at:*)) or (not (united.agent.last_checkin:*)) or (united.agent.unenrollment_started_at:*) or (not united.agent.policy_revision_idx:*)) AND not ((united.agent.last_checkin < now-300s) or ((united.agent.last_checkin_status:error or united.agent.last_checkin_status:degraded or united.agent.last_checkin_status:DEGRADED or united.agent.last_checkin_status:ERROR) AND not ((united.agent.last_checkin < now-300s) or (united.agent.unenrollment_started_at:*)))))"`
|
||||
`"((status:updating or status:unenrolling or status:enrolling))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly builds kuery for inactive status', () => {
|
||||
const status = ['inactive'];
|
||||
const kuery = buildStatusesKuery(status);
|
||||
const expected = '(united.agent.active:false)';
|
||||
expect(kuery).toEqual(expected);
|
||||
expect(kuery).toMatchInlineSnapshot(`"(status:inactive)"`);
|
||||
});
|
||||
|
||||
it('correctly builds kuery for unenrolled status', () => {
|
||||
const status = ['unenrolled'];
|
||||
const kuery = buildStatusesKuery(status);
|
||||
expect(kuery).toMatchInlineSnapshot(`"(status:unenrolled)"`);
|
||||
});
|
||||
|
||||
it('correctly builds kuery for multiple statuses', () => {
|
||||
const statuses = ['offline', 'unhealthy'];
|
||||
const kuery = buildStatusesKuery(statuses);
|
||||
expect(kuery).toMatchInlineSnapshot(
|
||||
`"(united.agent.last_checkin < now-300s OR (united.agent.last_checkin_status:error or united.agent.last_checkin_status:degraded or united.agent.last_checkin_status:DEGRADED or united.agent.last_checkin_status:ERROR) AND not ((united.agent.last_checkin < now-300s) or (united.agent.unenrollment_started_at:*)))"`
|
||||
`"(status:offline OR (status:error or status:degraded))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,20 +10,19 @@ import { AgentStatusKueryHelper } from '@kbn/fleet-plugin/common/services';
|
|||
import type { Agent } from '@kbn/fleet-plugin/common/types/models';
|
||||
import { HostStatus } from '../../../../../common/endpoint/types';
|
||||
|
||||
const getStatusQueryMap = (path: string = '') =>
|
||||
new Map([
|
||||
[HostStatus.HEALTHY.toString(), AgentStatusKueryHelper.buildKueryForOnlineAgents(path)],
|
||||
[HostStatus.OFFLINE.toString(), AgentStatusKueryHelper.buildKueryForOfflineAgents(path)],
|
||||
[HostStatus.UNHEALTHY.toString(), AgentStatusKueryHelper.buildKueryForErrorAgents(path)],
|
||||
[HostStatus.UPDATING.toString(), AgentStatusKueryHelper.buildKueryForUpdatingAgents(path)],
|
||||
[HostStatus.INACTIVE.toString(), AgentStatusKueryHelper.buildKueryForInactiveAgents(path)],
|
||||
]);
|
||||
const STATUS_QUERY_MAP = new Map([
|
||||
[HostStatus.HEALTHY.toString(), AgentStatusKueryHelper.buildKueryForOnlineAgents()],
|
||||
[HostStatus.OFFLINE.toString(), AgentStatusKueryHelper.buildKueryForOfflineAgents()],
|
||||
[HostStatus.UNHEALTHY.toString(), AgentStatusKueryHelper.buildKueryForErrorAgents()],
|
||||
[HostStatus.UPDATING.toString(), AgentStatusKueryHelper.buildKueryForUpdatingAgents()],
|
||||
[HostStatus.INACTIVE.toString(), AgentStatusKueryHelper.buildKueryForInactiveAgents()],
|
||||
[HostStatus.UNENROLLED.toString(), AgentStatusKueryHelper.buildKueryForUnenrolledAgents()],
|
||||
]);
|
||||
|
||||
export function buildStatusesKuery(statusesToFilter: string[]): string | undefined {
|
||||
if (!statusesToFilter.length) {
|
||||
return;
|
||||
}
|
||||
const STATUS_QUERY_MAP = getStatusQueryMap('united.agent.');
|
||||
const statusQueries = statusesToFilter.map((status) => STATUS_QUERY_MAP.get(status));
|
||||
if (!statusQueries.length) {
|
||||
return;
|
||||
|
@ -40,7 +39,6 @@ export async function findAgentIdsByStatus(
|
|||
if (!statuses.length) {
|
||||
return [];
|
||||
}
|
||||
const STATUS_QUERY_MAP = getStatusQueryMap();
|
||||
const helpers = statuses.map((s) => STATUS_QUERY_MAP.get(s));
|
||||
const searchOptions = (pageNum: number) => {
|
||||
return {
|
||||
|
|
|
@ -47,7 +47,8 @@ export function legacyMetadataSearchResponseMock(
|
|||
|
||||
export function unitedMetadataSearchResponseMock(
|
||||
hostMetadata: HostMetadata = {} as HostMetadata,
|
||||
agent: Agent = {} as Agent
|
||||
agent: Agent = {} as Agent,
|
||||
agentStatus: Agent['status'] = 'online'
|
||||
): estypes.SearchResponse<UnitedAgentMetadata> {
|
||||
return {
|
||||
took: 15,
|
||||
|
@ -83,6 +84,9 @@ export function unitedMetadataSearchResponseMock(
|
|||
},
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
status: [agentStatus],
|
||||
},
|
||||
sort: [1588337587997],
|
||||
},
|
||||
]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { EndpointMetadataServiceTestContextMock } from './mocks';
|
||||
import { createEndpointMetadataServiceTestContextMock } from './mocks';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import {
|
||||
legacyMetadataSearchResponseMock,
|
||||
|
@ -22,11 +22,13 @@ import type { HostMetadata } from '../../../../common/endpoint/types';
|
|||
import type { Agent, PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import type { AgentPolicyServiceInterface } from '@kbn/fleet-plugin/server/services';
|
||||
import { EndpointError } from '../../../../common/endpoint/errors';
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
describe('EndpointMetadataService', () => {
|
||||
let testMockedContext: EndpointMetadataServiceTestContextMock;
|
||||
let metadataService: EndpointMetadataServiceTestContextMock['endpointMetadataService'];
|
||||
let esClient: ElasticsearchClientMock;
|
||||
let soClient: SavedObjectsClientContract;
|
||||
let endpointDocGenerator: EndpointDocGenerator;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -34,6 +36,8 @@ describe('EndpointMetadataService', () => {
|
|||
testMockedContext = createEndpointMetadataServiceTestContextMock();
|
||||
metadataService = testMockedContext.endpointMetadataService;
|
||||
esClient = elasticsearchServiceMock.createScopedClusterClient().asInternalUser;
|
||||
soClient = savedObjectsClientMock.create();
|
||||
soClient.find = jest.fn().mockResolvedValue({ saved_objects: [] });
|
||||
});
|
||||
|
||||
describe('#findHostMetadataForFleetAgents()', () => {
|
||||
|
@ -106,6 +110,7 @@ describe('EndpointMetadataService', () => {
|
|||
esClient.search.mockRejectedValue({});
|
||||
const metadataListResponse = metadataService.getHostMetadataList(
|
||||
esClient,
|
||||
soClient,
|
||||
testMockedContext.fleetServices,
|
||||
{
|
||||
page: 0,
|
||||
|
@ -163,10 +168,19 @@ describe('EndpointMetadataService', () => {
|
|||
const queryOptions = { page: 1, pageSize: 10, kuery: '', hostStatuses: [] };
|
||||
const metadataListResponse = await metadataService.getHostMetadataList(
|
||||
esClient,
|
||||
soClient,
|
||||
testMockedContext.fleetServices,
|
||||
queryOptions
|
||||
);
|
||||
const unitedIndexQuery = await buildUnitedIndexQuery(queryOptions, packagePolicyIds);
|
||||
const unitedIndexQuery = await buildUnitedIndexQuery(
|
||||
soClient,
|
||||
queryOptions,
|
||||
packagePolicyIds
|
||||
);
|
||||
|
||||
expect(unitedIndexQuery.body.runtime_mappings.status).toBeDefined();
|
||||
// @ts-expect-error runtime_mappings is not typed
|
||||
unitedIndexQuery.body.runtime_mappings.status.script.source = expect.any(String);
|
||||
|
||||
expect(esClient.search).toBeCalledWith(unitedIndexQuery);
|
||||
expect(agentPolicyServiceMock.getByIds).toBeCalledWith(expect.anything(), agentPolicyIds);
|
||||
|
@ -174,7 +188,7 @@ describe('EndpointMetadataService', () => {
|
|||
data: [
|
||||
{
|
||||
metadata: endpointMetadataDoc,
|
||||
host_status: 'inactive',
|
||||
host_status: 'healthy',
|
||||
policy_info: {
|
||||
agent: {
|
||||
applied: {
|
||||
|
|
|
@ -13,10 +13,9 @@ import type {
|
|||
} from '@kbn/core/server';
|
||||
|
||||
import type { SearchTotalHits, SearchResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Agent, AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import type { Agent, AgentPolicy, AgentStatus, PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import type { AgentPolicyServiceInterface, PackagePolicyClient } from '@kbn/fleet-plugin/server';
|
||||
import { AgentNotFoundError } from '@kbn/fleet-plugin/server';
|
||||
import { getAgentStatus } from '@kbn/fleet-plugin/common/services/agent_status';
|
||||
import type {
|
||||
HostInfo,
|
||||
HostMetadata,
|
||||
|
@ -203,7 +202,7 @@ export class EndpointMetadataService {
|
|||
* If undefined, it will be retrieved from Fleet using the ID in the endpointMetadata.
|
||||
* If passing in an `Agent` record that was retrieved from the Endpoint Unified transform index,
|
||||
* ensure that its `.status` property is properly set to the calculated value done by
|
||||
* fleet `getAgentStatus()` method.
|
||||
* fleet.
|
||||
*/
|
||||
_fleetAgent?: MaybeImmutable<Agent>,
|
||||
/** If undefined, it will be retrieved from Fleet using data from the endpointMetadata */
|
||||
|
@ -272,7 +271,6 @@ export class EndpointMetadataService {
|
|||
this.logger?.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: endpointMetadata,
|
||||
host_status: fleetAgent
|
||||
|
@ -396,12 +394,13 @@ export class EndpointMetadataService {
|
|||
*/
|
||||
async getHostMetadataList(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
fleetServices: EndpointFleetServicesInterface,
|
||||
queryOptions: GetMetadataListRequestQuery
|
||||
): Promise<Pick<MetadataListResponse, 'data' | 'total'>> {
|
||||
const endpointPolicies = await this.getAllEndpointPackagePolicies();
|
||||
const endpointPolicyIds = endpointPolicies.map((policy) => policy.policy_id);
|
||||
const unitedIndexQuery = await buildUnitedIndexQuery(queryOptions, endpointPolicyIds);
|
||||
const unitedIndexQuery = await buildUnitedIndexQuery(soClient, queryOptions, endpointPolicyIds);
|
||||
|
||||
let unitedMetadataQueryResponse: SearchResponse<UnitedAgentMetadata>;
|
||||
|
||||
|
@ -443,22 +442,17 @@ export class EndpointMetadataService {
|
|||
|
||||
for (const doc of docs) {
|
||||
const { endpoint: metadata, agent: _agent } = doc?._source?.united ?? {};
|
||||
|
||||
if (metadata && _agent) {
|
||||
// `_agent: Agent` here is the record stored in the unified index, whose `status` **IS NOT** the
|
||||
// calculated status returned by the normal fleet API/Service. So lets calculated it before
|
||||
// passing this on to other methods that expect an `Agent` type
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const agentPolicy = agentPoliciesMap[_agent.policy_id!];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const endpointPolicy = endpointPoliciesMap[_agent.policy_id!];
|
||||
// add the agent status from the fleet runtime field to
|
||||
// the agent object
|
||||
const agent: typeof _agent = {
|
||||
..._agent,
|
||||
// Casting below necessary to remove `Immutable<>` from the type
|
||||
status: getAgentStatus(_agent as Agent),
|
||||
status: doc?.fields?.status?.[0] as AgentStatus,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const agentPolicy = agentPoliciesMap[agent.policy_id!];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const endpointPolicy = endpointPoliciesMap[agent.policy_id!];
|
||||
|
||||
hosts.push(
|
||||
await this.enrichHostMetadata(fleetServices, metadata, agent, agentPolicy, endpointPolicy)
|
||||
);
|
||||
|
|
|
@ -5204,6 +5204,18 @@
|
|||
"description": "The total number of enrolled agents currently offline"
|
||||
}
|
||||
},
|
||||
"inactive": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The total number of of enrolled agents currently inactive"
|
||||
}
|
||||
},
|
||||
"unenrolled": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The total number of agents currently unenrolled"
|
||||
}
|
||||
},
|
||||
"total_all_statuses": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
|
|
|
@ -18,6 +18,29 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
describe('fleet_agents_status', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents');
|
||||
await es.create({
|
||||
id: 'ingest-agent-policies:policy-inactivity-timeout',
|
||||
index: '.kibana',
|
||||
refresh: 'wait_for',
|
||||
document: {
|
||||
type: 'ingest-agent-policies',
|
||||
'ingest-agent-policies': {
|
||||
name: 'Test policy',
|
||||
namespace: 'default',
|
||||
description: 'Policy with inactivity timeout',
|
||||
status: 'active',
|
||||
is_default: true,
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
revision: 2,
|
||||
updated_at: '2020-05-07T19:34:42.533Z',
|
||||
updated_by: 'system',
|
||||
inactivity_timeout: 60,
|
||||
},
|
||||
migrationVersion: {
|
||||
'ingest-agent-policies': '7.10.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
// 2 agents online
|
||||
await es.update({
|
||||
id: 'agent1',
|
||||
|
@ -53,22 +76,33 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
});
|
||||
// 1 agent upgrading
|
||||
// 1 agents inactive
|
||||
await es.update({
|
||||
id: 'agent4',
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
body: {
|
||||
doc: {
|
||||
policy_id: 'policy-inactivity-timeout',
|
||||
policy_revision_idx: 1,
|
||||
last_checkin: new Date().toISOString(),
|
||||
upgrade_started_at: new Date().toISOString(),
|
||||
last_checkin: new Date(Date.now() - 1000 * 60).toISOString(), // policy timeout 1 min
|
||||
},
|
||||
},
|
||||
});
|
||||
// 1 agent upgrading
|
||||
await es.create({
|
||||
id: 'agent5',
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
document: {
|
||||
policy_revision_idx: 1,
|
||||
last_checkin: new Date().toISOString(),
|
||||
upgrade_started_at: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
// 1 agent reassigned to a new policy
|
||||
await es.create({
|
||||
id: 'agent5',
|
||||
id: 'agent6',
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
document: {
|
||||
|
@ -84,9 +118,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
});
|
||||
|
||||
// 1 agent inactive
|
||||
// 1 agent unenrolled
|
||||
await es.create({
|
||||
id: 'agent6',
|
||||
id: 'agent7',
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
document: {
|
||||
|
@ -98,9 +132,46 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
local_metadata: { host: { hostname: 'host6' } },
|
||||
user_provided_metadata: {},
|
||||
enrolled_at: '2022-06-21T12:17:25Z',
|
||||
unenrolled_at: '2022-06-21T12:29:29Z',
|
||||
last_checkin: '2022-06-27T12:29:29Z',
|
||||
},
|
||||
});
|
||||
// 1 agent error
|
||||
await es.create({
|
||||
id: 'agent8',
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
document: {
|
||||
active: true,
|
||||
access_api_key_id: 'api-key-4',
|
||||
policy_id: 'policy1',
|
||||
type: 'PERMANENT',
|
||||
policy_revision_idx: 1,
|
||||
local_metadata: { host: { hostname: 'host6' } },
|
||||
user_provided_metadata: {},
|
||||
enrolled_at: '2022-06-21T12:17:25Z',
|
||||
last_checkin: new Date().toISOString(),
|
||||
last_checkin_status: 'ERROR',
|
||||
},
|
||||
});
|
||||
// 1 agent degraded (error category)
|
||||
await es.create({
|
||||
id: 'agent9',
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
document: {
|
||||
active: true,
|
||||
access_api_key_id: 'api-key-4',
|
||||
policy_id: 'policy1',
|
||||
type: 'PERMANENT',
|
||||
policy_revision_idx: 1,
|
||||
local_metadata: { host: { hostname: 'host6' } },
|
||||
user_provided_metadata: {},
|
||||
enrolled_at: '2022-06-21T12:17:25Z',
|
||||
last_checkin: new Date().toISOString(),
|
||||
last_checkin_status: 'DEGRADED',
|
||||
},
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents');
|
||||
|
@ -111,13 +182,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(apiResponse).to.eql({
|
||||
results: {
|
||||
events: 0,
|
||||
total: 5,
|
||||
other: 0,
|
||||
total: 7,
|
||||
online: 2,
|
||||
error: 0,
|
||||
error: 2,
|
||||
offline: 1,
|
||||
updating: 2,
|
||||
other: 3,
|
||||
inactive: 1,
|
||||
unenrolled: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -136,6 +136,8 @@ export default function (providerContext: FtrProviderContext) {
|
|||
healthy: 3,
|
||||
unhealthy: 3,
|
||||
offline: 1,
|
||||
unenrolled: 0,
|
||||
inactive: 0,
|
||||
updating: 1,
|
||||
total_all_statuses: 8,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue