[UII] Hide agentless agents and policies (#219175)

This commit is contained in:
Jen Huang 2025-06-03 06:35:38 -07:00 committed by GitHub
parent 168eef7949
commit 6c2cbf10a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 268 additions and 32 deletions

View file

@ -20092,6 +20092,15 @@
"type": "string"
}
},
{
"in": "query",
"name": "showAgentless",
"required": false,
"schema": {
"default": true,
"type": "boolean"
}
},
{
"in": "query",
"name": "showInactive",

View file

@ -20092,6 +20092,15 @@
"type": "string"
}
},
{
"in": "query",
"name": "showAgentless",
"required": false,
"schema": {
"default": true,
"type": "boolean"
}
},
{
"in": "query",
"name": "showInactive",

View file

@ -23025,6 +23025,12 @@ paths:
required: false
schema:
type: string
- in: query
name: showAgentless
required: false
schema:
default: true
type: boolean
- in: query
name: showInactive
required: false

View file

@ -25267,6 +25267,12 @@ paths:
required: false
schema:
type: string
- in: query
name: showAgentless
required: false
schema:
default: true
type: boolean
- in: query
name: showInactive
required: false

View file

@ -42,6 +42,7 @@ export const AGENT_POLICY_MAPPINGS = {
unenroll_timeout: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },
supports_agentless: { type: 'boolean' },
},
} as const;

View file

@ -21,6 +21,7 @@ import type { ListResult, ListWithKuery } from './common';
export interface GetAgentsRequest {
query: ListWithKuery & {
showAgentless?: boolean;
showInactive?: boolean;
showUpgradeable?: boolean;
withMetrics?: boolean;

View file

@ -409,6 +409,13 @@ describe('getFieldSpecs', () => {
searchable: true,
type: 'string',
},
{
aggregatable: true,
esTypes: ['boolean'],
name: 'ingest-agent-policies.supports_agentless',
searchable: true,
type: 'boolean',
},
]
);
});

View file

@ -6,7 +6,7 @@
*/
import React, { useMemo, useState, useCallback } from 'react';
import { Redirect, useRouteMatch, useLocation } from 'react-router-dom';
import { Redirect, useRouteMatch } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
import { i18n } from '@kbn/i18n';
@ -24,6 +24,7 @@ import {
useStartServices,
useFleetStatus,
useIntraAppState,
useUrlParams,
} from '../../../hooks';
import { Loading, Error, AgentEnrollmentFlyout } from '../../../components';
import { WithHeaderLayout } from '../../../layouts';
@ -40,12 +41,20 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => {
params: { policyId, tabId = '' },
} = useRouteMatch<{ policyId: string; tabId?: string }>();
const { getHref } = useLink();
const { urlParams } = useUrlParams();
const showAgentless = urlParams.showAgentless === 'true';
const agentPolicyRequest = useGetOneAgentPolicy(policyId);
const agentPolicy = agentPolicyRequest.data ? agentPolicyRequest.data.item : null;
// If the agent policy is agentless, hide it by default unless `showAgentless` url param is true
const agentPolicy =
agentPolicyRequest.data?.item &&
(!agentPolicyRequest.data.item.supports_agentless || showAgentless)
? agentPolicyRequest.data.item
: null;
const { isLoading, error, sendRequest: refreshAgentPolicy } = agentPolicyRequest;
const queryParams = new URLSearchParams(useLocation().search);
const openEnrollmentFlyoutOpenByDefault = queryParams.get('openEnrollmentFlyout') === 'true';
const openAddAgentHelpPopoverOpenByDefault = queryParams.get('showAddAgentHelp') === 'true';
const openEnrollmentFlyoutOpenByDefault = urlParams.openEnrollmentFlyout === 'true';
const openAddAgentHelpPopoverOpenByDefault = urlParams.showAddAgentHelp === 'true';
const [redirectToAgentPolicyList] = useState<boolean>(false);
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(
openEnrollmentFlyoutOpenByDefault

View file

@ -26,7 +26,6 @@ import { useHistory } from 'react-router-dom';
import type { AgentPolicy } from '../../../types';
import { getRootIntegrations } from '../../../../../../common/services';
import {
AGENT_POLICY_SAVED_OBJECT_TYPE,
LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE,
INGEST_SAVED_OBJECT_INDEX,
} from '../../../constants';
@ -39,7 +38,6 @@ import {
useUrlParams,
useBreadcrumbs,
useGetAgentPoliciesQuery,
useFleetStatus,
} from '../../../hooks';
import { SearchBar } from '../../../components';
import { AgentPolicySummaryLine } from '../../../../../components';
@ -48,17 +46,17 @@ import { LinkedAgentCount, AgentPolicyActionMenu } from '../components';
import { CreateAgentPolicyFlyout } from './components';
export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
const { isSpaceAwarenessEnabled } = useFleetStatus();
useBreadcrumbs('policies_list');
const { getPath } = useLink();
const hasFleetAllAgentPoliciesPrivileges = useAuthz().fleet.allAgentPolicies;
const agentPolicySavedObjectType = LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE;
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
// Table and search states
const { urlParams, toUrlParams } = useUrlParams();
const showAgentless = urlParams.showAgentless === 'true';
const [search, setSearch] = useState<string>(
Array.isArray(urlParams.kuery)
? urlParams.kuery[urlParams.kuery.length - 1]
@ -87,6 +85,15 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
[getPath, history, isCreateAgentPolicyFlyoutOpen, toUrlParams, urlParams]
);
// Hide agentless policies by default unless `showAgentless` url param is true
const getSearchWithDefaults = (newSearch: string) => {
if (showAgentless) {
return newSearch;
}
const defaultSearch = `NOT ${agentPolicySavedObjectType}.supports_agentless:true`;
return newSearch.trim() ? `(${defaultSearch}) AND (${newSearch})` : defaultSearch;
};
// Fetch agent policies
const {
isLoading,
@ -97,7 +104,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
perPage: pagination.pageSize,
sortField: sorting?.field,
sortOrder: sorting?.direction,
kuery: search,
kuery: getSearchWithDefaults(search),
withAgentCount: true, // Explicitly fetch agent count
full: true,
});
@ -328,11 +335,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
<SearchBar
value={search}
indexPattern={INGEST_SAVED_OBJECT_INDEX}
fieldPrefix={
isSpaceAwarenessEnabled
? AGENT_POLICY_SAVED_OBJECT_TYPE
: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE
}
fieldPrefix={agentPolicySavedObjectType}
onChange={(newSearch) => {
setPagination({
...pagination,

View file

@ -23,6 +23,7 @@ import {
useBreadcrumbs,
useStartServices,
useIntraAppState,
useUrlParams,
} from '../../../hooks';
import { WithHeaderLayout } from '../../../layouts';
@ -40,6 +41,8 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
params: { agentId, tabId = '' },
} = useRouteMatch<{ agentId: string; tabId?: string }>();
const { getHref } = useLink();
const { urlParams } = useUrlParams();
const showAgentless = urlParams.showAgentless === 'true';
const {
isLoading,
isInitialRequest,
@ -71,7 +74,12 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
}
}, [routeState, navigateToApp]);
const host = agentData?.item?.local_metadata?.host;
const agent =
agentData?.item &&
(showAgentless || !agentData.item.local_metadata?.host?.hostname?.startsWith('agentless-')) // Hide agentless agents
? agentData.item
: null;
const host = agent && agent.local_metadata?.host;
const headerLeftContent = useMemo(
() => (
@ -110,14 +118,14 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
const headerRightContent = useMemo(
() =>
agentData && agentData.item ? (
agent ? (
<>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" gutterSize="s" direction="row">
{!isAgentPolicyLoading && (
<EuiFlexItem grow={false}>
<AgentDetailsActionMenu
agent={agentData.item}
agent={agent}
agentPolicy={agentPolicyData?.item}
assignFlyoutOpenByDefault={openReassignFlyoutOpenByDefault}
onCancelReassign={
@ -199,8 +207,8 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
}
error={error}
/>
) : agentData && agentData.item ? (
<AgentDetailsPageContent agent={agentData.item} agentPolicy={agentPolicyData?.item} />
) : agent ? (
<AgentDetailsPageContent agent={agent} agentPolicy={agentPolicyData?.item} />
) : (
<Error
title={

View file

@ -101,6 +101,7 @@ export function useFetchAgentsData() {
const history = useHistory();
const { urlParams, toUrlParams } = useUrlParams();
const showAgentless = urlParams.showAgentless === 'true';
const defaultKuery: string = (urlParams.kuery as string) || '';
const urlHasInactive = (urlParams.showInactive as string) === 'true';
@ -213,6 +214,7 @@ export function useFetchAgentsData() {
kuery: kuery && kuery !== '' ? kuery : undefined,
sortField: getSortFieldForAPI(sortField),
sortOrder,
showAgentless,
showInactive,
showUpgradeable,
getStatusSummary: true,
@ -358,6 +360,7 @@ export function useFetchAgentsData() {
kuery,
sortField,
sortOrder,
showAgentless,
showInactive,
showUpgradeable,
fullAgentPolicyFecher,

View file

@ -203,6 +203,7 @@ export const getAgentsHandler: FleetRequestHandler<
const agentRes = await agentClient.asCurrentUser.listAgents({
page: request.query.page,
perPage: request.query.perPage,
showAgentless: request.query.showAgentless,
showInactive: request.query.showInactive,
showUpgradeable: request.query.showUpgradeable,
kuery: request.query.kuery,

View file

@ -227,6 +227,7 @@ export abstract class ActionRunner {
return getAgentsByKuery(this.esClient, this.soClient, {
kuery,
showAgentless: this.actionParams.showAgentless,
showInactive: this.actionParams.showInactive ?? false,
page: 1,
perPage,

View file

@ -99,7 +99,9 @@ describe('AgentService', () => {
});
it('rejects on listAgents', async () => {
await expect(agentClient.listAgents({ showInactive: true })).rejects.toThrowError(
await expect(
agentClient.listAgents({ showAgentless: true, showInactive: true })
).rejects.toThrowError(
new FleetUnauthorizedError(
`User does not have adequate permissions to access Fleet agents.`
)
@ -224,10 +226,11 @@ function expectApisToCallServicesSuccessfully(
test('client.listAgents calls getAgentsByKuery and returns results', async () => {
mockGetAgentsByKuery.mockResolvedValue('getAgentsByKuery success');
await expect(agentClient.listAgents({ showInactive: true })).resolves.toEqual(
'getAgentsByKuery success'
);
await expect(
agentClient.listAgents({ showAgentless: true, showInactive: true })
).resolves.toEqual('getAgentsByKuery success');
expect(mockGetAgentsByKuery).toHaveBeenCalledWith(mockEsClient, mockSoClient, {
showAgentless: true,
showInactive: true,
spaceId,
});

View file

@ -89,6 +89,7 @@ export interface AgentClient {
*/
listAgents(
options: ListWithKuery & {
showAgentless?: boolean;
showInactive: boolean;
aggregations?: Record<string, AggregationsAggregationContainer>;
searchAfter?: SortResults;
@ -134,6 +135,7 @@ class AgentClientImpl implements AgentClient {
public async listAgents(
options: ListWithKuery & {
showAgentless?: boolean;
showInactive: boolean;
aggregations?: Record<string, AggregationsAggregationContainer>;
searchAfter?: SortResults;

View file

@ -181,6 +181,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2', '3', '4', '5', 'up', '7'], 7, 'inactive'))
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: true,
showInactive: false,
page: 1,
@ -212,6 +213,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2', '3', 'up', '5', 'up2', '7'], 7, 'inactive'))
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: true,
showInactive: false,
page: 1,
@ -250,6 +252,7 @@ describe('Agents CRUD test', () => {
)
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: true,
showInactive: false,
page: 2,
@ -277,6 +280,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2', '3', 'up', '5'], 10001, 'inactive'))
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: true,
showInactive: false,
page: 1,
@ -307,6 +311,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2', '3', 'up', '5'], 100, 'updating'))
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: true,
showInactive: false,
getStatusSummary: true,
@ -344,6 +349,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2', '3', 'up', '5'], 10001, 'updating'))
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: true,
showInactive: false,
getStatusSummary: true,
@ -377,6 +383,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['6', '7'], 7, 'inactive'))
);
const result = await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showUpgradeable: false,
showInactive: false,
page: 2,
@ -411,6 +418,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2'], 2, 'inactive'))
);
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: false,
});
@ -425,6 +433,7 @@ describe('Agents CRUD test', () => {
Promise.resolve(getEsResponse(['1', '2'], 2, 'inactive'))
);
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: false,
sortField: 'policy_id',
});
@ -437,6 +446,7 @@ describe('Agents CRUD test', () => {
});
it('should add inactive and unenrolled filter', async () => {
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: false,
kuery: '',
});
@ -448,6 +458,7 @@ describe('Agents CRUD test', () => {
it('should add unenrolled filter', async () => {
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: true,
kuery: '',
});
@ -459,6 +470,7 @@ describe('Agents CRUD test', () => {
it('should not add unenrolled filter', async () => {
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: true,
kuery: 'status:unenrolled',
});
@ -470,6 +482,7 @@ describe('Agents CRUD test', () => {
it('should add inactive filter', async () => {
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: false,
kuery: 'status:*',
});
@ -481,6 +494,7 @@ describe('Agents CRUD test', () => {
it('should not add inactive filter', async () => {
await getAgentsByKuery(esClientMock, soClientMock, {
showAgentless: true,
showInactive: true,
kuery: 'status:*',
});

View file

@ -20,7 +20,7 @@ import type { AgentStatus, FleetServerAgent } from '../../../common/types';
import { SO_SEARCH_LIMIT } from '../../../common/constants';
import { getSortConfig } from '../../../common';
import { isAgentUpgradeAvailable } from '../../../common/services';
import { AGENTS_INDEX } from '../../constants';
import { AGENTS_INDEX, LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../constants';
import {
FleetError,
isESClientError,
@ -89,6 +89,7 @@ export type GetAgentsOptions =
}
| {
kuery: string;
showAgentless?: boolean;
showInactive?: boolean;
perPage?: number;
};
@ -107,6 +108,7 @@ export async function getAgents(
agents = (
await getAllAgentsByKuery(esClient, soClient, {
kuery: options.kuery,
showAgentless: options.showAgentless,
showInactive: options.showInactive ?? false,
})
).agents;
@ -200,6 +202,7 @@ export async function getAgentsByKuery(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
options: ListWithKuery & {
showAgentless?: boolean;
showInactive: boolean;
spaceId?: string;
getStatusSummary?: boolean;
@ -225,6 +228,7 @@ export async function getAgentsByKuery(
sortField = options.sortField ?? 'enrolled_at',
sortOrder = options.sortOrder ?? 'desc',
kuery,
showAgentless = true,
showInactive = false,
getStatusSummary = false,
showUpgradeable,
@ -241,9 +245,26 @@ export async function getAgentsByKuery(
filters.push(kuery);
}
// Hides agents enrolled in agentless policies by excluding the first 1000 agentless policy IDs
// from the search. This limitation is to avoid hitting the `max_clause_count` limit.
// In the future, we should hopefully be able to filter agentless agents using metadata:
// https://github.com/elastic/elastic-agent/issues/7946
if (showAgentless === false) {
const agentlessPolicies = await agentPolicyService.list(soClient, {
perPage: 1000,
kuery: `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.supports_agentless:true`,
});
if (agentlessPolicies.items.length > 0) {
filters.push(
`NOT policy_id: (${agentlessPolicies.items.map((policy) => `"${policy.id}"`).join(' or ')})`
);
}
}
if (showInactive === false) {
filters.push(ACTIVE_AGENT_CONDITION);
}
if (!includeUnenrolled(kuery)) {
filters.push(ENROLLED_AGENT_CONDITION);
}
@ -386,6 +407,7 @@ export async function getAllAgentsByKuery(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
options: Omit<ListWithKuery, 'page' | 'perPage'> & {
showAgentless?: boolean;
showInactive: boolean;
}
): Promise<{

View file

@ -127,6 +127,7 @@ export async function reassignAgents(
const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery;
const res = await getAgentsByKuery(esClient, soClient, {
kuery,
showAgentless: options.showAgentless,
showInactive: options.showInactive ?? false,
page: 1,
perPage: batchSize,

View file

@ -106,6 +106,7 @@ export async function unenrollAgents(
const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery;
const res = await getAgentsByKuery(esClient, soClient, {
kuery,
showAgentless: options.showAgentless,
showInactive: options.showInactive ?? false,
page: 1,
perPage: batchSize,

View file

@ -22,9 +22,9 @@ export async function unenrollForAgentPolicyId(
while (hasMore) {
const { agents } = await getAgentsByKuery(esClient, soClient, {
kuery: `${AGENTS_PREFIX}.policy_id:"${policyId}"`,
showInactive: false,
page: page++,
perPage: 1000,
showInactive: false,
});
if (agents.length === 0) {

View file

@ -62,6 +62,7 @@ export async function updateAgentTags(
// calculate total count
const res = await getAgentsByKuery(esClient, soClient, {
kuery,
showAgentless: options.showAgentless,
showInactive: options.showInactive ?? false,
perPage: 0,
pitId,

View file

@ -101,6 +101,7 @@ export async function sendUpgradeAgentsActions(
const res = await getAgentsByKuery(esClient, soClient, {
kuery,
showAgentless: options.showAgentless,
showInactive: options.showInactive ?? false,
page: 1,
perPage: batchSize,

View file

@ -262,8 +262,13 @@ async function createSetupSideEffects(
await ensureAgentPoliciesFleetServerKeysAndPolicies({ soClient, esClient, logger });
stepSpan?.end();
logger.debug('Backfilling package policy supports_agentless field');
await backfillPackagePolicySupportsAgentless(esClient);
let backfillPackagePolicySupportsAgentlessError;
try {
logger.debug('Backfilling package policy supports_agentless field');
await backfillPackagePolicySupportsAgentless(esClient);
} catch (error) {
backfillPackagePolicySupportsAgentlessError = { error };
}
let ensureCorrectAgentlessSettingsIdsError;
try {
@ -286,6 +291,9 @@ async function createSetupSideEffects(
const nonFatalErrors = [
...preconfiguredPackagesNonFatalErrors,
...(messageSigningServiceNonFatalError ? [messageSigningServiceNonFatalError] : []),
...(backfillPackagePolicySupportsAgentlessError
? [backfillPackagePolicySupportsAgentlessError]
: []),
...(ensureCorrectAgentlessSettingsIdsError ? [ensureCorrectAgentlessSettingsIdsError] : []),
];

View file

@ -33,6 +33,7 @@ export const GetAgentsRequestSchema = {
},
})
),
showAgentless: schema.boolean({ defaultValue: true }),
showInactive: schema.boolean({ defaultValue: false }),
withMetrics: schema.boolean({ defaultValue: false }),
showUpgradeable: schema.boolean({ defaultValue: false }),

View file

@ -47,6 +47,7 @@ export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContex
async (context, request, response) => {
let esAgents;
const query = request.query as ListWithKuery & {
showAgentless?: boolean;
showInactive: boolean;
searchAfter?: SortResults;
pitId?: string;
@ -110,6 +111,7 @@ export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContex
pitId: query.pitId,
searchAfter: query.searchAfter,
kuery,
showAgentless: query.showAgentless,
showInactive: query.showInactive,
aggregations: {
platforms: {

View file

@ -0,0 +1,41 @@
/*
* 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 { createServer } from '@mswjs/http-middleware';
import { http, HttpResponse, StrictResponse } from 'msw';
export const setupMockServer = () => {
const server = createServer(...deploymentHandler);
return server;
};
interface AgentlessApiDeploymentResponse {
code: string;
error: string | null;
}
const deploymentHandler = [
http.post(
'agentless-api/api/v1/ess/deployments',
async ({ request }): Promise<StrictResponse<AgentlessApiDeploymentResponse>> => {
return HttpResponse.json({
code: 'SUCCESS',
error: null,
});
}
),
http.delete(
'agentless-api/api/v1/ess/deployments/*',
async (): Promise<StrictResponse<AgentlessApiDeploymentResponse>> => {
return HttpResponse.json({
code: 'SUCCESS',
error: null,
});
}
),
];

View file

@ -5,19 +5,25 @@
* 2.0.
*/
import * as http from 'http';
import expect from '@kbn/expect';
import { type Agent, FLEET_ELASTIC_AGENT_PACKAGE, AGENTS_INDEX } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { setupMockServer } from './helpers/mock_agentless_api';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const es = getService('es');
const mockAgentlessApiService = setupMockServer();
let elasticAgentpkgVersion: string;
describe('fleet_list_agent', () => {
let mockApiServer: http.Server;
before(async () => {
mockApiServer = await mockAgentlessApiService.listen(8089); // Start the agentless api mock server on port 8089
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents');
const getPkRes = await supertest
.get(`/api/fleet/epm/packages/${FLEET_ELASTIC_AGENT_PACKAGE}`)
@ -34,6 +40,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200);
});
after(async () => {
mockApiServer.close();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents');
await supertest
.delete(`/api/fleet/epm/packages/${FLEET_ELASTIC_AGENT_PACKAGE}/${elasticAgentpkgVersion}`)
@ -267,6 +274,73 @@ export default function ({ getService }: FtrProviderContext) {
});
});
it('should not return agentless agents when showAgentless=false', async () => {
// Set up default Fleet Server host, needed during agentless agent creation
await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
id: 'default-agentless-fleet-server-host',
name: 'Default',
is_default: true,
host_urls: ['https://test.com:8080', 'https://test.com:8081'],
})
.expect(200);
// Create an agentless agent policy
const { body: policyRes } = await supertest
.post('/api/fleet/agent_policies')
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Agentless Policy',
namespace: 'default',
supports_agentless: true,
})
.expect(200);
const agentlessPolicyId = policyRes.item.id;
// Enroll an agent into the agentless policy
const agentId = 'agentless-agent-1';
await es.index({
index: AGENTS_INDEX,
id: agentId,
refresh: 'wait_for',
document: {
type: 'PERMANENT',
active: true,
enrolled_at: new Date().toISOString(),
local_metadata: { elastic: { agent: { id: agentId, version: '9.0.0' } } },
status: 'online',
policy_id: agentlessPolicyId,
},
});
// Call the agent list API without showAgentless parameter
const { body: apiResponseWithAgentless } = await supertest.get('/api/fleet/agents');
// Assert that the agentless agent is returned
const agentIdsWithAgentless = apiResponseWithAgentless.items.map((agent: any) => agent.id);
expect(agentIdsWithAgentless).to.contain(agentId);
// Call the agent list API with showAgentless=false
const { body: apiResponse } = await supertest
.get('/api/fleet/agents?showAgentless=false')
.expect(200);
// Assert that the agentless agent is not returned
const agentIds = apiResponse.items.map((agent: any) => agent.id);
expect(agentIds).not.contain(agentId);
// Cleanup: delete the agent and policy
await es.delete({ index: AGENTS_INDEX, id: agentId, refresh: 'wait_for' });
await supertest
.post(`/api/fleet/agent_policies/delete`)
.set('kbn-xsrf', 'xxxx')
.send({ agentPolicyId: agentlessPolicyId })
.expect(200);
});
describe('advanced search params', () => {
afterEach(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents');

View file

@ -14,6 +14,7 @@ import {
getKibanaCliLoggers,
} from '@kbn/test';
import { ScoutTestRunConfigCategory } from '@kbn/scout-info';
import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
const getFullPath = (relativePath: string) => path.join(path.dirname(__filename), relativePath);
@ -91,10 +92,10 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext
])}`,
`--xpack.cloud.id='123456789'`,
`--xpack.fleet.agentless.enabled=true`,
`--xpack.fleet.agentless.api.url=https://api.agentless.url/api/v1/ess`,
`--xpack.fleet.agentless.api.tls.certificate=./config/node.crt`,
`--xpack.fleet.agentless.api.tls.key=./config/node.key`,
`--xpack.fleet.agentless.api.tls.ca=./config/ca.crt`,
`--xpack.fleet.agentless.api.url=http://localhost:8089/agentless-api`,
`--xpack.fleet.agentless.api.tls.certificate=${KBN_CERT_PATH}`,
`--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`,
`--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`,
`--xpack.fleet.internal.registry.kibanaVersionCheckEnabled=false`,
`--xpack.fleet.internal.registry.spec.min=1.0`,
`--logging.loggers=${JSON.stringify([