[Fleet] Show Count of Agent Policies on Integration Details (#86916)
* component to show count of agent policies for integration * API route and service to return stats of package usage
This commit is contained in:
parent
31e66979b9
commit
02695ef5ad
|
@ -29,6 +29,7 @@ export const EPM_API_ROUTES = {
|
||||||
DELETE_PATTERN: EPM_PACKAGES_ONE,
|
DELETE_PATTERN: EPM_PACKAGES_ONE,
|
||||||
FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`,
|
FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`,
|
||||||
CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`,
|
CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`,
|
||||||
|
STATS_PATTERN: `${EPM_PACKAGES_MANY}/{pkgName}/stats`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Data stream API routes
|
// Data stream API routes
|
||||||
|
|
|
@ -35,6 +35,10 @@ export const epmRouteService = {
|
||||||
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey);
|
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getStatsPath: (pkgName: string) => {
|
||||||
|
return EPM_API_ROUTES.STATS_PATTERN.replace('{pkgName}', pkgName);
|
||||||
|
},
|
||||||
|
|
||||||
getFilePath: (filePath: string) => {
|
getFilePath: (filePath: string) => {
|
||||||
return `${EPM_API_ROOT}${filePath.replace('/package', '/packages')}`;
|
return `${EPM_API_ROOT}${filePath.replace('/package', '/packages')}`;
|
||||||
},
|
},
|
||||||
|
|
|
@ -279,6 +279,10 @@ export interface Installation extends SavedObjectAttributes {
|
||||||
install_source: InstallSource;
|
install_source: InstallSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PackageUsageStats {
|
||||||
|
agent_policy_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type Installable<T> = Installed<T> | NotInstalled<T>;
|
export type Installable<T> = Installed<T> | NotInstalled<T>;
|
||||||
|
|
||||||
export type Installed<T = {}> = T & {
|
export type Installed<T = {}> = T & {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
Installable,
|
Installable,
|
||||||
RegistrySearchResult,
|
RegistrySearchResult,
|
||||||
PackageInfo,
|
PackageInfo,
|
||||||
|
PackageUsageStats,
|
||||||
} from '../models/epm';
|
} from '../models/epm';
|
||||||
|
|
||||||
export interface GetCategoriesRequest {
|
export interface GetCategoriesRequest {
|
||||||
|
@ -54,6 +55,16 @@ export interface GetInfoResponse {
|
||||||
response: PackageInfo;
|
response: PackageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetStatsRequest {
|
||||||
|
params: {
|
||||||
|
pkgname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetStatsResponse {
|
||||||
|
response: PackageUsageStats;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InstallPackageRequest {
|
export interface InstallPackageRequest {
|
||||||
params: {
|
params: {
|
||||||
pkgkey: string;
|
pkgkey: string;
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
InstallPackageResponse,
|
InstallPackageResponse,
|
||||||
DeletePackageResponse,
|
DeletePackageResponse,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import { GetStatsResponse } from '../../../../../common';
|
||||||
|
|
||||||
export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
|
export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
|
||||||
return useRequest<GetCategoriesResponse>({
|
return useRequest<GetCategoriesResponse>({
|
||||||
|
@ -47,6 +48,13 @@ export const useGetPackageInfoByKey = (pkgkey: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetPackageStats = (pkgName: string) => {
|
||||||
|
return useRequest<GetStatsResponse>({
|
||||||
|
path: epmRouteService.getStatsPath(pkgName),
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const sendGetPackageInfoByKey = (pkgkey: string) => {
|
export const sendGetPackageInfoByKey = (pkgkey: string) => {
|
||||||
return sendRequest<GetInfoResponse>({
|
return sendRequest<GetInfoResponse>({
|
||||||
path: epmRouteService.getInfoPath(pkgkey),
|
path: epmRouteService.getInfoPath(pkgkey),
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
GetFleetStatusResponse,
|
GetFleetStatusResponse,
|
||||||
GetInfoResponse,
|
GetInfoResponse,
|
||||||
GetPackagePoliciesResponse,
|
GetPackagePoliciesResponse,
|
||||||
|
GetStatsResponse,
|
||||||
} from '../../../../../../../common/types/rest_spec';
|
} from '../../../../../../../common/types/rest_spec';
|
||||||
import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models';
|
import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models';
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +30,7 @@ describe('when on integration detail', () => {
|
||||||
const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey });
|
const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey });
|
||||||
let testRenderer: TestRenderer;
|
let testRenderer: TestRenderer;
|
||||||
let renderResult: ReturnType<typeof testRenderer.render>;
|
let renderResult: ReturnType<typeof testRenderer.render>;
|
||||||
let mockedApi: MockedApi;
|
let mockedApi: MockedApi<EpmPackageDetailsResponseProvidersMock>;
|
||||||
const render = () =>
|
const render = () =>
|
||||||
(renderResult = testRenderer.render(
|
(renderResult = testRenderer.render(
|
||||||
<Route path={PAGE_ROUTING_PATHS.integration_details}>
|
<Route path={PAGE_ROUTING_PATHS.integration_details}>
|
||||||
|
@ -48,6 +49,39 @@ describe('when on integration detail', () => {
|
||||||
window.location.hash = '#/';
|
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', () => {
|
describe('and a custom UI extension is NOT registered', () => {
|
||||||
beforeEach(() => render());
|
beforeEach(() => render());
|
||||||
|
|
||||||
|
@ -190,12 +224,27 @@ describe('when on integration detail', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MockedApi {
|
interface MockedApi<
|
||||||
|
R extends Record<string, jest.MockedFunction<any>> = Record<string, jest.MockedFunction<any>>
|
||||||
|
> {
|
||||||
/** Will return a promise that resolves when triggered APIs are complete */
|
/** Will return a promise that resolves when triggered APIs are complete */
|
||||||
waitForApi: () => Promise<void>;
|
waitForApi: () => Promise<void>;
|
||||||
|
/** 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<EpmPackageDetailsResponseProvidersMock> => {
|
||||||
let inflightApiCalls = 0;
|
let inflightApiCalls = 0;
|
||||||
const apiDoneListeners: Array<() => void> = [];
|
const apiDoneListeners: Array<() => void> = [];
|
||||||
const markApiCallAsHandled = async () => {
|
const markApiCallAsHandled = async () => {
|
||||||
|
@ -663,41 +712,13 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
http.get.mockImplementation(async (path) => {
|
const epmGetStatsResponse: GetStatsResponse = {
|
||||||
if (typeof path === 'string') {
|
response: {
|
||||||
if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) {
|
agent_policy_count: 2,
|
||||||
markApiCallAsHandled();
|
},
|
||||||
return epmPackageResponse;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) {
|
const mockedApiInterface: MockedApi<EpmPackageDetailsResponseProvidersMock> = {
|
||||||
markApiCallAsHandled();
|
|
||||||
return packageReadMe;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path === fleetSetupRouteService.getFleetSetupPath()) {
|
|
||||||
markApiCallAsHandled();
|
|
||||||
return agentsSetupResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path === packagePolicyRouteService.getListPath()) {
|
|
||||||
markApiCallAsHandled();
|
|
||||||
return packagePoliciesResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path === agentPolicyRouteService.getListPath()) {
|
|
||||||
markApiCallAsHandled();
|
|
||||||
return agentPoliciesResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const err = new Error(`API [GET ${path}] is not MOCKED!`);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
waitForApi() {
|
waitForApi() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (inflightApiCalls > 0) {
|
if (inflightApiCalls > 0) {
|
||||||
|
@ -707,5 +728,54 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
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 mockedApiInterface.responseProvider.epmGetInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) {
|
||||||
|
markApiCallAsHandled();
|
||||||
|
return mockedApiInterface.responseProvider.epmGetFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === fleetSetupRouteService.getFleetSetupPath()) {
|
||||||
|
markApiCallAsHandled();
|
||||||
|
return mockedApiInterface.responseProvider.fleetSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === packagePolicyRouteService.getListPath()) {
|
||||||
|
markApiCallAsHandled();
|
||||||
|
return mockedApiInterface.responseProvider.packagePolicyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === agentPolicyRouteService.getListPath()) {
|
||||||
|
markApiCallAsHandled();
|
||||||
|
return mockedApiInterface.responseProvider.agentPolicyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === epmRouteService.getStatsPath('nginx')) {
|
||||||
|
markApiCallAsHandled();
|
||||||
|
return mockedApiInterface.responseProvider.epmGetStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = new Error(`API [GET ${path}] is not MOCKED!`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mockedApiInterface;
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,6 +44,7 @@ import './index.scss';
|
||||||
import { useUIExtension } from '../../../../hooks/use_ui_extension';
|
import { useUIExtension } from '../../../../hooks/use_ui_extension';
|
||||||
import { PLUGIN_ID } from '../../../../../../../common/constants';
|
import { PLUGIN_ID } from '../../../../../../../common/constants';
|
||||||
import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info';
|
import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info';
|
||||||
|
import { IntegrationAgentPolicyCount } from './integration_agent_policy_count';
|
||||||
|
|
||||||
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
|
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
|
||||||
|
|
||||||
|
@ -239,6 +240,18 @@ export function Detail() {
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
...(packageInstallStatus === 'installed'
|
||||||
|
? [
|
||||||
|
{ isDivider: true },
|
||||||
|
{
|
||||||
|
label: i18n.translate('xpack.fleet.epm.usedByLabel', {
|
||||||
|
defaultMessage: 'Agent Policies',
|
||||||
|
}),
|
||||||
|
'data-test-subj': 'agentPolicyCount',
|
||||||
|
content: <IntegrationAgentPolicyCount packageName={packageInfo.name} />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ isDivider: true },
|
{ isDivider: true },
|
||||||
{
|
{
|
||||||
content: (
|
content: (
|
||||||
|
@ -264,7 +277,7 @@ export function Detail() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
].map((item, index) => (
|
].map((item, index) => (
|
||||||
<EuiFlexItem grow={false} key={index}>
|
<EuiFlexItem grow={false} key={index} data-test-subj={item['data-test-subj']}>
|
||||||
{item.isDivider ?? false ? (
|
{item.isDivider ?? false ? (
|
||||||
<Divider />
|
<Divider />
|
||||||
) : item.label ? (
|
) : item.label ? (
|
||||||
|
@ -285,6 +298,7 @@ export function Detail() {
|
||||||
handleAddIntegrationPolicyClick,
|
handleAddIntegrationPolicyClick,
|
||||||
hasWriteCapabilites,
|
hasWriteCapabilites,
|
||||||
packageInfo,
|
packageInfo,
|
||||||
|
packageInstallStatus,
|
||||||
pkgkey,
|
pkgkey,
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
]
|
]
|
||||||
|
|
|
@ -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}</>;
|
||||||
|
});
|
|
@ -17,6 +17,7 @@ import {
|
||||||
BulkInstallPackageInfo,
|
BulkInstallPackageInfo,
|
||||||
BulkInstallPackagesResponse,
|
BulkInstallPackagesResponse,
|
||||||
IBulkInstallPackageHTTPError,
|
IBulkInstallPackageHTTPError,
|
||||||
|
GetStatsResponse,
|
||||||
} from '../../../common';
|
} from '../../../common';
|
||||||
import {
|
import {
|
||||||
GetCategoriesRequestSchema,
|
GetCategoriesRequestSchema,
|
||||||
|
@ -27,6 +28,7 @@ import {
|
||||||
InstallPackageByUploadRequestSchema,
|
InstallPackageByUploadRequestSchema,
|
||||||
DeletePackageRequestSchema,
|
DeletePackageRequestSchema,
|
||||||
BulkUpgradePackagesFromRegistryRequestSchema,
|
BulkUpgradePackagesFromRegistryRequestSchema,
|
||||||
|
GetStatsRequestSchema,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
BulkInstallResponse,
|
BulkInstallResponse,
|
||||||
|
@ -48,6 +50,7 @@ import { splitPkgKey } from '../../services/epm/registry';
|
||||||
import { licenseService } from '../../services';
|
import { licenseService } from '../../services';
|
||||||
import { getArchiveEntry } from '../../services/epm/archive/cache';
|
import { getArchiveEntry } from '../../services/epm/archive/cache';
|
||||||
import { getAsset } from '../../services/epm/archive/storage';
|
import { getAsset } from '../../services/epm/archive/storage';
|
||||||
|
import { getPackageUsageStats } from '../../services/epm/packages/get';
|
||||||
|
|
||||||
export const getCategoriesHandler: RequestHandler<
|
export const getCategoriesHandler: RequestHandler<
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -196,6 +199,23 @@ export const getInfoHandler: RequestHandler<TypeOf<typeof GetInfoRequestSchema.p
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStatsHandler: RequestHandler<TypeOf<typeof GetStatsRequestSchema.params>> = 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<
|
export const installPackageFromRegistryHandler: RequestHandler<
|
||||||
TypeOf<typeof InstallPackageFromRegistryRequestSchema.params>,
|
TypeOf<typeof InstallPackageFromRegistryRequestSchema.params>,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
installPackageByUploadHandler,
|
installPackageByUploadHandler,
|
||||||
deletePackageHandler,
|
deletePackageHandler,
|
||||||
bulkInstallPackagesFromRegistryHandler,
|
bulkInstallPackagesFromRegistryHandler,
|
||||||
|
getStatsHandler,
|
||||||
} from './handlers';
|
} from './handlers';
|
||||||
import {
|
import {
|
||||||
GetCategoriesRequestSchema,
|
GetCategoriesRequestSchema,
|
||||||
|
@ -25,6 +26,7 @@ import {
|
||||||
InstallPackageByUploadRequestSchema,
|
InstallPackageByUploadRequestSchema,
|
||||||
DeletePackageRequestSchema,
|
DeletePackageRequestSchema,
|
||||||
BulkUpgradePackagesFromRegistryRequestSchema,
|
BulkUpgradePackagesFromRegistryRequestSchema,
|
||||||
|
GetStatsRequestSchema,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
|
const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
|
||||||
|
@ -57,6 +59,15 @@ export const registerRoutes = (router: IRouter) => {
|
||||||
getLimitedListHandler
|
getLimitedListHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
{
|
||||||
|
path: EPM_API_ROUTES.STATS_PATTERN,
|
||||||
|
validate: GetStatsRequestSchema,
|
||||||
|
options: { tags: [`access:${PLUGIN_ID}`] },
|
||||||
|
},
|
||||||
|
getStatsHandler
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
path: EPM_API_ROUTES.FILEPATH_PATTERN,
|
path: EPM_API_ROUTES.FILEPATH_PATTERN,
|
||||||
|
|
171
x-pack/plugins/fleet/server/services/epm/packages/get.test.ts
Normal file
171
x-pack/plugins/fleet/server/services/epm/packages/get.test.ts
Normal file
|
@ -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<SavedObjectsClientContract>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
soClient = savedObjectsClientMock.create();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and invoking getPackageUsageStats()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const savedObjects: Array<SavedObjectsFindResult<PackagePolicySOAttributes>> = [
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,7 +5,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server';
|
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 { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
|
||||||
import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types';
|
import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types';
|
||||||
import { Installation, PackageInfo, KibanaAssetType } from '../../../types';
|
import { Installation, PackageInfo, KibanaAssetType } from '../../../types';
|
||||||
|
@ -13,6 +19,7 @@ import * as Registry from '../registry';
|
||||||
import { createInstallableFrom, isRequiredPackage } from './index';
|
import { createInstallableFrom, isRequiredPackage } from './index';
|
||||||
import { getEsPackage } from '../archive/storage';
|
import { getEsPackage } from '../archive/storage';
|
||||||
import { getArchivePackage } from '../archive';
|
import { getArchivePackage } from '../archive';
|
||||||
|
import { normalizeKuery } from '../../saved_object';
|
||||||
|
|
||||||
export { getFile, SearchParams } from '../registry';
|
export { getFile, SearchParams } from '../registry';
|
||||||
|
|
||||||
|
@ -116,6 +123,43 @@ export async function getPackageInfo(options: {
|
||||||
return createInstallableFrom(updated, savedObject);
|
return createInstallableFrom(updated, savedObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPackageUsageStats = async ({
|
||||||
|
savedObjectsClient,
|
||||||
|
pkgName,
|
||||||
|
}: {
|
||||||
|
savedObjectsClient: SavedObjectsClientContract;
|
||||||
|
pkgName: string;
|
||||||
|
}): Promise<PackageUsageStats> => {
|
||||||
|
const filter = normalizeKuery(
|
||||||
|
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||||
|
`${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${pkgName}`
|
||||||
|
);
|
||||||
|
const agentPolicyCount = new Set<string>();
|
||||||
|
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<PackagePolicySOAttributes>({
|
||||||
|
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 {
|
interface PackageResponse {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
packageInfo: ArchivePackage | RegistryPackage;
|
packageInfo: ArchivePackage | RegistryPackage;
|
||||||
|
|
|
@ -32,6 +32,12 @@ export const GetInfoRequestSchema = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GetStatsRequestSchema = {
|
||||||
|
params: schema.object({
|
||||||
|
pkgName: schema.string(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
export const InstallPackageFromRegistryRequestSchema = {
|
export const InstallPackageFromRegistryRequestSchema = {
|
||||||
params: schema.object({
|
params: schema.object({
|
||||||
pkgkey: schema.string(),
|
pkgkey: schema.string(),
|
||||||
|
|
Loading…
Reference in a new issue