From 2fee796ddce2cc66338ad7af6b78aa19a557c3df Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 9 Jan 2025 03:29:41 +1100 Subject: [PATCH] [8.x] Fix generation of dynamic mapping for object with specific subfield (#204104) (#204493) # Backport This will backport the following commits from `main` to `8.x`: - [Fix generation of dynamic mapping for object with specific subfield (#204104)](https://github.com/elastic/kibana/pull/204104) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Jaime Soriano Pastor","email":"jaime.soriano@elastic.co"},"sourceCommit":{"committedDate":"2024-12-16T20:58:30Z","message":"Fix generation of dynamic mapping for object with specific subfield (#204104)\n\nFix generation of dynamic mapping for objects that have more specific\r\nsubfields in separate definitions.\r\n\r\nThis can be reproduced for example with:\r\n```\r\n- name: labels\r\n type: object\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n- name: labels.count\r\n type: long\r\n```\r\n\r\nFleet expands and deduplicates field definitions before generating the\r\nmappings, so the definitions above are converted to something like the\r\nfollowing:\r\n```\r\n- name: labels\r\n type: group\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n fields:\r\n - name: count\r\n type: long\r\n```\r\n\r\nUsually fields of type `group` don't have an `object_type`, so this was\r\nbeing ignored, the dynamic mapping was not being generated.\r\n\r\nThis issue was not reproduced if the object field name includes a\r\nwildcard, like in `labels.*`, because then the expansion and\r\ndeduplication resolves to something like this:\r\n```\r\n- name: labels\r\n type: group\r\n fields:\r\n - name: '*'\r\n type: object\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n - name: count\r\n type: long\r\n```","sha":"e3877e053405bae71b5576648cb7c637c4a23f9a","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Fleet","v9.0.0","backport:prev-major"],"title":"Fix generation of dynamic mapping for object with specific subfield","number":204104,"url":"https://github.com/elastic/kibana/pull/204104","mergeCommit":{"message":"Fix generation of dynamic mapping for object with specific subfield (#204104)\n\nFix generation of dynamic mapping for objects that have more specific\r\nsubfields in separate definitions.\r\n\r\nThis can be reproduced for example with:\r\n```\r\n- name: labels\r\n type: object\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n- name: labels.count\r\n type: long\r\n```\r\n\r\nFleet expands and deduplicates field definitions before generating the\r\nmappings, so the definitions above are converted to something like the\r\nfollowing:\r\n```\r\n- name: labels\r\n type: group\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n fields:\r\n - name: count\r\n type: long\r\n```\r\n\r\nUsually fields of type `group` don't have an `object_type`, so this was\r\nbeing ignored, the dynamic mapping was not being generated.\r\n\r\nThis issue was not reproduced if the object field name includes a\r\nwildcard, like in `labels.*`, because then the expansion and\r\ndeduplication resolves to something like this:\r\n```\r\n- name: labels\r\n type: group\r\n fields:\r\n - name: '*'\r\n type: object\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n - name: count\r\n type: long\r\n```","sha":"e3877e053405bae71b5576648cb7c637c4a23f9a"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204104","number":204104,"mergeCommit":{"message":"Fix generation of dynamic mapping for object with specific subfield (#204104)\n\nFix generation of dynamic mapping for objects that have more specific\r\nsubfields in separate definitions.\r\n\r\nThis can be reproduced for example with:\r\n```\r\n- name: labels\r\n type: object\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n- name: labels.count\r\n type: long\r\n```\r\n\r\nFleet expands and deduplicates field definitions before generating the\r\nmappings, so the definitions above are converted to something like the\r\nfollowing:\r\n```\r\n- name: labels\r\n type: group\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n fields:\r\n - name: count\r\n type: long\r\n```\r\n\r\nUsually fields of type `group` don't have an `object_type`, so this was\r\nbeing ignored, the dynamic mapping was not being generated.\r\n\r\nThis issue was not reproduced if the object field name includes a\r\nwildcard, like in `labels.*`, because then the expansion and\r\ndeduplication resolves to something like this:\r\n```\r\n- name: labels\r\n type: group\r\n fields:\r\n - name: '*'\r\n type: object\r\n object_type: keyword\r\n object_type_mapping_type: '*'\r\n - name: count\r\n type: long\r\n```","sha":"e3877e053405bae71b5576648cb7c637c4a23f9a"}}]}] BACKPORT--> Co-authored-by: Jaime Soriano Pastor <jaime.soriano@elastic.co> --- .../elasticsearch/template/template.test.ts | 78 ++++++ .../epm/elasticsearch/template/template.ts | 242 +++++++++--------- 2 files changed, 203 insertions(+), 117 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.test.ts index 6b3d0c77ad74d..15f7e1f526085 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -786,6 +786,84 @@ describe('EPM template', () => { expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); }); + it('tests processing object field with more specific properties without wildcard', () => { + const objectFieldWithPropertyReversedLiteralYml = ` +- name: labels + type: object + object_type: keyword + object_type_mapping_type: '*' +- name: labels.count + type: long +`; + const objectFieldWithPropertyReversedMapping = { + dynamic_templates: [ + { + labels: { + path_match: 'labels.*', + match_mapping_type: '*', + mapping: { + type: 'keyword', + }, + }, + }, + ], + properties: { + labels: { + dynamic: true, + type: 'object', + properties: { + count: { + type: 'long', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); + }); + + it('tests processing object field with more specific properties with wildcard', () => { + const objectFieldWithPropertyReversedLiteralYml = ` +- name: labels.* + type: object + object_type: keyword + object_type_mapping_type: '*' +- name: labels.count + type: long +`; + const objectFieldWithPropertyReversedMapping = { + dynamic_templates: [ + { + 'labels.*': { + path_match: 'labels.*', + match_mapping_type: '*', + mapping: { + type: 'keyword', + }, + }, + }, + ], + properties: { + labels: { + dynamic: true, + type: 'object', + properties: { + count: { + type: 'long', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); + }); + it('tests processing object field with subobjects set to false (case B)', () => { const objectFieldWithPropertyReversedLiteralYml = ` - name: b.labels.* diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.ts index e48e6251a092a..89608d76ebf92 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/elasticsearch/template/template.ts @@ -299,6 +299,124 @@ function _generateMappings( } } + function addObjectAsDynamicMapping(field: Field) { + const path = ctx.groupFieldName ? `${ctx.groupFieldName}.${field.name}` : field.name; + const pathMatch = path.includes('*') ? path : `${path}.*`; + + let dynProperties: Properties = getDefaultProperties(field); + let matchingType: string | undefined; + switch (field.object_type) { + case 'histogram': + dynProperties = histogram(field); + matchingType = field.object_type_mapping_type ?? '*'; + break; + case 'ip': + case 'keyword': + case 'match_only_text': + case 'text': + case 'wildcard': + dynProperties.type = field.object_type; + matchingType = field.object_type_mapping_type ?? 'string'; + break; + case 'scaled_float': + dynProperties = scaledFloat(field); + matchingType = field.object_type_mapping_type ?? '*'; + break; + case 'aggregate_metric_double': + dynProperties.type = field.object_type; + dynProperties.metrics = field.metrics; + dynProperties.default_metric = field.default_metric; + matchingType = field.object_type_mapping_type ?? '*'; + break; + case 'double': + case 'float': + case 'half_float': + dynProperties.type = field.object_type; + if (isIndexModeTimeSeries) { + dynProperties.time_series_metric = field.metric_type; + } + matchingType = field.object_type_mapping_type ?? 'double'; + break; + case 'byte': + case 'long': + case 'short': + case 'unsigned_long': + dynProperties.type = field.object_type; + if (isIndexModeTimeSeries) { + dynProperties.time_series_metric = field.metric_type; + } + matchingType = field.object_type_mapping_type ?? 'long'; + break; + case 'integer': + // Map integers as long, as in other cases. + dynProperties.type = 'long'; + if (isIndexModeTimeSeries) { + dynProperties.time_series_metric = field.metric_type; + } + matchingType = field.object_type_mapping_type ?? 'long'; + break; + case 'boolean': + dynProperties.type = field.object_type; + if (isIndexModeTimeSeries) { + dynProperties.time_series_metric = field.metric_type; + } + matchingType = field.object_type_mapping_type ?? field.object_type; + break; + case 'group': + if (!field?.fields) { + break; + } + const subFields = field.fields.map((subField) => ({ + ...subField, + type: 'object', + object_type: subField.object_type ?? subField.type, + })); + const mappings = _generateMappings( + subFields, + { + ...ctx, + groupFieldName: ctx.groupFieldName ? `${ctx.groupFieldName}.${field.name}` : field.name, + }, + isIndexModeTimeSeries + ); + if (mappings.hasDynamicTemplateMappings) { + hasDynamicTemplateMappings = true; + } + break; + case 'flattened': + dynProperties.type = field.object_type; + matchingType = field.object_type_mapping_type ?? 'object'; + break; + default: + throw new PackageInvalidArchiveError( + `No dynamic mapping generated for field ${path} of type ${field.object_type}` + ); + } + + if (field.dimension && isIndexModeTimeSeries) { + dynProperties.time_series_dimension = field.dimension; + } + + // When a wildcard field specifies the subobjects setting, + // the parent intermediate object should set the subobjects + // setting. + // + // For example, if a wildcard field `foo.*` has subobjects, + // we should set subobjects on the intermediate object `foo`. + // + if (field.subobjects !== undefined && path.includes('*')) { + subobjects = field.subobjects; + } + + if (dynProperties && matchingType) { + addDynamicMappingWithIntermediateObjects(path, pathMatch, matchingType, dynProperties); + + // Add the parent object as static property, this is needed for + // index templates not using `"dynamic": true`. + addParentObjectAsStaticProperty(field); + } + } + // TODO: this can happen when the fields property in fields.yml is present but empty // Maybe validation should be moved to fields/field.ts if (fields) { @@ -360,123 +478,7 @@ function _generateMappings( } if (type === 'object' && field.object_type) { - const path = ctx.groupFieldName ? `${ctx.groupFieldName}.${field.name}` : field.name; - const pathMatch = path.includes('*') ? path : `${path}.*`; - - let dynProperties: Properties = getDefaultProperties(field); - let matchingType: string | undefined; - switch (field.object_type) { - case 'histogram': - dynProperties = histogram(field); - matchingType = field.object_type_mapping_type ?? '*'; - break; - case 'ip': - case 'keyword': - case 'match_only_text': - case 'text': - case 'wildcard': - dynProperties.type = field.object_type; - matchingType = field.object_type_mapping_type ?? 'string'; - break; - case 'scaled_float': - dynProperties = scaledFloat(field); - matchingType = field.object_type_mapping_type ?? '*'; - break; - case 'aggregate_metric_double': - dynProperties.type = field.object_type; - dynProperties.metrics = field.metrics; - dynProperties.default_metric = field.default_metric; - matchingType = field.object_type_mapping_type ?? '*'; - break; - case 'double': - case 'float': - case 'half_float': - dynProperties.type = field.object_type; - if (isIndexModeTimeSeries) { - dynProperties.time_series_metric = field.metric_type; - } - matchingType = field.object_type_mapping_type ?? 'double'; - break; - case 'byte': - case 'long': - case 'short': - case 'unsigned_long': - dynProperties.type = field.object_type; - if (isIndexModeTimeSeries) { - dynProperties.time_series_metric = field.metric_type; - } - matchingType = field.object_type_mapping_type ?? 'long'; - break; - case 'integer': - // Map integers as long, as in other cases. - dynProperties.type = 'long'; - if (isIndexModeTimeSeries) { - dynProperties.time_series_metric = field.metric_type; - } - matchingType = field.object_type_mapping_type ?? 'long'; - break; - case 'boolean': - dynProperties.type = field.object_type; - if (isIndexModeTimeSeries) { - dynProperties.time_series_metric = field.metric_type; - } - matchingType = field.object_type_mapping_type ?? field.object_type; - break; - case 'group': - if (!field?.fields) { - break; - } - const subFields = field.fields.map((subField) => ({ - ...subField, - type: 'object', - object_type: subField.object_type ?? subField.type, - })); - const mappings = _generateMappings( - subFields, - { - ...ctx, - groupFieldName: ctx.groupFieldName - ? `${ctx.groupFieldName}.${field.name}` - : field.name, - }, - isIndexModeTimeSeries - ); - if (mappings.hasDynamicTemplateMappings) { - hasDynamicTemplateMappings = true; - } - break; - case 'flattened': - dynProperties.type = field.object_type; - matchingType = field.object_type_mapping_type ?? 'object'; - break; - default: - throw new PackageInvalidArchiveError( - `No dynamic mapping generated for field ${path} of type ${field.object_type}` - ); - } - - if (field.dimension && isIndexModeTimeSeries) { - dynProperties.time_series_dimension = field.dimension; - } - - // When a wildcard field specifies the subobjects setting, - // the parent intermediate object should set the subobjects - // setting. - // - // For example, if a wildcard field `foo.*` has subobjects, - // we should set subobjects on the intermediate object `foo`. - // - if (field.subobjects !== undefined && path.includes('*')) { - subobjects = field.subobjects; - } - - if (dynProperties && matchingType) { - addDynamicMappingWithIntermediateObjects(path, pathMatch, matchingType, dynProperties); - - // Add the parent object as static property, this is needed for - // index templates not using `"dynamic": true`. - addParentObjectAsStaticProperty(field); - } + addObjectAsDynamicMapping(field); } else { let fieldProps = getDefaultProperties(field); @@ -492,6 +494,12 @@ function _generateMappings( }, isIndexModeTimeSeries ); + if (field.object_type) { + // A group can have an object_type if it has been merged with an object during deduplication, + // generate also the dynamic mapping for the object. + addObjectAsDynamicMapping(field); + mappings.hasDynamicTemplateMappings = true; + } if (mappings.hasNonDynamicTemplateMappings) { fieldProps = { properties: