From 8352b86f59522319f6d20ae2165d11b621f1f22b Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Molins Date: Tue, 19 Nov 2024 15:27:19 +0100 Subject: [PATCH] [Fleet] Filter integrations/packages list shown depending on the `policy_templates_behavior` field (#200605) ## Summary Closes https://github.com/elastic/kibana/issues/198145 Add support to filter the tiles shown in the integrations UI as well as the packages shown in the global search provider depending on the `policy_templates_behaviour` field introduced in https://github.com/elastic/package-spec/issues/802. If this new field is not present in the package manifest, the same behavior is kept. Therefore, it is shown a tile for the package itself plus a tile for each policy template defined in the manifest. Tested using a custom Elastic Package Registry with some packages defining this new `policy_templates_behavior` via the key: ```yaml xpack.fleet.registryUrl: http://localhost:8080 ``` ### Screenshots Checked option "Elastic Agent only" in the integrations UI to avoid tutorials based on Beats. Example with `azure_metrics` package in the Integrations UI: - `policy_templates_behavior: all` ![All policy templates](https://github.com/user-attachments/assets/907618e3-f2db-44df-b1ac-3965b1978b2c) - `policy_templates_behavior: combined_policy` ![Just combined policy](https://github.com/user-attachments/assets/77293616-8125-4d01-81f3-b3f17135ca49) - `policy_templates_behavior: individual_policies` ![Just individual policy templates](https://github.com/user-attachments/assets/b68ad474-8aac-464b-9946-9ae6104dd2ae) Example in the Global Search with `azure_metrics` package and `combined policy` behavior: ![global search with azure metrics package](https://github.com/user-attachments/assets/e70315b7-d303-4b32-aa9e-8e1e9b056239) ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] 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) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_node:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] --- x-pack/plugins/fleet/common/services/index.ts | 1 + .../common/services/policy_template.test.ts | 124 +++++++++++ .../fleet/common/services/policy_template.ts | 25 +++ .../plugins/fleet/common/types/models/epm.ts | 1 + .../fleet/common/types/models/package_spec.ts | 1 + .../home/hooks/use_available_packages.tsx | 50 +++-- .../fleet/public/search_provider.test.ts | 200 ++++++++++++++++++ .../plugins/fleet/public/search_provider.ts | 9 +- 8 files changed, 385 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 4443878617796..7061d6d3028d8 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -33,6 +33,7 @@ export { isIntegrationPolicyTemplate, getNormalizedInputs, getNormalizedDataStreams, + filterPolicyTemplatesTiles, } from './policy_template'; export { doesPackageHaveIntegrations } from './packages_with_integrations'; export type { diff --git a/x-pack/plugins/fleet/common/services/policy_template.test.ts b/x-pack/plugins/fleet/common/services/policy_template.test.ts index 87ce2121b8b59..b0cba311fe70c 100644 --- a/x-pack/plugins/fleet/common/services/policy_template.test.ts +++ b/x-pack/plugins/fleet/common/services/policy_template.test.ts @@ -10,6 +10,7 @@ import type { RegistryPolicyIntegrationTemplate, PackageInfo, RegistryVarType, + PackageListItem, } from '../types'; import { @@ -17,6 +18,7 @@ import { isIntegrationPolicyTemplate, getNormalizedInputs, getNormalizedDataStreams, + filterPolicyTemplatesTiles, } from './policy_template'; describe('isInputOnlyPolicyTemplate', () => { @@ -280,3 +282,125 @@ describe('getNormalizedDataStreams', () => { expect(result?.[0].streams?.[0]?.vars).toEqual([datasetVar]); }); }); + +describe('filterPolicyTemplatesTiles', () => { + const topPackagePolicy: PackageListItem = { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }; + + const childPolicyTemplates: PackageListItem[] = [ + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + ]; + it('should return all tiles as undefined value', () => { + expect(filterPolicyTemplatesTiles(undefined, topPackagePolicy, childPolicyTemplates)).toEqual([ + { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + ]); + }); + it('should return all tiles', () => { + expect(filterPolicyTemplatesTiles('all', topPackagePolicy, childPolicyTemplates)).toEqual([ + { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + ]); + }); + it('should return just the combined policy tile', () => { + expect( + filterPolicyTemplatesTiles('combined_policy', topPackagePolicy, childPolicyTemplates) + ).toEqual([ + { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + ]); + }); + it('should return just the individual policies (tiles)', () => { + expect( + filterPolicyTemplatesTiles('individual_policies', topPackagePolicy, childPolicyTemplates) + ).toEqual([ + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + }, + ]); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/policy_template.ts b/x-pack/plugins/fleet/common/services/policy_template.ts index ed390e0c6b45d..efa65a880576a 100644 --- a/x-pack/plugins/fleet/common/services/policy_template.ts +++ b/x-pack/plugins/fleet/common/services/policy_template.ts @@ -39,6 +39,7 @@ export function packageHasNoPolicyTemplates(packageInfo: PackageInfo): boolean { ) ); } + export function isInputOnlyPolicyTemplate( policyTemplate: RegistryPolicyTemplate ): policyTemplate is RegistryPolicyInputOnlyTemplate { @@ -142,3 +143,27 @@ const createDefaultDatasetName = ( packageInfo: { name: string }, policyTemplate: { name: string } ): string => packageInfo.name + '.' + policyTemplate.name; + +export function filterPolicyTemplatesTiles( + templatesBehavior: string | undefined, + packagePolicy: T, + packagePolicyTemplates: T[] +): T[] { + switch (templatesBehavior || 'all') { + case 'combined_policy': + return [packagePolicy]; + case 'individual_policies': + return [ + ...(packagePolicyTemplates && packagePolicyTemplates.length > 1 + ? packagePolicyTemplates + : []), + ]; + default: + return [ + packagePolicy, + ...(packagePolicyTemplates && packagePolicyTemplates.length > 1 + ? packagePolicyTemplates + : []), + ]; + } +} diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index f1cd9e5ee4a7f..36a83c1c0a09e 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -311,6 +311,7 @@ export type RegistrySearchResult = Pick< | 'icons' | 'internal' | 'data_streams' + | 'policy_templates_behavior' | 'policy_templates' | 'categories' >; diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts index 18c10e4617417..6ae8ba984f5f6 100644 --- a/x-pack/plugins/fleet/common/types/models/package_spec.ts +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -24,6 +24,7 @@ export interface PackageSpecManifest { conditions?: PackageSpecConditions; icons?: PackageSpecIcon[]; screenshots?: PackageSpecScreenshot[]; + policy_templates_behavior?: 'all' | 'combined_policy' | 'individual_policies'; policy_templates?: RegistryPolicyTemplate[]; vars?: RegistryVarsEntry[]; owner: { github?: string; type?: 'elastic' | 'partner' | 'community' }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx index 2f506b30b2626..c399a0241c22a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx @@ -28,6 +28,7 @@ import { doesPackageHaveIntegrations, ExperimentalFeaturesService } from '../../ import { isInputOnlyPolicyTemplate, isIntegrationPolicyTemplate, + filterPolicyTemplatesTiles, } from '../../../../../../../../common/services'; import { @@ -83,30 +84,33 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { categories: getAllCategoriesFromIntegrations(pkg), }; - return [ - ...acc, + const integrationsPolicyTemplates = doesPackageHaveIntegrations(pkg) + ? policyTemplates.map((policyTemplate) => { + const { name, title, description, icons } = policyTemplate; + + const categories = + isIntegrationPolicyTemplate(policyTemplate) && policyTemplate.categories + ? policyTemplate.categories + : []; + const allCategories = [...topCategories, ...categories]; + return { + ...restOfPackage, + id: `${restOfPackage.id}-${name}`, + integration: name, + title, + description, + icons: icons || restOfPackage.icons, + categories: uniq(allCategories), + }; + }) + : []; + + const tiles = filterPolicyTemplatesTiles( + pkg.policy_templates_behavior, topPackage, - ...(doesPackageHaveIntegrations(pkg) - ? policyTemplates.map((policyTemplate) => { - const { name, title, description, icons } = policyTemplate; - - const categories = - isIntegrationPolicyTemplate(policyTemplate) && policyTemplate.categories - ? policyTemplate.categories - : []; - const allCategories = [...topCategories, ...categories]; - return { - ...restOfPackage, - id: `${restOfPackage.id}-${name}`, - integration: name, - title, - description, - icons: icons || restOfPackage.icons, - categories: uniq(allCategories), - }; - }) - : []), - ]; + integrationsPolicyTemplates + ); + return [...acc, ...tiles]; }, []); }; diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index 68ba3042a8e76..4ff04862c756f 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -112,6 +112,123 @@ const testResponse: GetPackagesResponse['items'] = [ }, ]; +const testResponseBehaviorField: GetPackagesResponse['items'] = [ + { + description: 'testWithPolicyTemplateBehaviorAll', + download: 'testWithPolicyTemplateBehaviorAll', + id: 'testWithPolicyTemplateBehaviorAll', + name: 'testWithPolicyTemplateBehaviorAll', + path: 'testWithPolicyTemplateBehaviorAll', + release: 'ga', + status: 'not_installed', + title: 'testWithPolicyTemplateBehaviorAll', + version: 'testWithPolicyTemplateBehaviorAll', + policy_templates_behavior: 'all', + policy_templates: [ + { + description: 'testPolicyTemplate1BehaviorAll', + name: 'testPolicyTemplate1BehaviorAll', + icons: [ + { + src: 'testPolicyTemplate1BehaviorAll', + path: 'testPolicyTemplate1BehaviorAll', + }, + ], + title: 'testPolicyTemplate1BehaviorAll', + type: 'testPolicyTemplate1BehaviorAll', + }, + { + description: 'testPolicyTemplate2BehaviorAll', + name: 'testPolicyTemplate2BehaviorAll', + icons: [ + { + src: 'testPolicyTemplate2BehaviorAll', + path: 'testPolicyTemplate2BehaviorAll', + }, + ], + title: 'testPolicyTemplate2BehaviorAll', + type: 'testPolicyTemplate2BehaviorAll', + }, + ], + }, + { + description: 'testWithPolicyTemplateBehaviorCombined', + download: 'testWithPolicyTemplateBehaviorCombined', + id: 'testWithPolicyTemplateBehaviorCombined', + name: 'testWithPolicyTemplateBehaviorCombined', + path: 'testWithPolicyTemplateBehaviorCombined', + release: 'ga', + status: 'not_installed', + title: 'testWithPolicyTemplateBehaviorCombined', + version: 'testWithPolicyTemplateBehaviorCombined', + policy_templates_behavior: 'combined_policy', + policy_templates: [ + { + description: 'testPolicyTemplate1BehaviorCombined', + name: 'testPolicyTemplate1BehaviorCombined', + icons: [ + { + src: 'testPolicyTemplate1BehaviorCombined', + path: 'testPolicyTemplate1BehaviorCombined', + }, + ], + title: 'testPolicyTemplate1BehaviorCombined', + type: 'testPolicyTemplate1BehaviorCombined', + }, + { + description: 'testPolicyTemplate2BehaviorCombined', + name: 'testPolicyTemplate2BehaviorCombined', + icons: [ + { + src: 'testPolicyTemplate2BehaviorCombined', + path: 'testPolicyTemplate2BehaviorCombined', + }, + ], + title: 'testPolicyTemplate2BehaviorCombined', + type: 'testPolicyTemplate2BehaviorCombined', + }, + ], + }, + { + description: 'testWithPolicyTemplateBehaviorIndividual', + download: 'testWithPolicyTemplateBehaviorIndividual', + id: 'testWithPolicyTemplateBehaviorIndividual', + name: 'testWithPolicyTemplateBehaviorIndividual', + path: 'testWithPolicyTemplateBehaviorIndividual', + release: 'ga', + status: 'not_installed', + title: 'testWithPolicyTemplateBehaviorIndividual', + version: 'testWithPolicyTemplateBehaviorIndividual', + policy_templates_behavior: 'individual_policies', + policy_templates: [ + { + description: 'testPolicyTemplate1BehaviorIndividual', + name: 'testPolicyTemplate1BehaviorIndividual', + icons: [ + { + src: 'testPolicyTemplate1BehaviorIndividual', + path: 'testPolicyTemplate1BehaviorIndividual', + }, + ], + title: 'testPolicyTemplate1BehaviorIndividual', + type: 'testPolicyTemplate1BehaviorIndividual', + }, + { + description: 'testPolicyTemplate2BehaviorIndividual', + name: 'testPolicyTemplate2BehaviorIndividual', + icons: [ + { + src: 'testPolicyTemplate2BehaviorIndividual', + path: 'testPolicyTemplate2BehaviorIndividual', + }, + ], + title: 'testPolicyTemplate2BehaviorIndividual', + type: 'testPolicyTemplate2BehaviorIndividual', + }, + ], + }, +]; + const getTestScheduler = () => { return new TestScheduler((actual, expected) => { return expect(actual).toEqual(expected); @@ -394,6 +511,89 @@ describe('Package search provider', () => { expect(sendGetPackages).toHaveBeenCalledTimes(1); }); + test('with integration tag, with policy_templates_behavior field', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + mockSendGetPackages.mockReturnValue( + hot('--(a|)', { a: { data: { items: testResponseBehaviorField } } }) + ); + setupMock.getStartServices.mockReturnValue( + hot('--(a|)', { a: [coreMock.createStart()] }) as any + ); + const packageSearchProvider = createPackageSearchProvider(setupMock); + expectObservable( + packageSearchProvider.find( + { types: ['integration'] }, + { aborted$: NEVER, maxResults: 100, preference: '' } + ) + ).toBe('--(a|)', { + a: [ + { + id: 'testWithPolicyTemplateBehaviorAll', + score: 80, + title: 'testWithPolicyTemplateBehaviorAll', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorAll/overview', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate1BehaviorAll', + score: 80, + title: 'testPolicyTemplate1BehaviorAll', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorAll/overview?integration=testPolicyTemplate1BehaviorAll', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate2BehaviorAll', + score: 80, + title: 'testPolicyTemplate2BehaviorAll', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorAll/overview?integration=testPolicyTemplate2BehaviorAll', + prependBasePath: false, + }, + }, + { + id: 'testWithPolicyTemplateBehaviorCombined', + score: 80, + title: 'testWithPolicyTemplateBehaviorCombined', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorCombined/overview', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate1BehaviorIndividual', + score: 80, + title: 'testPolicyTemplate1BehaviorIndividual', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorIndividual/overview?integration=testPolicyTemplate1BehaviorIndividual', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate2BehaviorIndividual', + score: 80, + title: 'testPolicyTemplate2BehaviorIndividual', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorIndividual/overview?integration=testPolicyTemplate2BehaviorIndividual', + prependBasePath: false, + }, + }, + ], + }); + }); + + expect(sendGetPackages).toHaveBeenCalledTimes(1); + }); + test('with integration tag, with search term', () => { getTestScheduler().run(({ hot, expectObservable }) => { mockSendGetPackages.mockReturnValue( diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index a6810633c428e..c329443288e4b 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -16,6 +16,7 @@ import type { } from '@kbn/global-search-plugin/public'; import { INTEGRATIONS_PLUGIN_ID } from '../common'; +import { filterPolicyTemplatesTiles } from '../common/services'; import { sendGetPackages } from './hooks'; import type { GetPackagesResponse, PackageListItem } from './types'; @@ -74,10 +75,12 @@ export const toSearchResult = ( }) ); - return [ + const tiles = filterPolicyTemplatesTiles( + pkg.policy_templates_behavior, packageResult, - ...(policyTemplateResults && policyTemplateResults.length > 1 ? policyTemplateResults : []), - ]; + policyTemplateResults || [] + ); + return [...tiles]; }; export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => {