[8.15] [UII] Fill in empty values for `constant_keyword` fields from existing mappings (#188145) (#188170)

# Backport

This will backport the following commits from `main` to `8.15`:
- [[UII] Fill in empty values for `constant_keyword` fields
from existing mappings
(#188145)](https://github.com/elastic/kibana/pull/188145)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Jen
Huang","email":"its.jenetic@gmail.com"},"sourceCommit":{"committedDate":"2024-07-12T03:05:03Z","message":"[UII]
Fill in empty values for `constant_keyword` fields from existing
mappings (#188145)\n\n## Summary\r\n\r\nResolves
https://github.com/elastic/kibana/issues/178528.\r\n\r\nSome packages
declare `constant_keyword` type fields without an explicit\r\nvalue.
This causes ES to fill in the value in the mappings using the\r\nfirst
ingested value.\r\n\r\nWhen upgrading this type of package & field after
the value has already\r\nbeen populated in this way, the mappings update
fail due to pushing a\r\n`null` value into an existing value, triggering
unnecessary rollovers.\r\n\r\nThis PR fixes that by filling in the empty
values from the existing\r\nmappings.\r\n\r\n## Test\r\n1. On an empty
cluster, turn on debug logs\r\n2. Set up Fleet Server policy and Fleet
Server agent\r\n3. Force install old version of Elastic Agent
integration, v1.19.2:\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_agent/1.19.2\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n4. Create a new empty policy, **deselect system and
agent monitoring**\r\n(otherwise the integration will be upgraded, we do
not want this yet)\r\n5. Manually add Elastic Agent integration v1.19.2
to the new policy\r\n6. Edit the policy to enable logs and metrics
monitoring\r\n7. Enroll agent into the policy, confirm that monitoring
logs and\r\nmetrics are being ingested and that a value exists for
`event.dataset`\r\nmapping for the logs:\r\n```\r\nGET
logs-elastic_agent*/_mappings\r\n```\r\n```\r\n \"dataset\": {\r\n
\"type\": \"constant_keyword\",\r\n \"value\": \"elastic_agent\"\r\n
}\r\n```\r\n9. Upgrade Elastic Agent integration to v1.20.0 (note we are
not\r\nupgrading to the newest versions, 2.0+, because these **are**
expected\r\nto trigger rollovers for some data streams):\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_agent/1.20.0\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n10. Confirm in Kibana logs that no rollovers
triggered during the\r\nupgrade\r\n11. Confirm that there is still only
1 backing index for monitoring\r\nlogs:\r\n```\r\nGET
logs-elastic_agent*\r\n```\r\n\r\n### Checklist\r\n\r\nDelete any items
that are not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"b7c96f4c09e88b820664bbd0bb996844dd50a0e6","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Fleet","backport:prev-minor","v8.16.0"],"title":"[UII]
Fill in empty values for `constant_keyword` fields from existing
mappings","number":188145,"url":"https://github.com/elastic/kibana/pull/188145","mergeCommit":{"message":"[UII]
Fill in empty values for `constant_keyword` fields from existing
mappings (#188145)\n\n## Summary\r\n\r\nResolves
https://github.com/elastic/kibana/issues/178528.\r\n\r\nSome packages
declare `constant_keyword` type fields without an explicit\r\nvalue.
This causes ES to fill in the value in the mappings using the\r\nfirst
ingested value.\r\n\r\nWhen upgrading this type of package & field after
the value has already\r\nbeen populated in this way, the mappings update
fail due to pushing a\r\n`null` value into an existing value, triggering
unnecessary rollovers.\r\n\r\nThis PR fixes that by filling in the empty
values from the existing\r\nmappings.\r\n\r\n## Test\r\n1. On an empty
cluster, turn on debug logs\r\n2. Set up Fleet Server policy and Fleet
Server agent\r\n3. Force install old version of Elastic Agent
integration, v1.19.2:\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_agent/1.19.2\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n4. Create a new empty policy, **deselect system and
agent monitoring**\r\n(otherwise the integration will be upgraded, we do
not want this yet)\r\n5. Manually add Elastic Agent integration v1.19.2
to the new policy\r\n6. Edit the policy to enable logs and metrics
monitoring\r\n7. Enroll agent into the policy, confirm that monitoring
logs and\r\nmetrics are being ingested and that a value exists for
`event.dataset`\r\nmapping for the logs:\r\n```\r\nGET
logs-elastic_agent*/_mappings\r\n```\r\n```\r\n \"dataset\": {\r\n
\"type\": \"constant_keyword\",\r\n \"value\": \"elastic_agent\"\r\n
}\r\n```\r\n9. Upgrade Elastic Agent integration to v1.20.0 (note we are
not\r\nupgrading to the newest versions, 2.0+, because these **are**
expected\r\nto trigger rollovers for some data streams):\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_agent/1.20.0\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n10. Confirm in Kibana logs that no rollovers
triggered during the\r\nupgrade\r\n11. Confirm that there is still only
1 backing index for monitoring\r\nlogs:\r\n```\r\nGET
logs-elastic_agent*\r\n```\r\n\r\n### Checklist\r\n\r\nDelete any items
that are not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"b7c96f4c09e88b820664bbd0bb996844dd50a0e6"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/188145","number":188145,"mergeCommit":{"message":"[UII]
Fill in empty values for `constant_keyword` fields from existing
mappings (#188145)\n\n## Summary\r\n\r\nResolves
https://github.com/elastic/kibana/issues/178528.\r\n\r\nSome packages
declare `constant_keyword` type fields without an explicit\r\nvalue.
This causes ES to fill in the value in the mappings using the\r\nfirst
ingested value.\r\n\r\nWhen upgrading this type of package & field after
the value has already\r\nbeen populated in this way, the mappings update
fail due to pushing a\r\n`null` value into an existing value, triggering
unnecessary rollovers.\r\n\r\nThis PR fixes that by filling in the empty
values from the existing\r\nmappings.\r\n\r\n## Test\r\n1. On an empty
cluster, turn on debug logs\r\n2. Set up Fleet Server policy and Fleet
Server agent\r\n3. Force install old version of Elastic Agent
integration, v1.19.2:\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_agent/1.19.2\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n4. Create a new empty policy, **deselect system and
agent monitoring**\r\n(otherwise the integration will be upgraded, we do
not want this yet)\r\n5. Manually add Elastic Agent integration v1.19.2
to the new policy\r\n6. Edit the policy to enable logs and metrics
monitoring\r\n7. Enroll agent into the policy, confirm that monitoring
logs and\r\nmetrics are being ingested and that a value exists for
`event.dataset`\r\nmapping for the logs:\r\n```\r\nGET
logs-elastic_agent*/_mappings\r\n```\r\n```\r\n \"dataset\": {\r\n
\"type\": \"constant_keyword\",\r\n \"value\": \"elastic_agent\"\r\n
}\r\n```\r\n9. Upgrade Elastic Agent integration to v1.20.0 (note we are
not\r\nupgrading to the newest versions, 2.0+, because these **are**
expected\r\nto trigger rollovers for some data streams):\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_agent/1.20.0\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n10. Confirm in Kibana logs that no rollovers
triggered during the\r\nupgrade\r\n11. Confirm that there is still only
1 backing index for monitoring\r\nlogs:\r\n```\r\nGET
logs-elastic_agent*\r\n```\r\n\r\n### Checklist\r\n\r\nDelete any items
that are not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"b7c96f4c09e88b820664bbd0bb996844dd50a0e6"}}]}]
BACKPORT-->

Co-authored-by: Jen Huang <its.jenetic@gmail.com>
This commit is contained in:
Kibana Machine 2024-07-12 06:48:50 +02:00 committed by GitHub
parent d8611f1b13
commit 6302a65c80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 270 additions and 6 deletions

View file

@ -39,7 +39,7 @@ import { retryTransientEsErrors } from '../retry';
import { PackageESError, PackageInvalidArchiveError } from '../../../../errors';
import { getDefaultProperties, histogram, keyword, scaledFloat } from './mappings';
import { isUserSettingsTemplate } from './utils';
import { isUserSettingsTemplate, fillConstantKeywordValues } from './utils';
interface Properties {
[key: string]: any;
@ -986,7 +986,7 @@ const updateAllDataStreams = async (
});
},
{
// Limit concurrent putMapping/rollover requests to avoid overhwhelming ES cluster
// Limit concurrent putMapping/rollover requests to avoid overwhelming ES cluster
concurrency: 20,
}
);
@ -1017,19 +1017,23 @@ const updateExistingDataStream = async ({
const currentSourceType = currentBackingIndexConfig.mappings?._source?.mode;
let settings: IndicesIndexSettings;
let mappings: MappingTypeMapping;
let mappings: MappingTypeMapping = {};
let lifecycle: any;
let subobjectsFieldChanged: boolean = false;
let simulateResult: any = {};
try {
const simulateResult = await retryTransientEsErrors(async () =>
simulateResult = await retryTransientEsErrors(async () =>
esClient.indices.simulateTemplate({
name: await getIndexTemplate(esClient, dataStreamName),
})
);
settings = simulateResult.template.settings;
mappings = simulateResult.template.mappings;
// @ts-expect-error template is not yet typed with DLM
mappings = fillConstantKeywordValues(
currentBackingIndexConfig?.mappings || {},
simulateResult.template.mappings
);
lifecycle = simulateResult.template.lifecycle;
// for now, remove from object so as not to update stream or data stream properties of the index until type and name
@ -1063,6 +1067,7 @@ const updateExistingDataStream = async ({
subobjectsFieldChanged
) {
logger.info(`Mappings update for ${dataStreamName} failed due to ${err}`);
logger.trace(`Attempted mappings: ${mappings}`);
if (options?.skipDataStreamRollover === true) {
logger.info(
`Skipping rollover for ${dataStreamName} as "skipDataStreamRollover" is enabled`
@ -1075,6 +1080,7 @@ const updateExistingDataStream = async ({
}
}
logger.error(`Mappings update for ${dataStreamName} failed due to unexpected error: ${err}`);
logger.trace(`Attempted mappings: ${mappings}`);
if (options?.ignoreMappingUpdateErrors === true) {
logger.info(`Ignore mapping update errors as "ignoreMappingUpdateErrors" is enabled`);
return;

View file

@ -0,0 +1,226 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { fillConstantKeywordValues } from './utils';
describe('fillConstantKeywordValues', () => {
const oldMappings = {
dynamic: false,
_meta: {
managed_by: 'fleet',
managed: true,
package: {
name: 'elastic_agent',
},
},
dynamic_templates: [
{
ecs_timestamp: {
match: '@timestamp',
mapping: {
ignore_malformed: false,
type: 'date',
},
},
},
],
date_detection: false,
properties: {
'@timestamp': {
type: 'date',
ignore_malformed: false,
},
load: {
properties: {
'1': {
type: 'double',
},
'5': {
type: 'double',
},
'15': {
type: 'double',
},
},
},
event: {
properties: {
agent_id_status: {
type: 'keyword',
ignore_above: 1024,
},
dataset: {
type: 'constant_keyword',
value: 'elastic_agent.metricbeat',
},
ingested: {
type: 'date',
format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis',
ignore_malformed: false,
},
},
},
message: {
type: 'match_only_text',
},
'dot.field': {
type: 'keyword',
},
constant_keyword_without_value: {
type: 'constant_keyword',
},
},
};
const newMappings = {
dynamic: false,
_meta: {
managed_by: 'fleet',
managed: true,
package: {
name: 'elastic_agent',
},
},
dynamic_templates: [
{
ecs_timestamp: {
match: '@timestamp',
mapping: {
ignore_malformed: false,
type: 'date',
},
},
},
],
date_detection: false,
properties: {
'@timestamp': {
type: 'date',
ignore_malformed: false,
},
load: {
properties: {
'1': {
type: 'double',
},
'5': {
type: 'double',
},
'15': {
type: 'double',
},
},
},
event: {
properties: {
agent_id_status: {
type: 'keyword',
ignore_above: 1024,
},
dataset: {
type: 'constant_keyword',
},
ingested: {
type: 'date',
format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis',
ignore_malformed: false,
},
},
},
message: {
type: 'match_only_text',
},
'dot.field': {
type: 'keyword',
},
some_new_field: {
type: 'keyword',
},
constant_keyword_without_value: {
type: 'constant_keyword',
},
},
};
it('should fill in missing constant_keyword values from old mappings correctly', () => {
// @ts-ignore
expect(fillConstantKeywordValues(oldMappings, newMappings)).toEqual({
dynamic: false,
_meta: {
managed_by: 'fleet',
managed: true,
package: {
name: 'elastic_agent',
},
},
dynamic_templates: [
{
ecs_timestamp: {
match: '@timestamp',
mapping: {
ignore_malformed: false,
type: 'date',
},
},
},
],
date_detection: false,
properties: {
'@timestamp': {
type: 'date',
ignore_malformed: false,
},
load: {
properties: {
'1': {
type: 'double',
},
'5': {
type: 'double',
},
'15': {
type: 'double',
},
},
},
event: {
properties: {
agent_id_status: {
type: 'keyword',
ignore_above: 1024,
},
dataset: {
type: 'constant_keyword',
value: 'elastic_agent.metricbeat',
},
ingested: {
type: 'date',
format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis',
ignore_malformed: false,
},
},
},
message: {
type: 'match_only_text',
},
'dot.field': {
type: 'keyword',
},
some_new_field: {
type: 'keyword',
},
constant_keyword_without_value: {
type: 'constant_keyword',
},
},
});
});
it('should return the same mappings if old mappings are not provided', () => {
// @ts-ignore
expect(fillConstantKeywordValues({}, newMappings)).toMatchObject(newMappings);
});
});

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { USER_SETTINGS_TEMPLATE_SUFFIX } from '../../../../constants';
@ -12,3 +13,34 @@ type UserSettingsTemplateName = `${TemplateBaseName}${typeof USER_SETTINGS_TEMPL
export const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName =>
name.endsWith(USER_SETTINGS_TEMPLATE_SUFFIX);
// For any `constant_keyword` fields in `newMappings` that don't have a `value`, access the same field in
// the `oldMappings` and fill in the value from there
export const fillConstantKeywordValues = (
oldMappings: MappingTypeMapping,
newMappings: MappingTypeMapping
) => {
const filledMappings = JSON.parse(JSON.stringify(newMappings)) as MappingTypeMapping;
const deepGet = (obj: any, keys: string[]) => keys.reduce((xs, x) => xs?.[x] ?? undefined, obj);
const fillEmptyConstantKeywordFields = (mappings: unknown, currentPath: string[] = []) => {
if (!mappings) return;
for (const [key, potentialField] of Object.entries(mappings)) {
const path = [...currentPath, key];
if (typeof potentialField === 'object') {
if (potentialField.type === 'constant_keyword' && potentialField.value === undefined) {
const valueFromOldMappings = deepGet(oldMappings.properties, [...path, 'value']);
if (valueFromOldMappings !== undefined) {
potentialField.value = valueFromOldMappings;
}
} else if (potentialField.properties && typeof potentialField.properties === 'object') {
fillEmptyConstantKeywordFields(potentialField.properties, [...path, 'properties']);
}
}
}
};
fillEmptyConstantKeywordFields(filledMappings.properties);
return filledMappings;
};