From bfbd584c0fbd667bc77568f0878c57f933d448a3 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 13 Jan 2021 08:20:23 -0500 Subject: [PATCH] [Fleet] Show Count of Agent Policies on Integration Details (#86916) (#88068) * component to show count of agent policies for integration * API route and service to return stats of package usage --- .../plugins/fleet/common/constants/routes.ts | 1 + .../plugins/fleet/common/services/routes.ts | 4 + .../plugins/fleet/common/types/models/epm.ts | 4 + .../fleet/common/types/rest_spec/epm.ts | 11 ++ .../fleet/hooks/use_request/epm.ts | 8 + .../epm/screens/detail/index.test.tsx | 108 +++++++++-- .../sections/epm/screens/detail/index.tsx | 16 +- .../detail/integration_agent_policy_count.tsx | 17 ++ .../fleet/server/routes/epm/handlers.ts | 20 ++ .../plugins/fleet/server/routes/epm/index.ts | 11 ++ .../server/services/epm/packages/get.test.ts | 171 ++++++++++++++++++ .../fleet/server/services/epm/packages/get.ts | 46 ++++- .../fleet/server/types/rest_spec/epm.ts | 6 + 13 files changed, 402 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/get.test.ts diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index ed38d8a29f035..a583f38419ae1 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -29,6 +29,7 @@ export const EPM_API_ROUTES = { DELETE_PATTERN: EPM_PACKAGES_ONE, FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`, CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, + STATS_PATTERN: `${EPM_PACKAGES_MANY}/{pkgName}/stats`, }; // Data stream API routes diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 4af3f3beb32be..89c8a7ef40729 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -35,6 +35,10 @@ export const epmRouteService = { return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); }, + getStatsPath: (pkgName: string) => { + return EPM_API_ROUTES.STATS_PATTERN.replace('{pkgName}', pkgName); + }, + getFilePath: (filePath: string) => { return `${EPM_API_ROOT}${filePath.replace('/package', '/packages')}`; }, diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 77625e48dbc96..08f92f406b90f 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -279,6 +279,10 @@ export interface Installation extends SavedObjectAttributes { install_source: InstallSource; } +export interface PackageUsageStats { + agent_policy_count: number; +} + export type Installable = Installed | NotInstalled; export type Installed = T & { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index 7299fbb5e5d65..f931138f7ad49 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -10,6 +10,7 @@ import { Installable, RegistrySearchResult, PackageInfo, + PackageUsageStats, } from '../models/epm'; export interface GetCategoriesRequest { @@ -54,6 +55,16 @@ export interface GetInfoResponse { response: PackageInfo; } +export interface GetStatsRequest { + params: { + pkgname: string; + }; +} + +export interface GetStatsResponse { + response: PackageUsageStats; +} + export interface InstallPackageRequest { params: { pkgkey: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/epm.ts index 40a22f6b44d50..d8bf7d208c05f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/epm.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/epm.ts @@ -16,6 +16,7 @@ import { InstallPackageResponse, DeletePackageResponse, } from '../../types'; +import { GetStatsResponse } from '../../../../../common'; export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => { return useRequest({ @@ -47,6 +48,13 @@ export const useGetPackageInfoByKey = (pkgkey: string) => { }); }; +export const useGetPackageStats = (pkgName: string) => { + return useRequest({ + path: epmRouteService.getStatsPath(pkgName), + method: 'get', + }); +}; + export const sendGetPackageInfoByKey = (pkgkey: string) => { return sendRequest({ path: epmRouteService.getInfoPath(pkgkey), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 2e4c65955e0da..7709e494cf9fc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -14,6 +14,7 @@ import { GetFleetStatusResponse, GetInfoResponse, GetPackagePoliciesResponse, + GetStatsResponse, } from '../../../../../../../common/types/rest_spec'; import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models'; import { @@ -29,7 +30,7 @@ describe('when on integration detail', () => { const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey }); let testRenderer: TestRenderer; let renderResult: ReturnType; - let mockedApi: MockedApi; + let mockedApi: MockedApi; const render = () => (renderResult = testRenderer.render( @@ -48,6 +49,39 @@ describe('when on integration detail', () => { window.location.hash = '#/'; }); + describe('and the package is installed', () => { + beforeEach(() => render()); + + it('should display agent policy usage count', async () => { + await mockedApi.waitForApi(); + expect(renderResult.queryByTestId('agentPolicyCount')).not.toBeNull(); + }); + + it('should show the Policies tab', async () => { + await mockedApi.waitForApi(); + expect(renderResult.queryByTestId('tab-policies')).not.toBeNull(); + }); + }); + + describe('and the package is not installed', () => { + beforeEach(() => { + const unInstalledPackage = mockedApi.responseProvider.epmGetInfo(); + unInstalledPackage.response.status = 'not_installed'; + mockedApi.responseProvider.epmGetInfo.mockReturnValue(unInstalledPackage); + render(); + }); + + it('should NOT display agent policy usage count', async () => { + await mockedApi.waitForApi(); + expect(renderResult.queryByTestId('agentPolicyCount')).toBeNull(); + }); + + it('should NOT the Policies tab', async () => { + await mockedApi.waitForApi(); + expect(renderResult.queryByTestId('tab-policies')).toBeNull(); + }); + }); + describe('and a custom UI extension is NOT registered', () => { beforeEach(() => render()); @@ -190,12 +224,27 @@ describe('when on integration detail', () => { }); }); -interface MockedApi { +interface MockedApi< + R extends Record> = Record> +> { /** Will return a promise that resolves when triggered APIs are complete */ waitForApi: () => Promise; + /** A object containing the list of API response provider functions that are used by the mocked API */ + responseProvider: R; } -const mockApiCalls = (http: MockedFleetStartServices['http']): MockedApi => { +interface EpmPackageDetailsResponseProvidersMock { + epmGetInfo: jest.MockedFunction<() => GetInfoResponse>; + epmGetFile: jest.MockedFunction<() => string>; + epmGetStats: jest.MockedFunction<() => GetStatsResponse>; + fleetSetup: jest.MockedFunction<() => GetFleetStatusResponse>; + packagePolicyList: jest.MockedFunction<() => GetPackagePoliciesResponse>; + agentPolicyList: jest.MockedFunction<() => GetAgentPoliciesResponse>; +} + +const mockApiCalls = ( + http: MockedFleetStartServices['http'] +): MockedApi => { let inflightApiCalls = 0; const apiDoneListeners: Array<() => void> = []; const markApiCallAsHandled = async () => { @@ -663,31 +712,62 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos perPage: 100, }; + const epmGetStatsResponse: GetStatsResponse = { + response: { + agent_policy_count: 2, + }, + }; + + const mockedApiInterface: MockedApi = { + waitForApi() { + return new Promise((resolve) => { + if (inflightApiCalls > 0) { + apiDoneListeners.push(resolve); + } else { + resolve(); + } + }); + }, + responseProvider: { + epmGetInfo: jest.fn().mockReturnValue(epmPackageResponse), + epmGetFile: jest.fn().mockReturnValue(packageReadMe), + epmGetStats: jest.fn().mockReturnValue(epmGetStatsResponse), + fleetSetup: jest.fn().mockReturnValue(agentsSetupResponse), + packagePolicyList: jest.fn().mockReturnValue(packagePoliciesResponse), + agentPolicyList: jest.fn().mockReturnValue(agentPoliciesResponse), + }, + }; + http.get.mockImplementation(async (path) => { if (typeof path === 'string') { if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) { markApiCallAsHandled(); - return epmPackageResponse; + return mockedApiInterface.responseProvider.epmGetInfo(); } if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) { markApiCallAsHandled(); - return packageReadMe; + return mockedApiInterface.responseProvider.epmGetFile(); } if (path === fleetSetupRouteService.getFleetSetupPath()) { markApiCallAsHandled(); - return agentsSetupResponse; + return mockedApiInterface.responseProvider.fleetSetup(); } if (path === packagePolicyRouteService.getListPath()) { markApiCallAsHandled(); - return packagePoliciesResponse; + return mockedApiInterface.responseProvider.packagePolicyList(); } if (path === agentPolicyRouteService.getListPath()) { markApiCallAsHandled(); - return agentPoliciesResponse; + return mockedApiInterface.responseProvider.agentPolicyList(); + } + + if (path === epmRouteService.getStatsPath('nginx')) { + markApiCallAsHandled(); + return mockedApiInterface.responseProvider.epmGetStats(); } const err = new Error(`API [GET ${path}] is not MOCKED!`); @@ -697,15 +777,5 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos } }); - return { - waitForApi() { - return new Promise((resolve) => { - if (inflightApiCalls > 0) { - apiDoneListeners.push(resolve); - } else { - resolve(); - } - }); - }, - }; + return mockedApiInterface; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index eee5dede16cad..a9c117222c3c2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -44,6 +44,7 @@ import './index.scss'; import { useUIExtension } from '../../../../hooks/use_ui_extension'; import { PLUGIN_ID } from '../../../../../../../common/constants'; import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; +import { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -239,6 +240,18 @@ export function Detail() { ), }, + ...(packageInstallStatus === 'installed' + ? [ + { isDivider: true }, + { + label: i18n.translate('xpack.fleet.epm.usedByLabel', { + defaultMessage: 'Agent Policies', + }), + 'data-test-subj': 'agentPolicyCount', + content: , + }, + ] + : []), { isDivider: true }, { content: ( @@ -264,7 +277,7 @@ export function Detail() { ), }, ].map((item, index) => ( - + {item.isDivider ?? false ? ( ) : item.label ? ( @@ -285,6 +298,7 @@ export function Detail() { handleAddIntegrationPolicyClick, hasWriteCapabilites, packageInfo, + packageInstallStatus, pkgkey, updateAvailable, ] diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx new file mode 100644 index 0000000000000..2baf999977e3c --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { useGetPackageStats } from '../../../../hooks'; + +/** + * Displays a count of Agent Policies that are using the given integration + */ +export const IntegrationAgentPolicyCount = memo<{ packageName: string }>(({ packageName }) => { + const { data } = useGetPackageStats(packageName); + + return <>{data?.response.agent_policy_count ?? 0}; +}); diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 9ccf60dc80a5f..23e3e6e7b34f1 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -17,6 +17,7 @@ import { BulkInstallPackageInfo, BulkInstallPackagesResponse, IBulkInstallPackageHTTPError, + GetStatsResponse, } from '../../../common'; import { GetCategoriesRequestSchema, @@ -27,6 +28,7 @@ import { InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, BulkUpgradePackagesFromRegistryRequestSchema, + GetStatsRequestSchema, } from '../../types'; import { BulkInstallResponse, @@ -48,6 +50,7 @@ import { splitPkgKey } from '../../services/epm/registry'; import { licenseService } from '../../services'; import { getArchiveEntry } from '../../services/epm/archive/cache'; import { getAsset } from '../../services/epm/archive/storage'; +import { getPackageUsageStats } from '../../services/epm/packages/get'; export const getCategoriesHandler: RequestHandler< undefined, @@ -196,6 +199,23 @@ export const getInfoHandler: RequestHandler> = async ( + context, + request, + response +) => { + try { + const { pkgName } = request.params; + const savedObjectsClient = context.core.savedObjects.client; + const body: GetStatsResponse = { + response: await getPackageUsageStats({ savedObjectsClient, pkgName }), + }; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + export const installPackageFromRegistryHandler: RequestHandler< TypeOf, undefined, diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index eaf61335b5e06..337903aa89993 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -15,6 +15,7 @@ import { installPackageByUploadHandler, deletePackageHandler, bulkInstallPackagesFromRegistryHandler, + getStatsHandler, } from './handlers'; import { GetCategoriesRequestSchema, @@ -25,6 +26,7 @@ import { InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, BulkUpgradePackagesFromRegistryRequestSchema, + GetStatsRequestSchema, } from '../../types'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -57,6 +59,15 @@ export const registerRoutes = (router: IRouter) => { getLimitedListHandler ); + router.get( + { + path: EPM_API_ROUTES.STATS_PATTERN, + validate: GetStatsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getStatsHandler + ); + router.get( { path: EPM_API_ROUTES.FILEPATH_PATTERN, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts new file mode 100644 index 0000000000000..7ee193b29a165 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract, SavedObjectsFindResult } from 'kibana/server'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PackagePolicySOAttributes } from '../../../../common'; +import { getPackageUsageStats } from './get'; + +describe('When using EPM `get` services', () => { + let soClient: jest.Mocked; + + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + }); + + describe('and invoking getPackageUsageStats()', () => { + beforeEach(() => { + const savedObjects: Array> = [ + { + type: 'ingest-package-policies', + id: 'dcf83172-c38e-4501-b236-9f479da8a7d6', + attributes: { + name: 'system-3', + description: '', + namespace: 'default', + policy_id: '22222-22222-2222-2222', + enabled: true, + output_id: '', + inputs: [], + package: { name: 'system', title: 'System', version: '0.10.4' }, + revision: 1, + created_at: '2020-12-22T21:28:05.380Z', + created_by: 'elastic', + updated_at: '2020-12-22T21:28:05.380Z', + updated_by: 'elastic', + }, + references: [], + migrationVersion: { 'ingest-package-policies': '7.11.0' }, + updated_at: '2020-12-22T21:28:05.383Z', + version: 'WzE1NTAsMV0=', + score: 0, + }, + { + type: 'ingest-package-policies', + id: '5b61eb5c-d94c-48a6-a17c-b0d1f7c65336', + attributes: { + name: 'system-1', + namespace: 'default', + package: { name: 'system', title: 'System', version: '0.10.4' }, + enabled: true, + policy_id: '11111-111111-11111-11111', // << duplicate id with plicy below + output_id: 'ca111b80-43c1-11eb-84bf-7177b74381c5', + inputs: [], + revision: 1, + created_at: '2020-12-21T19:22:04.902Z', + created_by: 'system', + updated_at: '2020-12-21T19:22:04.902Z', + updated_by: 'system', + }, + references: [], + migrationVersion: { 'ingest-package-policies': '7.11.0' }, + updated_at: '2020-12-21T19:22:04.905Z', + version: 'WzIxNSwxXQ==', + score: 0, + }, + { + type: 'ingest-package-policies', + id: 'dcf83172-c38e-4501-b236-9f479da8a7d6', + attributes: { + name: 'system-2', + description: '', + namespace: 'default', + policy_id: '11111-111111-11111-11111', + enabled: true, + output_id: '', + inputs: [], + package: { name: 'system', title: 'System', version: '0.10.4' }, + revision: 1, + created_at: '2020-12-22T21:28:05.380Z', + created_by: 'elastic', + updated_at: '2020-12-22T21:28:05.380Z', + updated_by: 'elastic', + }, + references: [], + migrationVersion: { 'ingest-package-policies': '7.11.0' }, + updated_at: '2020-12-22T21:28:05.383Z', + version: 'WzE1NTAsMV0=', + score: 0, + }, + { + type: 'ingest-package-policies', + id: 'dcf83172-c38e-4501-b236-9f479da8a7d6', + attributes: { + name: 'system-4', + description: '', + namespace: 'default', + policy_id: '33333-33333-333333-333333', + enabled: true, + output_id: '', + inputs: [], + package: { name: 'system', title: 'System', version: '0.10.4' }, + revision: 1, + created_at: '2020-12-22T21:28:05.380Z', + created_by: 'elastic', + updated_at: '2020-12-22T21:28:05.380Z', + updated_by: 'elastic', + }, + references: [], + migrationVersion: { 'ingest-package-policies': '7.11.0' }, + updated_at: '2020-12-22T21:28:05.383Z', + version: 'WzE1NTAsMV0=', + score: 0, + }, + ]; + soClient.find.mockImplementation(async ({ page = 1, perPage = 20 }) => { + let savedObjectsResponse: typeof savedObjects; + + switch (page) { + case 1: + savedObjectsResponse = [savedObjects[0]]; + break; + case 2: + savedObjectsResponse = savedObjects.slice(1); + break; + default: + savedObjectsResponse = []; + } + + return { + page, + per_page: perPage, + total: 1500, + saved_objects: savedObjectsResponse, + }; + }); + }); + + it('should query and paginate SO using package name as filter', async () => { + await getPackageUsageStats({ savedObjectsClient: soClient, pkgName: 'system' }); + expect(soClient.find).toHaveBeenNthCalledWith(1, { + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + perPage: 1000, + page: 1, + filter: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: system`, + }); + expect(soClient.find).toHaveBeenNthCalledWith(2, { + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + perPage: 1000, + page: 2, + filter: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: system`, + }); + expect(soClient.find).toHaveBeenNthCalledWith(3, { + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + perPage: 1000, + page: 3, + filter: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: system`, + }); + }); + + it('should return count of unique agent policies', async () => { + expect( + await getPackageUsageStats({ savedObjectsClient: soClient, pkgName: 'system' }) + ).toEqual({ + agent_policy_count: 3, + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index f59b7a8484035..07d651668409d 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -5,7 +5,13 @@ */ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; -import { isPackageLimited, installationStatuses } from '../../../../common'; +import { + isPackageLimited, + installationStatuses, + PackageUsageStats, + PackagePolicySOAttributes, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, +} from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; @@ -13,6 +19,7 @@ import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; import { getEsPackage } from '../archive/storage'; import { getArchivePackage } from '../archive'; +import { normalizeKuery } from '../../saved_object'; export { getFile, SearchParams } from '../registry'; @@ -116,6 +123,43 @@ export async function getPackageInfo(options: { return createInstallableFrom(updated, savedObject); } +export const getPackageUsageStats = async ({ + savedObjectsClient, + pkgName, +}: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; +}): Promise => { + const filter = normalizeKuery( + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${pkgName}` + ); + const agentPolicyCount = new Set(); + let page = 1; + let hasMore = true; + + while (hasMore) { + // using saved Objects client directly, instead of the `list()` method of `package_policy` service + // in order to not cause a circular dependency (package policy service imports from this module) + const packagePolicies = await savedObjectsClient.find({ + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + perPage: 1000, + page: page++, + filter, + }); + + for (let index = 0, total = packagePolicies.saved_objects.length; index < total; index++) { + agentPolicyCount.add(packagePolicies.saved_objects[index].attributes.policy_id); + } + + hasMore = packagePolicies.saved_objects.length > 0; + } + + return { + agent_policy_count: agentPolicyCount.size, + }; +}; + interface PackageResponse { paths: string[]; packageInfo: ArchivePackage | RegistryPackage; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 5d2a078374854..1947a6146c955 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -32,6 +32,12 @@ export const GetInfoRequestSchema = { }), }; +export const GetStatsRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + }), +}; + export const InstallPackageFromRegistryRequestSchema = { params: schema.object({ pkgkey: schema.string(),