From 4dc3f94de0444e68f1ea623c4949c5be44f12838 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 13 Dec 2022 20:52:07 -0500 Subject: [PATCH] [Fleet] Fix datastream settings on package upgrade and package policy creation (#147368) --- .../package_to_package_policy.test.ts | 44 ++++++++++++++++ .../services/package_to_package_policy.ts | 8 +++ .../experimental_datastream_settings.test.tsx | 51 +++++++++++++++++++ .../experimental_datastream_settings.tsx | 4 +- .../elasticsearch/template/install.test.ts | 40 +++++++++++++++ .../epm/elasticsearch/template/install.ts | 31 ++++++++--- .../services/epm/packages/_install_package.ts | 10 +++- .../fleet/server/services/package_policy.ts | 12 +++++ x-pack/plugins/fleet/server/types/index.tsx | 1 + 9 files changed, 190 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index f3e1121ab766f..c9f5201ebff85 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -360,6 +360,50 @@ describe('Fleet - packageToPackagePolicy', () => { }); }); + it('returns package policy with experimental datastream features', () => { + expect( + packageToPackagePolicy( + { + ...mockPackage, + savedObject: { + attributes: { + experimental_data_stream_features: [ + { + data_stream: 'metrics-test.testdataset', + features: { + synthetic_source: true, + tsdb: true, + }, + }, + ], + }, + } as any, + }, + '1' + ) + ).toEqual({ + policy_id: '1', + namespace: '', + enabled: true, + inputs: [], + name: 'mock-package-1', + package: { + name: 'mock-package', + title: 'Mock package', + version: '0.0.0', + experimental_data_stream_features: [ + { + data_stream: 'metrics-test.testdataset', + features: { + synthetic_source: true, + tsdb: true, + }, + }, + ], + }, + }); + }); + it('returns package policy with custom name', () => { expect(packageToPackagePolicy(mockPackage, '1', 'default', 'pkgPolicy-1')).toEqual({ policy_id: '1', diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts index 50dd519bc00cd..52d55f384e96e 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts @@ -205,6 +205,11 @@ export const packageToPackagePolicy = ( description?: string, integrationToEnable?: string ): NewPackagePolicy => { + const experimentalDataStreamFeatures = + 'savedObject' in packageInfo + ? packageInfo.savedObject?.attributes?.experimental_data_stream_features + : undefined; + const packagePolicy: NewPackagePolicy = { name: packagePolicyName || `${packageInfo.name}-1`, namespace, @@ -213,6 +218,9 @@ export const packageToPackagePolicy = ( name: packageInfo.name, title: packageInfo.title, version: packageInfo.version, + ...(experimentalDataStreamFeatures + ? { experimental_data_stream_features: experimentalDataStreamFeatures } + : undefined), }, enabled: true, policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.test.tsx index e552288d673d8..1bd0e7bb39919 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { act, fireEvent } from '@testing-library/react'; import { createFleetTestRendererMock } from '../../../../../../../../mock'; @@ -131,5 +132,55 @@ describe('ExperimentDatastreamSettings', () => { expect(syntheticSourceSwitch).not.toBeEnabled(); expect(mockSetNewExperimentalDataFeatures).not.toBeCalled(); }); + + it('should not mutate original experimental feature if a user change one', () => { + const mockSetNewExperimentalDataFeatures = jest.fn(); + const experimentalDataFeatures = [ + { + data_stream: 'logs-test', + features: { + synthetic_source: true, + tsdb: false, + }, + }, + ]; + const res = createFleetTestRendererMock().render( + + ); + + act(() => { + fireEvent.click( + res.getByTestId('packagePolicyEditor.syntheticSourceExperimentalFeature.switch') + ); + }); + expect(mockSetNewExperimentalDataFeatures).toBeCalled(); + expect(mockSetNewExperimentalDataFeatures.mock.calls[0][0]).toEqual([ + { + data_stream: 'logs-test', + features: { + synthetic_source: false, + tsdb: false, + }, + }, + ]); + expect(experimentalDataFeatures).toEqual([ + { + data_stream: 'logs-test', + features: { + synthetic_source: true, + tsdb: false, + }, + }, + ]); + }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.tsx index c91e5bf767666..8910805f52df5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/experimental_datastream_settings.tsx @@ -73,14 +73,14 @@ export const ExperimentDatastreamSettings: React.FunctionComponent = ({ const onIndexingSettingChange = ( features: Partial> ) => { - const newExperimentalDataStreamFeatures = [...(experimentalDataFeatures ?? [])]; + const newExperimentalDataStreamFeatures = + experimentalDataFeatures?.map((feature) => ({ ...feature })) ?? []; const dataStream = getRegistryDataStreamAssetBaseName(registryDataStream); const existingSettingRecord = newExperimentalDataStreamFeatures.find( (x) => x.data_stream === dataStream ); - if (existingSettingRecord) { existingSettingRecord.features = { ...existingSettingRecord.features, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index d8cc0e93c9ea0..4b62a2f4a60fb 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -122,4 +122,44 @@ describe('EPM index template install', () => { expect(packageTemplate.mappings).toHaveProperty('_source'); expect(packageTemplate.mappings._source).toEqual({ mode: 'synthetic' }); }); + + it('tests prepareTemplate to not set source mode to synthetics if specified but user disabled it', async () => { + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + elasticsearch: { + source_mode: 'synthetic', + }, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + + const { componentTemplates } = prepareTemplate({ + pkg, + dataStream: dataStreamDatasetIsPrefixTrue, + experimentalDataStreamFeature: { + data_stream: 'metrics-package.dataset', + features: { + synthetic_source: false, + tsdb: false, + }, + }, + }); + + const packageTemplate = componentTemplates['metrics-package.dataset@package'].template; + + if (!('mappings' in packageTemplate)) { + throw new Error('no mappings on package template'); + } + + expect(packageTemplate.mappings).not.toHaveProperty('_source'); + }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 5d30433e80d4b..df744c7ae532e 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -23,6 +23,7 @@ import { getPipelineNameForDatastream, getFileDataIndexName, getFileMetadataIndexName, + getRegistryDataStreamAssetBaseName, } from '../../../../../common/services'; import type { RegistryDataStream, @@ -35,6 +36,7 @@ import type { TemplateMap, EsAssetReference, PackageInfo, + ExperimentalDataStreamFeature, } from '../../../../types'; import { loadFieldsFromYaml, processFields } from '../../fields/field'; import { getAsset, getPathParts } from '../../archive'; @@ -61,7 +63,8 @@ const FLEET_COMPONENT_TEMPLATE_NAMES = FLEET_COMPONENT_TEMPLATES.map((tmpl) => t export const prepareToInstallTemplates = ( installablePackage: InstallablePackage, paths: string[], - esReferences: EsAssetReference[] + esReferences: EsAssetReference[], + experimentalDataStreamFeatures: ExperimentalDataStreamFeature[] = [] ): { assetsToAdd: EsAssetReference[]; assetsToRemove: EsAssetReference[]; @@ -78,9 +81,15 @@ export const prepareToInstallTemplates = ( const dataStreams = installablePackage.data_streams; if (!dataStreams) return { assetsToAdd: [], assetsToRemove, install: () => Promise.resolve([]) }; - const templates = dataStreams.map((dataStream) => - prepareTemplate({ pkg: installablePackage, dataStream }) - ); + const templates = dataStreams.map((dataStream) => { + const experimentalDataStreamFeature = experimentalDataStreamFeatures.find( + (datastreamFeature) => + datastreamFeature.data_stream === getRegistryDataStreamAssetBaseName(dataStream) + ); + + return prepareTemplate({ pkg: installablePackage, dataStream, experimentalDataStreamFeature }); + }); + const assetsToAdd = getAllTemplateRefs(templates.map((template) => template.indexTemplate)); return { @@ -243,6 +252,7 @@ export function buildComponentTemplates(params: { packageName: string; pipelineName?: string; defaultSettings: IndexTemplate['template']['settings']; + experimentalDataStreamFeature?: ExperimentalDataStreamFeature; }) { const { templateName, @@ -271,6 +281,10 @@ export function buildComponentTemplates(params: { (dynampingTemplate) => Object.keys(dynampingTemplate)[0] ); + const sourceModeSynthetic = + params.experimentalDataStreamFeature?.features.synthetic_source !== false && + (params.experimentalDataStreamFeature?.features.synthetic_source === true || + registryElasticsearch?.source_mode === 'synthetic'); templatesMap[packageTemplateName] = { template: { settings: { @@ -291,13 +305,11 @@ export function buildComponentTemplates(params: { properties: mappingsProperties, dynamic_templates: mappingsDynamicTemplates.length ? mappingsDynamicTemplates : undefined, ...omit(indexTemplateMappings, 'properties', 'dynamic_templates', '_source'), - ...(indexTemplateMappings?._source || registryElasticsearch?.source_mode + ...(indexTemplateMappings?._source || sourceModeSynthetic ? { _source: { ...indexTemplateMappings?._source, - ...(registryElasticsearch?.source_mode === 'synthetic' - ? { mode: 'synthetic' } - : {}), + ...(sourceModeSynthetic ? { mode: 'synthetic' } : {}), }, } : {}), @@ -465,9 +477,11 @@ export async function ensureAliasHasWriteIndex(opts: { export function prepareTemplate({ pkg, dataStream, + experimentalDataStreamFeature, }: { pkg: Pick; dataStream: RegistryDataStream; + experimentalDataStreamFeature?: ExperimentalDataStreamFeature; }): { componentTemplates: TemplateMap; indexTemplate: IndexTemplateEntry } { const { name: packageName, version: packageVersion } = pkg; const fields = loadFieldsFromYaml(pkg, dataStream.path); @@ -494,6 +508,7 @@ export function prepareTemplate({ templateName, pipelineName, registryElasticsearch: dataStream.elasticsearch, + experimentalDataStreamFeature, }); const template = getTemplate({ diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 4b0aed34f8520..2109cb00599da 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -180,8 +180,16 @@ export async function _installPackage({ * This split of prepare/install could be extended to all asset types. Besides performance, it also allows us to * more easily write unit tests against the asset generation code without needing to mock ES responses. */ + const experimentalDataStreamFeatures = + installedPkg?.attributes?.experimental_data_stream_features ?? []; + const preparedIngestPipelines = prepareToInstallPipelines(packageInfo, paths); - const preparedIndexTemplates = prepareToInstallTemplates(packageInfo, paths, esReferences); + const preparedIndexTemplates = prepareToInstallTemplates( + packageInfo, + paths, + esReferences, + experimentalDataStreamFeatures + ); // Update the references for the templates and ingest pipelines together. Need to be done togther to avoid race // conditions on updating the installed_es field at the same time diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index e0213106eab77..7acd555380a68 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -211,6 +211,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { SAVED_OBJECT_TYPE, { ...packagePolicy, + ...(packagePolicy.package + ? { package: omit(packagePolicy.package, 'experimental_data_stream_features') } + : {}), inputs, elasticsearch, revision: 1, @@ -285,6 +288,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { id: packagePolicyId, attributes: { ...pkgPolicyWithoutId, + ...(packagePolicy.package + ? { package: omit(packagePolicy.package, 'experimental_data_stream_features') } + : {}), inputs, elasticsearch, policy_id: agentPolicyId, @@ -526,6 +532,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { id, { ...restOfPackagePolicy, + ...(restOfPackagePolicy.package + ? { package: omit(restOfPackagePolicy.package, 'experimental_data_stream_features') } + : {}), inputs, elasticsearch, revision: oldPackagePolicy.revision + 1, @@ -620,6 +629,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { id, attributes: { ...restOfPackagePolicy, + ...(restOfPackagePolicy.package + ? { package: omit(restOfPackagePolicy.package, 'experimental_data_stream_features') } + : {}), inputs, elasticsearch, revision: oldPackagePolicy.revision + 1, diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index b75bbf0d119ff..1913563c6fc5d 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -87,6 +87,7 @@ export type { PackageVerificationStatus, BulkInstallPackageInfo, PackageAssetReference, + ExperimentalDataStreamFeature, } from '../../common/types'; export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types'; export { dataTypes } from '../../common/constants';