[Response Ops][Alerting] Adding null checks when iterating through index template list (#158742)

## Summary

When updating common component templates during AAD resource
installation, we occassionally run into errors where the index templates
using the common component template has a total field limit that is less
than the new total number of fields with the updated component template.
When this occurs, we query the ES index template API to get the list of
index templates in order to check their `composed_of` field to see if
they reference the specific component template and act accordingly. In
theory, `composed_of` is (and is typed as) a required array. In
practice, it seems that this field can be missing from the response.
Since we're doing a `.includes` check on this array, this can lead to
null dereference errors that can halt alerts as data resource
installation.

This PR adds a check to make sure the `composed_of` field exists before
using it.

## To Verify
1. Run Kibana 8.6 locally and create a metric threshold rule & detection
rule that generates alerts. Make sure the alert index templates for
these rule types have been created and the rule runs successfully.
2. Update to Kibana 8.7. Make sure the rules run successfully and
generate alerts. Create a data view with no component templates. To do
this, go to `Stack Management > Index Management > Index Templates` and
click `Create template`. Fill in a name and index pattern (`test*` is
fine) and make sure the `Create data stream` switch is checked. Click
through all the remaining options without setting anything else. Use
`GET /_index_template/<name>` to verify that this index template has no
`composed_of` field
3. Update to this branch. All alert resources should be installed
successfully and there should be no errors on startup.
This commit is contained in:
Ying Mao 2023-06-01 07:56:51 -04:00 committed by GitHub
parent 36ae6c8e62
commit 0f02b9e968
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 116 additions and 2 deletions

View file

@ -189,6 +189,112 @@ describe('createOrUpdateComponentTemplate', () => {
});
});
it(`should update index template field limit and retry if putTemplate throws error with field limit error when there are malformed index templates`, async () => {
clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(
new EsErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
statusCode: 400,
body: {
error: {
root_cause: [
{
type: 'illegal_argument_exception',
reason:
'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged',
},
],
type: 'illegal_argument_exception',
reason:
'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged',
caused_by: {
type: 'illegal_argument_exception',
reason:
'composable template [.alerts-security.alerts-default-index-template] template after composition with component templates [.alerts-ecs-mappings, .alerts-security.alerts-mappings, .alerts-technical-mappings] is invalid',
caused_by: {
type: 'illegal_argument_exception',
reason:
'invalid composite mappings for [.alerts-security.alerts-default-index-template]',
caused_by: {
type: 'illegal_argument_exception',
reason: 'Limit of total fields [1900] has been exceeded',
},
},
},
},
},
})
)
);
const existingIndexTemplate = {
name: 'test-template',
index_template: {
index_patterns: ['test*'],
composed_of: ['test-mappings'],
template: {
settings: {
auto_expand_replicas: '0-1',
hidden: true,
'index.lifecycle': {
name: '.alerts-ilm-policy',
rollover_alias: `.alerts-empty-default`,
},
'index.mapping.total_fields.limit': 1800,
},
mappings: {
dynamic: false,
},
},
},
};
clusterClient.indices.getIndexTemplate.mockResolvedValueOnce({
index_templates: [
existingIndexTemplate,
{
name: 'lyndon',
// @ts-expect-error
index_template: {
index_patterns: ['intel*'],
},
},
{
name: 'sample_ds',
// @ts-expect-error
index_template: {
index_patterns: ['sample_ds-*'],
data_stream: {
hidden: false,
allow_custom_routing: false,
},
},
},
],
});
await createOrUpdateComponentTemplate({
logger,
esClient: clusterClient,
template: ComponentTemplate,
totalFieldsLimit: 2500,
});
expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(2);
expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(1);
expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({
name: existingIndexTemplate.name,
body: {
...existingIndexTemplate.index_template,
template: {
...existingIndexTemplate.index_template.template,
settings: {
...existingIndexTemplate.index_template.template?.settings,
'index.mapping.total_fields.limit': 2500,
},
},
},
});
});
it(`should retry getIndexTemplate and putIndexTemplate on transient ES errors`, async () => {
clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(
new EsErrors.ResponseError(

View file

@ -32,8 +32,16 @@ const getIndexTemplatesUsingComponentTemplate = async (
{ logger }
);
const indexTemplatesUsingComponentTemplate = (indexTemplates ?? []).filter(
(indexTemplate: IndicesGetIndexTemplateIndexTemplateItem) =>
indexTemplate.index_template.composed_of.includes(componentTemplateName)
(indexTemplate: IndicesGetIndexTemplateIndexTemplateItem) => {
if (
indexTemplate &&
indexTemplate.index_template &&
indexTemplate.index_template.composed_of
) {
return indexTemplate.index_template.composed_of.includes(componentTemplateName);
}
return false;
}
);
await asyncForEach(
indexTemplatesUsingComponentTemplate,