From e08682b662238995d861b9cb9fb75b43e0fb522d Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 26 Jun 2023 17:21:57 -0400 Subject: [PATCH] [ResponseOps] use Data Streams for AAD indices in serverless Changes the way the alerts-as-data (AAD) indices are created and written to, to allow them to be built as they have been in the past (alias and backing indices created manually) OR as an ES Data Stream. Serverless will use Data Streams, other environments will use the existing alias and backing indices. The configuration `xpack.alerting.useDataStreamForAlerts` determines which to use, and is false by default. This configuration is not intended to be used by customers, as it when used in the wrong environment, will cause failures creating and writing to the alert indices. PR https://github.com/elastic/kibana/pull/160572 - fix jest test - start integrating bits of the dataStreamAdapter - fix tests - fix function tests - fix FT - change the rule registry writer - fix FT - start using the new ds adapter to create alias indices - add create data stream logic - use alias by default createConcreteWriteIndex() as it's being called by other plugins now - start adapting jest tests to test datastream alerts as well - extend create_concrete_write_index jest tests for data stream - rebase, fix merge conflicts - fix jest test after merge conflict - convert the RR trial test to use data streams - fix jest test, remove ILM for datastream, run serverless o11y test with ds, add rr test with ds - fix jest and function tests - fix jest tests --- .buildkite/ftr_configs.yml | 2 +- .../resources/base/bin/kibana-docker | 1 + .../alerts_service/alerts_service.test.ts | 3690 +++++++++-------- .../server/alerts_service/alerts_service.ts | 9 + .../lib/create_concrete_write_index.test.ts | 1046 +++-- .../lib/create_concrete_write_index.ts | 129 +- .../lib/create_or_update_ilm_policy.test.ts | 6 + .../lib/create_or_update_ilm_policy.ts | 5 + .../create_or_update_index_template.test.ts | 38 +- .../lib/create_or_update_index_template.ts | 21 +- .../lib/data_stream_adapter.mock.ts | 18 + .../alerts_service/lib/data_stream_adapter.ts | 224 + x-pack/plugins/alerting/server/config.test.ts | 1 + x-pack/plugins/alerting/server/config.ts | 7 + x-pack/plugins/alerting/server/index.ts | 2 + x-pack/plugins/alerting/server/mocks.ts | 3 + x-pack/plugins/alerting/server/plugin.test.ts | 57 +- x-pack/plugins/alerting/server/plugin.ts | 6 + .../task_runner_alerts_client.test.ts | 3 + .../alerting/server/test_utils/index.ts | 28 + x-pack/plugins/alerting/server/types.ts | 2 + x-pack/plugins/rule_registry/server/plugin.ts | 3 + .../rule_data_client/rule_data_client.mock.ts | 1 + .../rule_data_client/rule_data_client.test.ts | 11 +- .../rule_data_client/rule_data_client.ts | 19 +- .../server/rule_data_client/types.ts | 1 + .../resource_installer.test.ts | 24 +- .../resource_installer.ts | 6 + .../rule_data_plugin_service.test.ts | 9 + .../rule_data_plugin_service.ts | 5 +- .../utils/create_lifecycle_executor.test.ts | 130 +- .../server/utils/create_lifecycle_executor.ts | 48 +- .../utils/create_lifecycle_rule_type.test.ts | 33 +- .../risk_engine_data_client.test.ts | 4 + .../risk_engine/risk_engine_data_client.ts | 4 +- x-pack/test/rule_registry/common/config.ts | 2 + .../lib/helpers/cleanup_registry_indices.ts | 25 +- .../rule_registry/common/lib/helpers/index.ts | 1 + .../lib/helpers/use_data_stream_for_alerts.ts | 18 + ...ig_basic.ts => config_trial_datastream.ts} | 5 +- .../spaces_only/tests/basic/index.ts | 22 - .../tests/trial/lifecycle_executor.ts | 13 +- .../api_integration/config.base.ts | 1 + 43 files changed, 3287 insertions(+), 2396 deletions(-) create mode 100644 x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts create mode 100644 x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts create mode 100644 x-pack/test/rule_registry/common/lib/helpers/use_data_stream_for_alerts.ts rename x-pack/test/rule_registry/spaces_only/{config_basic.ts => config_trial_datastream.ts} (82%) delete mode 100644 x-pack/test/rule_registry/spaces_only/tests/basic/index.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 70be71f08184a..6294bb171a50e 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -351,7 +351,7 @@ enabled: - x-pack/test/reporting_functional/reporting_and_timeout.config.ts - x-pack/test/rule_registry/security_and_spaces/config_basic.ts - x-pack/test/rule_registry/security_and_spaces/config_trial.ts - - x-pack/test/rule_registry/spaces_only/config_basic.ts + - x-pack/test/rule_registry/spaces_only/config_trial_datastream.ts - x-pack/test/rule_registry/spaces_only/config_trial.ts - x-pack/test/saved_object_api_integration/security_and_spaces/config_basic.ts - x-pack/test/saved_object_api_integration/security_and_spaces/config_trial.ts diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 43df58235e68d..9b6dac38f0753 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -231,6 +231,7 @@ kibana_vars=( xpack.alerting.rules.run.actions.max xpack.alerting.rules.run.alerts.max xpack.alerting.rules.run.actions.connectorTypeOverrides + xpack.alerting.useDataStreamForAlerts xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index e3942b26ee6fa..70916f5a75064 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { ReplaySubject, Subject } from 'rxjs'; import { AlertsService } from './alerts_service'; @@ -16,6 +17,7 @@ import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { AlertsClient } from '../alerts_client'; import { alertsClientMock } from '../alerts_client/alerts_client.mock'; +import { generateAlertingConfig } from '../test_utils'; jest.mock('../alerts_client'); @@ -63,6 +65,20 @@ const GetAliasResponse = { }, }; +const GetDataStreamResponse: IndicesGetDataStreamResponse = { + data_streams: [ + { + name: 'ignored', + generation: 1, + timestamp_field: { name: 'ignored' }, + hidden: true, + indices: [{ index_name: 'ignored', index_uuid: 'ignored' }], + status: 'green', + template: 'ignored', + }, + ], +}; + const IlmPutBody = { policy: { _meta: { @@ -88,6 +104,7 @@ interface GetIndexTemplatePutBodyOpts { useLegacyAlerts?: boolean; useEcs?: boolean; secondaryAlias?: string; + useDataStream?: boolean; } const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { const context = opts ? opts.context : undefined; @@ -95,25 +112,36 @@ const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { const useLegacyAlerts = opts ? opts.useLegacyAlerts : undefined; const useEcs = opts ? opts.useEcs : undefined; const secondaryAlias = opts ? opts.secondaryAlias : undefined; + const useDataStream = opts?.useDataStream ?? false; + + const indexPatterns = useDataStream + ? [`.alerts-${context ? context : 'test'}.alerts-${namespace}`] + : [`.internal.alerts-${context ? context : 'test'}.alerts-${namespace}-*`]; return { name: `.alerts-${context ? context : 'test'}.alerts-${namespace}-index-template`, body: { - index_patterns: [`.internal.alerts-${context ? context : 'test'}.alerts-${namespace}-*`], + index_patterns: indexPatterns, composed_of: [ ...(useEcs ? ['.alerts-ecs-mappings'] : []), `.alerts-${context ? `${context}.alerts` : 'test.alerts'}-mappings`, ...(useLegacyAlerts ? ['.alerts-legacy-alert-mappings'] : []), '.alerts-framework-mappings', ], + ...(useDataStream ? { data_stream: { hidden: true } } : {}), priority: namespace.length, template: { settings: { auto_expand_replicas: '0-1', hidden: true, - 'index.lifecycle': { - name: '.alerts-ilm-policy', - rollover_alias: `.alerts-${context ? context : 'test'}.alerts-${namespace}`, - }, + ...(useDataStream + ? {} + : { + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-${context ? context : 'test'}.alerts-${namespace}`, + }, + }), + 'index.mapping.total_fields.limit': 2500, }, mappings: { @@ -186,7 +214,7 @@ describe('Alerts Service', () => { let pluginStop$: Subject; beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); logger = loggingSystemMock.createLogger(); pluginStop$ = new ReplaySubject(1); jest.spyOn(global.Math, 'random').mockReturnValue(0.01); @@ -195,1809 +223,2063 @@ describe('Alerts Service', () => { async () => SimulateTemplateResponse ); clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); }); afterEach(() => { pluginStop$.next(); pluginStop$.complete(); }); - describe('AlertsService()', () => { - test('should correctly initialize common resources', async () => { - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - expect(alertsService.isInitialized()).toEqual(true); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); - - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - }); + for (const useDataStream of [false, true]) { + const label = useDataStream ? 'data streams' : 'aliases'; + const config = generateAlertingConfig(useDataStream); + + describe(`using ${label} for alert indices`, () => { + describe('AlertsService()', () => { + test('should correctly initialize common resources', async () => { + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + expect(alertsService.isInitialized()).toEqual(true); + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + if (!useDataStream) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); + + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + }); - test('should log error and set initialized to false if adding ILM policy throws error', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should log error and set initialized to false if adding ILM policy throws error', async () => { + if (useDataStream) return; - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); - expect(alertsService.isInitialized()).toEqual(false); + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - expect(logger.error).toHaveBeenCalledWith( - `Error installing ILM policy .alerts-ilm-policy - fail` - ); + expect(alertsService.isInitialized()).toEqual(false); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - }); + expect(logger.error).toHaveBeenCalledWith( + `Error installing ILM policy .alerts-ilm-policy - fail` + ); - test('should log error and set initialized to false if creating/updating common component template throws error', async () => { - clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); + }); - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + test('should log error and set initialized to false if creating/updating common component template throws error', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); - expect(alertsService.isInitialized()).toEqual(false); - expect(logger.error).toHaveBeenCalledWith( - `Error installing component template .alerts-framework-mappings - fail` - ); + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - }); + expect(alertsService.isInitialized()).toEqual(false); + expect(logger.error).toHaveBeenCalledWith( + `Error installing component template .alerts-framework-mappings - fail` + ); - test('should update index template field limit and retry initialization if creating/updating common component template fails with field limit error', async () => { - clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce( - new EsErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 400, - body: { - error: { - root_cause: [ - { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + }); + + test('should update index template field limit and retry initialization if creating/updating common component template fails with field limit error', 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', - }, - ], - 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', + 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: ['.alerts-framework-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, + }, }, }, - }) - ) - ); - const existingIndexTemplate = { - name: 'test-template', - index_template: { - index_patterns: ['test*'], - composed_of: ['.alerts-framework-mappings'], - template: { - settings: { - auto_expand_replicas: '0-1', - hidden: true, - 'index.lifecycle': { - name: '.alerts-ilm-policy', - rollover_alias: `.alerts-empty-default`, + }; + clusterClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [existingIndexTemplate], + }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + expect(clusterClient.indices.getIndexTemplate).toHaveBeenCalledTimes(1); + 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, + }, }, - 'index.mapping.total_fields.limit': 1800, - }, - mappings: { - dynamic: false, - }, - }, - }, - }; - clusterClient.indices.getIndexTemplate.mockResolvedValueOnce({ - index_templates: [existingIndexTemplate], - }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - expect(clusterClient.indices.getIndexTemplate).toHaveBeenCalledTimes(1); - 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, }, - }, - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - // 3x for framework, legacy-alert and ecs mappings, then 1 extra time to update component template - // after updating index template field limit - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - }); - }); + }); - describe('register()', () => { - let alertsService: AlertsService; - beforeEach(async () => { - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + // 3x for framework, legacy-alert and ecs mappings, then 1 extra time to update component template + // after updating index template field limit + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + }); }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - }); - - test('should correctly install resources for context when common initialization is complete', async () => { - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody() - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); + describe('register()', () => { + let alertsService: AlertsService; + beforeEach(async () => { + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + }); - test('should correctly install resources for context when useLegacyAlerts is true', async () => { - alertsService.register({ ...TestRegistrationContext, useLegacyAlerts: true }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody({ useLegacyAlerts: true }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); + test('should correctly install resources for context when common initialization is complete', async () => { + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStream) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ useDataStream }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalledWith({ + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + }); - test('should correctly install resources for context when useEcs is true', async () => { - alertsService.register({ ...TestRegistrationContext, useEcs: true }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody({ useEcs: true }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); + test('should correctly install resources for context when useLegacyAlerts is true', async () => { + alertsService.register({ ...TestRegistrationContext, useLegacyAlerts: true }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStream) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ useLegacyAlerts: true, useDataStream }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalledWith({ + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + }); - test('should correctly install resources for custom namespace on demand when isSpaceAware is true', async () => { - alertsService.register({ ...TestRegistrationContext, isSpaceAware: true }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( - 1, - getIndexTemplatePutBody() - ); - expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(1, { - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenNthCalledWith(1, { - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); + test('should correctly install resources for context when useEcs is true', async () => { + alertsService.register({ ...TestRegistrationContext, useEcs: true }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStream) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ useEcs: true, useDataStream }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenNthCalledWith(1, { + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + }); - await retryUntil( - 'context in namespace initialized', - async () => - (await getContextInitialized( - alertsService, - TestRegistrationContext.context, - 'another-namespace' - )) === true - ); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( - 2, - getIndexTemplatePutBody({ namespace: 'another-namespace' }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(2, { - index: '.internal.alerts-test.alerts-another-namespace-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.create).toHaveBeenNthCalledWith(2, { - index: '.internal.alerts-test.alerts-another-namespace-000001', - body: { - aliases: { - '.alerts-test.alerts-another-namespace': { - is_write_index: true, - }, - }, - }, - }); - }); + test('should correctly install resources for custom namespace on demand when isSpaceAware is true', async () => { + alertsService.register({ ...TestRegistrationContext, isSpaceAware: true }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStream) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 1, + getIndexTemplatePutBody({ useDataStream }) + ); + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenNthCalledWith(1, { + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenNthCalledWith(1, { + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(1, { + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + + clusterClient.indices.getDataStream.mockImplementationOnce(async () => ({ + data_streams: [], + })); + + await retryUntil( + 'context in namespace initialized', + async () => + (await getContextInitialized( + alertsService, + TestRegistrationContext.context, + 'another-namespace' + )) === true + ); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 2, + getIndexTemplatePutBody({ namespace: 'another-namespace', useDataStream }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 4); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 1 : 4 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 4); + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenNthCalledWith(1, { + name: '.alerts-test.alerts-another-namespace', + }); + expect(clusterClient.indices.getDataStream).toHaveBeenNthCalledWith(2, { + expand_wildcards: 'all', + name: '.alerts-test.alerts-another-namespace', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenNthCalledWith(2, { + index: '.internal.alerts-test.alerts-another-namespace-000001', + body: { + aliases: { + '.alerts-test.alerts-another-namespace': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(2, { + index: '.internal.alerts-test.alerts-another-namespace-*', + name: '.alerts-test.alerts-*', + }); + } + }); - test('should correctly install resources for context when secondaryAlias is defined', async () => { - alertsService.register({ ...TestRegistrationContext, secondaryAlias: 'another.alias' }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody({ secondaryAlias: 'another.alias' }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + test('should correctly install resources for context when secondaryAlias is defined', async () => { + if (useDataStream) return; + + alertsService.register({ ...TestRegistrationContext, secondaryAlias: 'another.alias' }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ secondaryAlias: 'another.alias', useDataStream }) + ); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, }, - }, - }, - }); - }); + }); + }); - test('should not install component template for context if fieldMap is empty', async () => { - alertsService.register({ - context: 'empty', - mappings: { fieldMap: {} }, - }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService, 'empty')) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({ - name: `.alerts-empty.alerts-default-index-template`, - body: { - index_patterns: [`.internal.alerts-empty.alerts-default-*`], - composed_of: ['.alerts-framework-mappings'], - priority: 7, - template: { - settings: { - auto_expand_replicas: '0-1', - hidden: true, - 'index.lifecycle': { - name: '.alerts-ilm-policy', - rollover_alias: `.alerts-empty.alerts-default`, + test('should not install component template for context if fieldMap is empty', async () => { + alertsService.register({ + context: 'empty', + mappings: { fieldMap: {} }, + }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService, 'empty')) === true + ); + + if (!useDataStream) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + + const template = { + name: `.alerts-empty.alerts-default-index-template`, + body: { + index_patterns: [ + useDataStream + ? `.alerts-empty.alerts-default` + : `.internal.alerts-empty.alerts-default-*`, + ], + composed_of: ['.alerts-framework-mappings'], + ...(useDataStream ? { data_stream: { hidden: true } } : {}), + priority: 7, + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + ...(useDataStream + ? {} + : { + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty.alerts-default`, + }, + }), + 'index.mapping.total_fields.limit': 2500, + }, + mappings: { + _meta: { + kibana: { version: '8.8.0' }, + managed: true, + namespace: 'default', + }, + dynamic: false, + }, }, - 'index.mapping.total_fields.limit': 2500, - }, - mappings: { _meta: { kibana: { version: '8.8.0' }, managed: true, namespace: 'default', }, - dynamic: false, - }, - }, - _meta: { - kibana: { version: '8.8.0' }, - managed: true, - namespace: 'default', - }, - }, - }); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-empty.alerts-default-*', - name: '.alerts-empty.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-empty.alerts-default-000001', - body: { - aliases: { - '.alerts-empty.alerts-default': { - is_write_index: true, }, - }, - }, - }); - }); + }; + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith(template); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalledWith({}); + expect(clusterClient.indices.getDataStream).toHaveBeenCalledWith({ + expand_wildcards: 'all', + name: '.alerts-empty.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-empty.alerts-default-000001', + body: { + aliases: { + '.alerts-empty.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-empty.alerts-default-*', + name: '.alerts-empty.alerts-*', + }); + } + + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + }); - test('should skip initialization if context already exists', async () => { - alertsService.register(TestRegistrationContext); - alertsService.register(TestRegistrationContext); + test('should skip initialization if context already exists', async () => { + alertsService.register(TestRegistrationContext); + alertsService.register(TestRegistrationContext); - expect(logger.debug).toHaveBeenCalledWith( - `Resources for context "test" have already been registered.` - ); - }); + expect(logger.debug).toHaveBeenCalledWith( + `Resources for context "test" have already been registered.` + ); + }); - test('should throw error if context already exists and has been registered with a different field map', async () => { - alertsService.register(TestRegistrationContext); - expect(() => { - alertsService.register({ - ...TestRegistrationContext, - mappings: { fieldMap: { anotherField: { type: 'keyword', required: false } } }, + test('should throw error if context already exists and has been registered with a different field map', async () => { + alertsService.register(TestRegistrationContext); + expect(() => { + alertsService.register({ + ...TestRegistrationContext, + mappings: { fieldMap: { anotherField: { type: 'keyword', required: false } } }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"test has already been registered with different options"` + ); }); - }).toThrowErrorMatchingInlineSnapshot( - `"test has already been registered with different options"` - ); - }); - test('should throw error if context already exists and has been registered with a different options', async () => { - alertsService.register(TestRegistrationContext); - expect(() => { - alertsService.register({ - ...TestRegistrationContext, - useEcs: true, + test('should throw error if context already exists and has been registered with a different options', async () => { + alertsService.register(TestRegistrationContext); + expect(() => { + alertsService.register({ + ...TestRegistrationContext, + useEcs: true, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"test has already been registered with different options"` + ); }); - }).toThrowErrorMatchingInlineSnapshot( - `"test has already been registered with different options"` - ); - }); - test('should not update index template if simulating template throws error', async () => { - clusterClient.indices.simulateTemplate.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to simulate index template mappings for .alerts-test.alerts-default-index-template; not applying mappings - fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - // putIndexTemplate is skipped but other operations are called as expected - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should not update index template if simulating template throws error', async () => { + clusterClient.indices.simulateTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(logger.error).toHaveBeenCalledWith( + `Failed to simulate index template mappings for .alerts-test.alerts-default-index-template; not applying mappings - fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + // putIndexTemplate is skipped but other operations are called as expected + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should log error and set initialized to false if simulating template returns empty mappings', async () => { - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ - result: false, - error: - 'Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping', - }); + test('should log error and set initialized to false if simulating template returns empty mappings', async () => { + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ + result: false, + error: + 'Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping', + }); + + expect(logger.error).toHaveBeenCalledWith( + new Error( + `No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping` + ) + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - expect(logger.error).toHaveBeenCalledWith( - new Error( - `No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping` - ) - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should log error and set initialized to false if updating index template throws error', async () => { + clusterClient.indices.putIndexTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith( + `Error installing index template .alerts-test.alerts-default-index-template - fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - test('should log error and set initialized to false if updating index template throws error', async () => { - clusterClient.indices.putIndexTemplate.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith( - `Error installing index template .alerts-test.alerts-default-index-template - fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should log error and set initialized to false if checking for concrete write index throws error', async () => { + clusterClient.indices.getAlias.mockRejectedValueOnce(new Error('fail')); + clusterClient.indices.getDataStream.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error fetching data stream for .alerts-test.alerts-default - fail` + : `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - test('should log error and set initialized to false if checking for concrete write index throws error', async () => { - clusterClient.indices.getAlias.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith( - `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should not throw error if checking for concrete write index throws 404', async () => { + const error = new Error(`index doesn't exist`) as HTTPError; + error.statusCode = 404; + clusterClient.indices.getAlias.mockRejectedValueOnce(error); + clusterClient.indices.getDataStream.mockRejectedValueOnce(error); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + } + }); - test('should not throw error if checking for concrete write index throws 404', async () => { - const error = new Error(`index doesn't exist`) as HTTPError; - error.statusCode = 404; - clusterClient.indices.getAlias.mockRejectedValueOnce(error); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if updating index settings for existing indices throws error', async () => { + clusterClient.indices.putSettings.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT index.mapping.total_fields.limit settings for .alerts-test.alerts-default: fail` + : `Failed to PUT index.mapping.total_fields.limit settings for alias_1: fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should log error and set initialized to false if updating index settings for existing indices throws error', async () => { - clusterClient.indices.putSettings.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT index.mapping.total_fields.limit settings for alias alias_1: fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should skip updating index mapping for existing indices if simulate index template throws error', async () => { + clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Ignored PUT mappings for .alerts-test.alerts-default; error generating simulated mappings: fail` + : `Ignored PUT mappings for alias_1; error generating simulated mappings: fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + + // this is called to update backing indices, so not used with data streams + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + } + }); - test('should skip updating index mapping for existing indices if simulate index template throws error', async () => { - clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(logger.error).toHaveBeenCalledWith( - `Ignored PUT mappings for alias alias_1; error generating simulated mappings: fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if updating index mappings for existing indices throws error', async () => { + clusterClient.indices.putMapping.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + if (useDataStream) { + expect(logger.error).toHaveBeenCalledWith( + `Failed to PUT mapping for .alerts-test.alerts-default: fail` + ); + } else { + expect(logger.error).toHaveBeenCalledWith(`Failed to PUT mapping for alias_1: fail`); + } + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should log error and set initialized to false if updating index mappings for existing indices throws error', async () => { - clusterClient.indices.putMapping.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith(`Failed to PUT mapping for alias alias_1: fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('does not updating settings or mappings if no existing concrete indices', async () => { + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({})); + clusterClient.indices.getDataStream.mockImplementationOnce(async () => ({ + data_streams: [], + })); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('does not updating settings or mappings if no existing concrete indices', async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({})); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if concrete indices exist but none are write index', async () => { + // not applicable for data streams + if (useDataStream) return; - test('should log error and set initialized to false if concrete indices exist but none are write index', async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-0001': { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: false, - is_hidden: true, - }, - alias_2: { - is_write_index: false, - is_hidden: true, + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-0001': { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: false, + is_hidden: true, + }, + alias_2: { + is_write_index: false, + is_hidden: true, + }, + }, }, - }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ - error: - 'Failure during installation. Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default', - result: false, - }); + })); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ + error: + 'Failure during installation. Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default', + result: false, + }); + + expect(logger.error).toHaveBeenCalledWith( + new Error( + `Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default` + ) + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - expect(logger.error).toHaveBeenCalledWith( - new Error( - `Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default` - ) - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('does not create new index if concrete write index exists', async () => { + // not applicable for data streams + if (useDataStream) return; - test('does not create new index if concrete write index exists', async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-0001': { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - is_hidden: true, - }, - alias_2: { - is_write_index: false, - is_hidden: true, + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-0001': { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + is_hidden: true, + }, + alias_2: { + is_write_index: false, + is_hidden: true, + }, + }, }, - }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); - - test('should log error and set initialized to false if create concrete index throws error', async () => { - clusterClient.indices.create.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); - - test('should not throw error if create concrete index throws resource_already_exists_exception error and write index already exists', async () => { - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.get).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); - - test('should log error and set initialized to false if create concrete index throws resource_already_exists_exception error and write index does not already exists', async () => { - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ - error: - 'Failure during installation. Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias', - result: false, - }); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.get).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); - }); - - describe('createAlertsClient()', () => { - let alertsService: AlertsService; - beforeEach(async () => { - (AlertsClient as jest.Mock).mockImplementation(() => alertsClient); - }); - - test('should create new AlertsClient', async () => { - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - }); - - test('should return null if rule type has no alert definition', async () => { - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - const result = await alertsService.createAlertsClient({ - logger, - ruleType, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(result).toBe(null); - expect(AlertsClient).not.toHaveBeenCalled(); - }); - - test('should retry initializing common resources if common resource initialization failed', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - - expect(result).not.toBe(null); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.info).toHaveBeenCalledWith( - `Resource installation for "test" succeeded after retry` - ); - }); + })); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should not retry initializing common resources if common resource initialization is in progress', async () => { - // this is the initial call that fails - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); + test('should log error and set initialized to false if create concrete index throws error', async () => { + // not applicable for data streams + if (useDataStream) return; + + clusterClient.indices.create.mockRejectedValueOnce(new Error('fail')); + clusterClient.indices.createDataStream.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - // this is the retry call that we'll artificially inflate the duration of - clusterClient.ilm.putLifecycle.mockImplementationOnce(async () => { - await new Promise((r) => setTimeout(r, 1000)); - return { acknowledged: true }; - }); + test('should not throw error if create concrete index throws resource_already_exists_exception error and write index already exists', async () => { + // not applicable for data streams + if (useDataStream) return; - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - // call createAlertsClient at the same time which will trigger the retries - const result = await Promise.all([ - alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }), - alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }), - ]); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - - expect(result[0]).not.toBe(null); - expect(result[1]).not.toBe(null); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.info).toHaveBeenCalledWith( - `Resource installation for "test" succeeded after retry` - ); - expect(logger.info).toHaveBeenCalledWith( - `Skipped retrying common resource initialization because it is already being retried.` - ); - }); - - test('should retry initializing context specific resources if context specific resource initialization failed', async () => { - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - - expect(result).not.toBe(null); - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.info).toHaveBeenCalledWith( - `Resource installation for "test" succeeded after retry` - ); - }); - - test('should not retry initializing context specific resources if context specific resource initialization is in progress', async () => { - // this is the initial call that fails - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); + })); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + expect(clusterClient.indices.get).toHaveBeenCalled(); + expect(clusterClient.indices.create).toHaveBeenCalled(); + }); - // this is the retry call that we'll artificially inflate the duration of - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => { - await new Promise((r) => setTimeout(r, 1000)); - return SimulateTemplateResponse; - }); + test('should log error and set initialized to false if create concrete index throws resource_already_exists_exception error and write index does not already exists', async () => { + // not applicable for data streams + if (useDataStream) return; - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const createAlertsClientWithDelay = async (delayMs: number | null) => { - if (delayMs) { - await new Promise((r) => setTimeout(r, delayMs)); - } - - return await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, + }, + })); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ + error: + 'Failure during installation. Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias', + result: false, + }); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + expect(clusterClient.indices.get).toHaveBeenCalled(); + expect(clusterClient.indices.create).toHaveBeenCalled(); }); - }; - - const result = await Promise.all([ - createAlertsClientWithDelay(null), - createAlertsClientWithDelay(1), - ]); - - expect(AlertsClient).toHaveBeenCalledTimes(2); - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', }); - expect(result[0]).not.toBe(null); - expect(result[1]).not.toBe(null); - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - - // Should only log the retry once because the second call should - // leverage the outcome of the first retry - expect( - logger.info.mock.calls.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` - ).length - ).toEqual(1); - expect( - logger.info.mock.calls.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (calls: any[]) => calls[0] === `Resource installation for "test" succeeded after retry` - ).length - ).toEqual(1); - }); + describe('createAlertsClient()', () => { + let alertsService: AlertsService; + beforeEach(async () => { + (AlertsClient as jest.Mock).mockImplementation(() => alertsClient); + }); - test('should throttle retries of initializing context specific resources', async () => { - // this is the initial call that fails - clusterClient.indices.simulateTemplate.mockImplementation(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); + test('should create new AlertsClient', async () => { + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const createAlertsClientWithDelay = async (delayMs: number | null) => { - if (delayMs) { - await new Promise((r) => setTimeout(r, delayMs)); - } - - return await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, + kibanaVersion: '8.8.0', + }); }); - }; - - await Promise.all([ - createAlertsClientWithDelay(null), - createAlertsClientWithDelay(1), - createAlertsClientWithDelay(2), - ]); - - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - - // Should only log the retry once because the second and third retries should be throttled - expect( - logger.info.mock.calls.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` - ).length - ).toEqual(1); - }); - - test('should return null if retrying common resources initialization fails again', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail again')); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - expect(result).toBe(null); - expect(AlertsClient).not.toHaveBeenCalled(); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.warn).toHaveBeenCalledWith( - `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation. fail; Error after retry: Failure during installation. fail again` - ); - }); - test('should return null if retrying common resources initialization fails again with same error', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - expect(result).toBe(null); - expect(AlertsClient).not.toHaveBeenCalled(); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.warn).toHaveBeenCalledWith( - `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Retry failed with error: Failure during installation. fail` - ); - }); - - test('should return null if retrying context specific initialization fails again', async () => { - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); - clusterClient.indices.putIndexTemplate.mockRejectedValueOnce( - new Error('fail index template') - ); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(AlertsClient).not.toHaveBeenCalled(); - expect(result).toBe(null); - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.warn).toHaveBeenCalledWith( - `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping; Error after retry: Failure during installation. fail index template` - ); - }); - }); - - describe('retries', () => { - test('should retry adding ILM policy for transient ES errors', async () => { - clusterClient.ilm.putLifecycle - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should return null if rule type has no alert definition', async () => { + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + const result = await alertsService.createAlertsClient({ + logger, + ruleType, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(3); - }); + expect(result).toBe(null); + expect(AlertsClient).not.toHaveBeenCalled(); + }); - test('should retry adding component template for transient ES errors', async () => { - clusterClient.cluster.putComponentTemplate - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should retry initializing common resources if common resource initialization failed', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing ILM policy failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 2); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result).not.toBe(null); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.info).toHaveBeenCalledWith( + `Resource installation for "test" succeeded after retry` + ); + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(5); - }); + test('should not retry initializing common resources if common resource initialization is in progress', async () => { + // this is the initial call that fails + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); + + // this is the retry call that we'll artificially inflate the duration of + clusterClient.cluster.putComponentTemplate.mockImplementationOnce(async () => { + await new Promise((r) => setTimeout(r, 1000)); + return { acknowledged: true }; + }); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing ILM policy failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + // call createAlertsClient at the same time which will trigger the retries + const result = await Promise.all([ + alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }), + alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }), + ]); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 2); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result[0]).not.toBe(null); + expect(result[1]).not.toBe(null); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.info).toHaveBeenCalledWith( + `Resource installation for "test" succeeded after retry` + ); + expect(logger.info).toHaveBeenCalledWith( + `Skipped retrying common resource initialization because it is already being retried.` + ); + }); - test('should retry updating index template for transient ES errors', async () => { - clusterClient.indices.putIndexTemplate - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should retry initializing context specific resources if context specific resource initialization failed', async () => { + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - expect(alertsService.isInitialized()).toEqual(true); + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result).not.toBe(null); + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.info).toHaveBeenCalledWith( + `Resource installation for "test" succeeded after retry` + ); + }); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); + test('should not retry initializing context specific resources if context specific resource initialization is in progress', async () => { + // this is the initial call that fails + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + // this is the retry call that we'll artificially inflate the duration of + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => { + await new Promise((r) => setTimeout(r, 1000)); + return SimulateTemplateResponse; + }); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const createAlertsClientWithDelay = async (delayMs: number | null) => { + if (delayMs) { + await new Promise((r) => setTimeout(r, delayMs)); + } - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(3); - }); + return await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + }; + + const result = await Promise.all([ + createAlertsClientWithDelay(null), + createAlertsClientWithDelay(1), + ]); + + expect(AlertsClient).toHaveBeenCalledTimes(2); + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result[0]).not.toBe(null); + expect(result[1]).not.toBe(null); + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + + // Should only log the retry once because the second call should + // leverage the outcome of the first retry + expect( + logger.info.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` + ).length + ).toEqual(1); + expect( + logger.info.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (calls: any[]) => + calls[0] === `Resource installation for "test" succeeded after retry` + ).length + ).toEqual(1); + }); - test('should retry updating index settings for existing indices for transient ES errors', async () => { - clusterClient.indices.putSettings - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should throttle retries of initializing context specific resources', async () => { + // this is the initial call that fails + clusterClient.indices.simulateTemplate.mockImplementation(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const createAlertsClientWithDelay = async (delayMs: number | null) => { + if (delayMs) { + await new Promise((r) => setTimeout(r, delayMs)); + } - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); + return await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + }; + + await Promise.all([ + createAlertsClientWithDelay(null), + createAlertsClientWithDelay(1), + createAlertsClientWithDelay(2), + ]); + + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + + // Should only log the retry once because the second and third retries should be throttled + expect( + logger.info.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` + ).length + ).toEqual(1); + }); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); + test('should return null if retrying common resources initialization fails again', async () => { + let failCount = 0; + clusterClient.cluster.putComponentTemplate.mockImplementation(() => { + throw new Error(`fail ${++failCount}`); + }); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing ILM policy failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 2); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + expect(result).toBe(null); + expect(AlertsClient).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching( + /There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation\. fail \d+; Error after retry: Failure during installation\. fail \d+/ + ) + ); + }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); - }); + test('should return null if retrying common resources initialization fails again with same error', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValue(new Error('fail')); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing component template failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 1); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(useDataStream ? 0 : 2); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + expect(result).toBe(null); + expect(AlertsClient).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.warn).toHaveBeenCalledWith( + `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Retry failed with error: Failure during installation. fail` + ); + }); - test('should retry updating index mappings for existing indices for transient ES errors', async () => { - clusterClient.indices.putMapping - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', + test('should return null if retrying context specific initialization fails again', async () => { + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + clusterClient.indices.putIndexTemplate.mockRejectedValueOnce( + new Error('fail index template') + ); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(AlertsClient).not.toHaveBeenCalled(); + expect(result).toBe(null); + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.warn).toHaveBeenCalledWith( + `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping; Error after retry: Failure during installation. fail index template` + ); + }); }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(4); - }); + describe('retries', () => { + test('should retry adding ILM policy for transient ES errors', async () => { + if (useDataStream) return; + + clusterClient.ilm.putLifecycle + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(3); + }); - test('should retry creating concrete index for transient ES errors', async () => { - clusterClient.indices.create - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ index: 'index', shards_acknowledged: true, acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should retry adding component template for transient ES errors', async () => { + clusterClient.cluster.putComponentTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(5); + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); + test('should retry updating index template for transient ES errors', async () => { + clusterClient.indices.putIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + expect(alertsService.isInitialized()).toEqual(true); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(3); + }); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); + test('should retry updating index settings for existing indices for transient ES errors', async () => { + clusterClient.indices.putSettings + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (useDataStream) { + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); + } + }); - expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); - }); - }); + test('should retry updating index mappings for existing indices for transient ES errors', async () => { + clusterClient.indices.putMapping + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 3 : 4); + }); - describe('timeout', () => { - test('should short circuit initialization if timeout exceeded', async () => { - clusterClient.ilm.putLifecycle.mockImplementationOnce(async () => { - await new Promise((resolve) => setTimeout(resolve, 20)); - return { acknowledged: true }; - }); - new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - timeoutMs: 10, + test('should retry creating concrete index for transient ES errors', async () => { + clusterClient.indices.getDataStream.mockImplementationOnce(async () => ({ + data_streams: [], + })); + clusterClient.indices.createDataStream + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + clusterClient.indices.create + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ index: 'index', shards_acknowledged: true, acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + config, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); + } + }); }); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect(logger.error).toHaveBeenCalledWith(new Error(`Timeout: it took more than 10ms`)); - }); + describe('timeout', () => { + test('should short circuit initialization if timeout exceeded', async () => { + clusterClient.cluster.putComponentTemplate.mockImplementationOnce(async () => { + await new Promise((resolve) => setTimeout(resolve, 20)); + return { acknowledged: true }; + }); + new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + timeoutMs: 10, + config, + }); + + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect(logger.error).toHaveBeenCalledWith(new Error(`Timeout: it took more than 10ms`)); + }); - test('should short circuit initialization if pluginStop$ signal received but not throw error', async () => { - pluginStop$.next(); - new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - timeoutMs: 10, + test('should short circuit initialization if pluginStop$ signal received but not throw error', async () => { + pluginStop$.next(); + new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + timeoutMs: 10, + config, + }); + + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect(logger.error).toHaveBeenCalledWith( + new Error(`Server is stopping; must stop all async operations`) + ); + }); }); - - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect(logger.error).toHaveBeenCalledWith( - new Error(`Server is stopping; must stop all async operations`) - ); }); - }); + } }); diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts index e8ecab61e76d9..1734257a8759f 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts @@ -20,6 +20,8 @@ import { getIndexTemplateAndPattern, } from './resource_installer_utils'; import { AlertInstanceContext, AlertInstanceState, IRuleTypeAlerts, RuleAlertData } from '../types'; +import { AlertingConfig } from '../config'; +import { DataStreamAdapter, getDataStreamAdapter } from './lib/data_stream_adapter'; import { createResourceInstallationHelper, errorResult, @@ -47,6 +49,7 @@ interface AlertsServiceParams { logger: Logger; pluginStop$: Observable; kibanaVersion: string; + config: AlertingConfig; elasticsearchClientPromise: Promise; timeoutMs?: number; } @@ -114,10 +117,13 @@ export class AlertsService implements IAlertsService { private resourceInitializationHelper: ResourceInstallationHelper; private registeredContexts: Map = new Map(); private commonInitPromise: Promise; + private dataStreamAdapter: DataStreamAdapter; constructor(private readonly options: AlertsServiceParams) { this.initialized = false; + this.dataStreamAdapter = getDataStreamAdapter(options.config); + // Kick off initialization of common assets and save the promise this.commonInitPromise = this.initializeCommon(this.options.timeoutMs); @@ -296,6 +302,7 @@ export class AlertsService implements IAlertsService { esClient, name: DEFAULT_ALERTS_ILM_POLICY_NAME, policy: DEFAULT_ALERTS_ILM_POLICY, + dataStreamAdapter: this.dataStreamAdapter, }), () => createOrUpdateComponentTemplate({ @@ -421,6 +428,7 @@ export class AlertsService implements IAlertsService { kibanaVersion: this.options.kibanaVersion, namespace, totalFieldsLimit: TOTAL_FIELDS_LIMIT, + dataStreamAdapter: this.dataStreamAdapter, }), }), async () => @@ -429,6 +437,7 @@ export class AlertsService implements IAlertsService { esClient, totalFieldsLimit: TOTAL_FIELDS_LIMIT, indexPatterns: indexTemplateAndPattern, + dataStreamAdapter: this.dataStreamAdapter, }), ]); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts index a4cb6a26d3767..e2ee309b123f5 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts @@ -6,7 +6,9 @@ */ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; +import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { createConcreteWriteIndex } from './create_concrete_write_index'; +import { getDataStreamAdapter } from './data_stream_adapter'; const randomDelayMultiplier = 0.01; const logger = loggingSystemMock.createLogger(); @@ -36,6 +38,10 @@ const GetAliasResponse = { }, }; +const GetDataStreamResponse = { + data_streams: ['any-content-here-means-already-exists'], +} as unknown as IndicesGetDataStreamResponse; + const SimulateTemplateResponse = { template: { aliases: { @@ -60,483 +66,609 @@ const IndexPatterns = { }; describe('createConcreteWriteIndex', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.spyOn(global.Math, 'random').mockReturnValue(randomDelayMultiplier); - }); - - it(`should call esClient to put index template`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + for (const useDataStream of [false, true]) { + const label = useDataStream ? 'data streams' : 'aliases'; + const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: useDataStream }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(global.Math, 'random').mockReturnValue(randomDelayMultiplier); }); - }); - - it(`should retry on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.create - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ - index: '.internal.alerts-test.alerts-default-000001', - shards_acknowledged: true, - acknowledged: true, + + describe(`using ${label} for alert indices`, () => { + it(`should call esClient to put index template`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledWith({ + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } }); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); - expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); - }); - - it(`should log and throw error if max retries exceeded`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.create.mockRejectedValue(new EsErrors.ConnectionError('foo')); - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - foo`); - expect(clusterClient.indices.create).toHaveBeenCalledTimes(4); - }); - - it(`should log and throw error if ES throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.create.mockRejectedValueOnce(new Error('generic error')); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); - - expect(logger.error).toHaveBeenCalledWith( - `Error creating concrete write index - generic error` - ); - }); - - it(`should log and return if ES throws resource_already_exists_exception error and existing index is already write index`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, - }, - })); + it(`should retry on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.create + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ + index: '.internal.alerts-test.alerts-default-000001', + shards_acknowledged: true, + acknowledged: true, + }); + clusterClient.indices.createDataStream + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ + acknowledged: true, + }); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); + } + }); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if max retries exceeded`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.create.mockRejectedValue(new EsErrors.ConnectionError('foo')); + clusterClient.indices.createDataStream.mockRejectedValue( + new EsErrors.ConnectionError('foo') + ); + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error creating data stream .alerts-test.alerts-default - foo` + : `Error creating concrete write index - foo` + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledTimes(4); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledTimes(4); + } + }); - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - }); - - it(`should retry getting index on transient ES error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, - }, - })); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if ES throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.create.mockRejectedValueOnce(new Error('generic error')); + clusterClient.indices.createDataStream.mockRejectedValueOnce(new Error('generic error')); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error creating data stream .alerts-test.alerts-default - generic error` + : `Error creating concrete write index - generic error` + ); + }); - expect(clusterClient.indices.get).toHaveBeenCalledTimes(3); - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - }); - - it(`should log and throw error if ES throws resource_already_exists_exception error and existing index is not the write index`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, - }, - })); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias"` - ); - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - }); - - it(`should call esClient to put index template if get alias throws 404`, async () => { - const error = new Error(`not found`) as EsError; - error.statusCode = 404; - clusterClient.indices.getAlias.mockRejectedValueOnce(error); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and return if ES throws resource_already_exists_exception error and existing index is already write index`, async () => { + if (useDataStream) return; - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - }, - }, - }); - }); - - it(`should log and throw error if get alias throws non-404 error`, async () => { - const error = new Error(`fatal error`) as EsError; - error.statusCode = 500; - clusterClient.indices.getAlias.mockRejectedValueOnce(error); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"fatal error"`); - expect(logger.error).toHaveBeenCalledWith( - `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fatal error` - ); - }); - - it(`should update underlying settings and mappings of existing concrete indices if they exist`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + it(`should retry getting index on transient ES error`, async () => { + if (useDataStream) return; + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.statusCode = 404; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - }, - }, - }); + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.get).toHaveBeenCalledTimes(3); + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - }); - - it(`should retry simulateIndexTemplate on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockImplementation(async () => SimulateTemplateResponse); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if ES throws resource_already_exists_exception error and existing index is not the write index`, async () => { + if (useDataStream) return; - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(4); - }); - - it(`should retry getting alias on transient ES errors`, async () => { - clusterClient.indices.getAlias - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, + }, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, + }, + })); - expect(clusterClient.indices.getAlias).toHaveBeenCalledTimes(3); - }); - - it(`should retry settings update on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putSettings - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + const ccwiPromise = createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); - }); - - it(`should log and throw error on settings update if max retries exceeded`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putSettings.mockRejectedValue(new EsErrors.ConnectionError('foo')); - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(7); - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT index.mapping.total_fields.limit settings for alias alias_1: foo` - ); - }); - - it(`should log and throw error on settings update if ES throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putSettings.mockRejectedValue(new Error('generic error')); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT index.mapping.total_fields.limit settings for alias alias_1: generic error` - ); - }); - - it(`should retry mappings update on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putMapping - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + await expect(() => ccwiPromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias"` + ); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(4); - }); - - it(`should log and throw error on mappings update if max retries exceeded`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putMapping.mockRejectedValue(new EsErrors.ConnectionError('foo')); - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(7); - expect(logger.error).toHaveBeenCalledWith(`Failed to PUT mapping for alias alias_1: foo`); - }); - - it(`should log and throw error on mappings update if ES throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putMapping.mockRejectedValue(new Error('generic error')); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT mapping for alias alias_1: generic error` - ); - }); - - it(`should log and return when simulating updated mappings throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); - expect(logger.error).toHaveBeenCalledWith( - `Ignored PUT mappings for alias alias_1; error generating simulated mappings: fail` - ); + it(`should call esClient to put index template if get alias throws 404`, async () => { + const error = new Error(`not found`) as EsError; + error.statusCode = 404; + clusterClient.indices.getAlias.mockRejectedValueOnce(error); + clusterClient.indices.getDataStream.mockRejectedValueOnce(error); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledWith({ + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); - - it(`should log and return when simulating updated mappings returns null`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { ...SimulateTemplateResponse.template, mappings: null }, - })); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if get alias throws non-404 error`, async () => { + const error = new Error(`fatal error`) as EsError; + error.statusCode = 500; + clusterClient.indices.getAlias.mockRejectedValueOnce(error); + clusterClient.indices.getDataStream.mockRejectedValueOnce(error); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"fatal error"`); + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error fetching data stream for .alerts-test.alerts-default - fatal error` + : `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fatal error` + ); + }); - expect(logger.error).toHaveBeenCalledWith( - `Ignored PUT mappings for alias alias_1; simulated mappings were empty` - ); + it(`should update underlying settings and mappings of existing concrete indices if they exist`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (!useDataStream) { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + }); + + it(`should retry simulateIndexTemplate on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => SimulateTemplateResponse); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 3 : 4 + ); + }); + + it(`should retry getting alias on transient ES errors`, async () => { + clusterClient.indices.getAlias + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.getDataStream).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.getAlias).toHaveBeenCalledTimes(3); + } + }); + + it(`should retry settings update on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 3 : 4); + }); + + it(`should log and throw error on settings update if max retries exceeded`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings.mockRejectedValue(new EsErrors.ConnectionError('foo')); + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 4 : 7); + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT index.mapping.total_fields.limit settings for .alerts-test.alerts-default: foo` + : `Failed to PUT index.mapping.total_fields.limit settings for alias_1: foo` + ); + }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + it(`should log and throw error on settings update if ES throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings.mockRejectedValue(new Error('generic error')); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT index.mapping.total_fields.limit settings for .alerts-test.alerts-default: generic error` + : `Failed to PUT index.mapping.total_fields.limit settings for alias_1: generic error` + ); + }); + + it(`should retry mappings update on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putMapping + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 3 : 4); + }); + + it(`should log and throw error on mappings update if max retries exceeded`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putMapping.mockRejectedValue(new EsErrors.ConnectionError('foo')); + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 4 : 7); + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT mapping for .alerts-test.alerts-default: foo` + : `Failed to PUT mapping for alias_1: foo` + ); + }); + + it(`should log and throw error on mappings update if ES throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putMapping.mockRejectedValue(new Error('generic error')); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT mapping for .alerts-test.alerts-default: generic error` + : `Failed to PUT mapping for alias_1: generic error` + ); + }); + + it(`should log and return when simulating updated mappings throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Ignored PUT mappings for .alerts-test.alerts-default; error generating simulated mappings: fail` + : `Ignored PUT mappings for alias_1; error generating simulated mappings: fail` + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + }); + + it(`should log and return when simulating updated mappings returns null`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { ...SimulateTemplateResponse.template, mappings: null }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Ignored PUT mappings for .alerts-test.alerts-default; simulated mappings were empty` + : `Ignored PUT mappings for alias_1; simulated mappings were empty` + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + }); + + it(`should throw error when there are concrete indices but none of them are the write index`, async () => { + if (useDataStream) return; + + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-0001': { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: false, + is_hidden: true, + }, + alias_2: { + is_write_index: false, + is_hidden: true, + }, + }, }, - }, - }, + })); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default"` + ); + }); }); - }); - - it(`should throw error when there are concrete indices but none of them are the write index`, async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-0001': { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: false, - is_hidden: true, - }, - alias_2: { - is_write_index: false, - is_hidden: true, - }, - }, - }, - })); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default"` - ); - }); + } }); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts index 31aface312913..4e66f5f9b0a4e 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts @@ -10,8 +10,9 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { get } from 'lodash'; import { IIndexPatternString } from '../resource_installer_utils'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { DataStreamAdapter, getDataStreamAdapter } from './data_stream_adapter'; -interface ConcreteIndexInfo { +export interface ConcreteIndexInfo { index: string; alias: string; isWriteIndex: boolean; @@ -50,7 +51,7 @@ const updateTotalFieldLimitSetting = async ({ return; } catch (err) { logger.error( - `Failed to PUT index.mapping.total_fields.limit settings for alias ${alias}: ${err.message}` + `Failed to PUT index.mapping.total_fields.limit settings for ${alias}: ${err.message}` ); throw err; } @@ -74,7 +75,7 @@ const updateUnderlyingMapping = async ({ ); } catch (err) { logger.error( - `Ignored PUT mappings for alias ${alias}; error generating simulated mappings: ${err.message}` + `Ignored PUT mappings for ${alias}; error generating simulated mappings: ${err.message}` ); return; } @@ -82,7 +83,7 @@ const updateUnderlyingMapping = async ({ const simulatedMapping = get(simulatedIndexMapping, ['template', 'mappings']); if (simulatedMapping == null) { - logger.error(`Ignored PUT mappings for alias ${alias}; simulated mappings were empty`); + logger.error(`Ignored PUT mappings for ${alias}; simulated mappings were empty`); return; } @@ -94,20 +95,22 @@ const updateUnderlyingMapping = async ({ return; } catch (err) { - logger.error(`Failed to PUT mapping for alias ${alias}: ${err.message}`); + logger.error(`Failed to PUT mapping for ${alias}: ${err.message}`); throw err; } }; /** * Updates the underlying mapping for any existing concrete indices */ -const updateIndexMappings = async ({ +export const updateIndexMappings = async ({ logger, esClient, totalFieldsLimit, concreteIndices, }: UpdateIndexMappingsOpts) => { - logger.debug(`Updating underlying mappings for ${concreteIndices.length} indices.`); + logger.debug( + `Updating underlying mappings for ${concreteIndices.length} indices / data streams.` + ); // Update total field limit setting of found indices // Other index setting changes are not updated at this time @@ -125,11 +128,12 @@ const updateIndexMappings = async ({ ); }; -interface CreateConcreteWriteIndexOpts { +export interface CreateConcreteWriteIndexOpts { logger: Logger; esClient: ElasticsearchClient; totalFieldsLimit: number; indexPatterns: IIndexPatternString; + dataStreamAdapter?: DataStreamAdapter; } /** * Installs index template that uses installed component template @@ -137,107 +141,14 @@ interface CreateConcreteWriteIndexOpts { * conflicts. Simulate should return an empty mapping if a template * conflicts with an already installed template. */ -export const createConcreteWriteIndex = async ({ - logger, - esClient, - indexPatterns, - totalFieldsLimit, -}: CreateConcreteWriteIndexOpts) => { - logger.info(`Creating concrete write index - ${indexPatterns.name}`); +export const createConcreteWriteIndex = async (opts: CreateConcreteWriteIndexOpts) => { + const { logger, indexPatterns } = opts; - // check if a concrete write index already exists - let concreteIndices: ConcreteIndexInfo[] = []; - try { - // Specify both the index pattern for the backing indices and their aliases - // The alias prevents the request from finding other namespaces that could match the -* pattern - const response = await retryTransientEsErrors( - () => - esClient.indices.getAlias({ - index: indexPatterns.pattern, - name: indexPatterns.basePattern, - }), - { logger } - ); - - concreteIndices = Object.entries(response).flatMap(([index, { aliases }]) => - Object.entries(aliases).map(([aliasName, aliasProperties]) => ({ - index, - alias: aliasName, - isWriteIndex: aliasProperties.is_write_index ?? false, - })) - ); + // use the alias data stream adapter as "legacy" default + const dataStreamAdapter = + opts.dataStreamAdapter || getDataStreamAdapter({ useDataStreamForAlerts: false }); - logger.debug( - `Found ${concreteIndices.length} concrete indices for ${ - indexPatterns.name - } - ${JSON.stringify(concreteIndices)}` - ); - } catch (error) { - // 404 is expected if no concrete write indices have been created - if (error.statusCode !== 404) { - logger.error( - `Error fetching concrete indices for ${indexPatterns.pattern} pattern - ${error.message}` - ); - throw error; - } - } - - let concreteWriteIndicesExist = false; - // if a concrete write index already exists, update the underlying mapping - if (concreteIndices.length > 0) { - await updateIndexMappings({ logger, esClient, totalFieldsLimit, concreteIndices }); - - const concreteIndicesExist = concreteIndices.some( - (index) => index.alias === indexPatterns.alias - ); - concreteWriteIndicesExist = concreteIndices.some( - (index) => index.alias === indexPatterns.alias && index.isWriteIndex - ); - - // If there are some concrete indices but none of them are the write index, we'll throw an error - // because one of the existing indices should have been the write target. - if (concreteIndicesExist && !concreteWriteIndicesExist) { - throw new Error( - `Indices matching pattern ${indexPatterns.pattern} exist but none are set as the write index for alias ${indexPatterns.alias}` - ); - } - } - - // check if a concrete write index already exists - if (!concreteWriteIndicesExist) { - try { - await retryTransientEsErrors( - () => - esClient.indices.create({ - index: indexPatterns.name, - body: { - aliases: { - [indexPatterns.alias]: { - is_write_index: true, - }, - }, - }, - }), - { logger } - ); - } catch (error) { - logger.error(`Error creating concrete write index - ${error.message}`); - // If the index already exists and it's the write index for the alias, - // something else created it so suppress the error. If it's not the write - // index, that's bad, throw an error. - if (error?.meta?.body?.error?.type === 'resource_already_exists_exception') { - const existingIndices = await retryTransientEsErrors( - () => esClient.indices.get({ index: indexPatterns.name }), - { logger } - ); - if (!existingIndices[indexPatterns.name]?.aliases?.[indexPatterns.alias]?.is_write_index) { - throw Error( - `Attempted to create index: ${indexPatterns.name} as the write index for alias: ${indexPatterns.alias}, but the index already exists and is not the write index for the alias` - ); - } - } else { - throw error; - } - } - } + const label = dataStreamAdapter.isUsingDataStreams() ? 'data stream' : 'concrete write index'; + logger.info(`Creating ${label} - ${indexPatterns.name}`); + await dataStreamAdapter.createStream(opts); }; diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts index e47bc92eb5ae0..8cacdb1e97563 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts @@ -7,10 +7,12 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { createOrUpdateIlmPolicy } from './create_or_update_ilm_policy'; +import { getDataStreamAdapter } from './data_stream_adapter'; const randomDelayMultiplier = 0.01; const logger = loggingSystemMock.createLogger(); const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; +const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: false }); const IlmPolicy = { _meta: { @@ -40,6 +42,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }); expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith({ @@ -58,6 +61,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }); expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(3); @@ -71,6 +75,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); @@ -87,6 +92,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts index d1c50b7474436..dfc967aa974d6 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts @@ -8,12 +8,14 @@ import { IlmPolicy } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { DataStreamAdapter } from './data_stream_adapter'; interface CreateOrUpdateIlmPolicyOpts { logger: Logger; esClient: ElasticsearchClient; name: string; policy: IlmPolicy; + dataStreamAdapter: DataStreamAdapter; } /** * Creates ILM policy if it doesn't already exist, updates it if it does @@ -23,7 +25,10 @@ export const createOrUpdateIlmPolicy = async ({ esClient, name, policy, + dataStreamAdapter, }: CreateOrUpdateIlmPolicyOpts) => { + if (dataStreamAdapter.isUsingDataStreams()) return; + logger.info(`Installing ILM policy ${name}`); try { diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts index d4ce203a0d0e3..2714f99c79fbc 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts @@ -7,12 +7,14 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { getIndexTemplate, createOrUpdateIndexTemplate } from './create_or_update_index_template'; +import { createDataStreamAdapterMock } from './data_stream_adapter.mock'; +import { DataStreamAdapter } from './data_stream_adapter'; const randomDelayMultiplier = 0.01; const logger = loggingSystemMock.createLogger(); const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; -const IndexTemplate = (namespace: string = 'default') => ({ +const IndexTemplate = (namespace: string = 'default', useDataStream: boolean = false) => ({ name: `.alerts-test.alerts-${namespace}-index-template`, body: { _meta: { @@ -38,10 +40,14 @@ const IndexTemplate = (namespace: string = 'default') => ({ settings: { auto_expand_replicas: '0-1', hidden: true, - 'index.lifecycle': { - name: 'test-ilm-policy', - rollover_alias: `.alerts-test.alerts-${namespace}`, - }, + ...(useDataStream + ? {} + : { + 'index.lifecycle': { + name: 'test-ilm-policy', + rollover_alias: `.alerts-test.alerts-${namespace}`, + }, + }), 'index.mapping.total_fields.limit': 2500, }, }, @@ -65,7 +71,20 @@ const SimulateTemplateResponse = { }; describe('getIndexTemplate', () => { + let dataStreamAdapter: DataStreamAdapter; + let useDataStream: boolean; + + beforeEach(() => { + dataStreamAdapter = createDataStreamAdapterMock(); + useDataStream = dataStreamAdapter.isUsingDataStreams(); + }); + it(`should create index template with given parameters in default namespace`, () => { + dataStreamAdapter.getIndexTemplateFields = jest.fn().mockReturnValue({ + index_patterns: ['.internal.alerts-test.alerts-default-*'], + rollover_alias: '.alerts-test.alerts-default', + }); + expect( getIndexTemplate({ kibanaVersion: '8.6.1', @@ -80,11 +99,17 @@ describe('getIndexTemplate', () => { namespace: 'default', componentTemplateRefs: ['mappings1', 'framework-mappings'], totalFieldsLimit: 2500, + dataStreamAdapter, }) ).toEqual(IndexTemplate()); }); it(`should create index template with given parameters in custom namespace`, () => { + dataStreamAdapter.getIndexTemplateFields = jest.fn().mockReturnValue({ + index_patterns: ['.internal.alerts-test.alerts-another-space-*'], + rollover_alias: '.alerts-test.alerts-another-space', + }); + expect( getIndexTemplate({ kibanaVersion: '8.6.1', @@ -99,8 +124,9 @@ describe('getIndexTemplate', () => { namespace: 'another-space', componentTemplateRefs: ['mappings1', 'framework-mappings'], totalFieldsLimit: 2500, + dataStreamAdapter, }) - ).toEqual(IndexTemplate('another-space')); + ).toEqual(IndexTemplate('another-space', useDataStream)); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts index a17fad2d875ed..bbe2ba666f1a9 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts @@ -14,6 +14,7 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { isEmpty } from 'lodash'; import { IIndexPatternString } from '../resource_installer_utils'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { DataStreamAdapter } from './data_stream_adapter'; interface GetIndexTemplateOpts { componentTemplateRefs: string[]; @@ -22,6 +23,7 @@ interface GetIndexTemplateOpts { kibanaVersion: string; namespace: string; totalFieldsLimit: number; + dataStreamAdapter: DataStreamAdapter; } export const getIndexTemplate = ({ @@ -31,6 +33,7 @@ export const getIndexTemplate = ({ kibanaVersion, namespace, totalFieldsLimit, + dataStreamAdapter, }: GetIndexTemplateOpts): IndicesPutIndexTemplateRequest => { const indexMetadata: Metadata = { kibana: { @@ -40,19 +43,27 @@ export const getIndexTemplate = ({ namespace, }; + const dataStreamFields = dataStreamAdapter.getIndexTemplateFields( + indexPatterns.alias, + indexPatterns.pattern + ); + + const indexLifecycle = { + name: ilmPolicyName, + rollover_alias: dataStreamFields.rollover_alias, + }; + return { name: indexPatterns.template, body: { - index_patterns: [indexPatterns.pattern], + ...(dataStreamFields.data_stream ? { data_stream: dataStreamFields.data_stream } : {}), + index_patterns: dataStreamFields.index_patterns, composed_of: componentTemplateRefs, template: { settings: { auto_expand_replicas: '0-1', hidden: true, - 'index.lifecycle': { - name: ilmPolicyName, - rollover_alias: indexPatterns.alias, - }, + ...(dataStreamAdapter.isUsingDataStreams() ? {} : { 'index.lifecycle': indexLifecycle }), 'index.mapping.total_fields.limit': totalFieldsLimit, }, mappings: { diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts new file mode 100644 index 0000000000000..8de9f7bcc1731 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts @@ -0,0 +1,18 @@ +/* + * 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 { DataStreamAdapter, GetDataStreamAdapterOpts } from './data_stream_adapter'; + +export function createDataStreamAdapterMock(opts?: GetDataStreamAdapterOpts): DataStreamAdapter { + return { + isUsingDataStreams: jest.fn().mockReturnValue(false), + getIndexTemplateFields: jest.fn().mockReturnValue({ + index_patterns: ['index-pattern'], + }), + createStream: jest.fn(), + }; +} diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts new file mode 100644 index 0000000000000..89d1704095fad --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts @@ -0,0 +1,224 @@ +/* + * 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. + */ + +// eslint-disable-next-line max-classes-per-file +import { + CreateConcreteWriteIndexOpts, + ConcreteIndexInfo, + updateIndexMappings, +} from './create_concrete_write_index'; +import { retryTransientEsErrors } from './retry_transient_es_errors'; + +export interface DataStreamAdapter { + isUsingDataStreams(): boolean; + getIndexTemplateFields(alias: string, pattern: string): IndexTemplateFields; + createStream(opts: CreateConcreteWriteIndexOpts): Promise; +} + +export interface BulkOpProperties { + require_alias: boolean; +} + +export interface IndexTemplateFields { + data_stream?: { hidden: true }; + index_patterns: string[]; + rollover_alias?: string; +} + +export interface GetDataStreamAdapterOpts { + useDataStreamForAlerts: boolean; +} + +export function getDataStreamAdapter(opts: GetDataStreamAdapterOpts): DataStreamAdapter { + if (opts.useDataStreamForAlerts) { + return new DataStreamImplementation(); + } else { + return new AliasImplementation(); + } +} + +// implementation using data streams +class DataStreamImplementation implements DataStreamAdapter { + isUsingDataStreams(): boolean { + return true; + } + + getIndexTemplateFields(alias: string, pattern: string): IndexTemplateFields { + return { + data_stream: { hidden: true }, + index_patterns: [alias], + }; + } + + async createStream(opts: CreateConcreteWriteIndexOpts): Promise { + return createDataStream(opts); + } +} + +// implementation using aliases and backing indices +class AliasImplementation implements DataStreamAdapter { + isUsingDataStreams(): boolean { + return false; + } + + getIndexTemplateFields(alias: string, pattern: string): IndexTemplateFields { + return { + index_patterns: [pattern], + rollover_alias: alias, + }; + } + + async createStream(opts: CreateConcreteWriteIndexOpts): Promise { + return createAliasStream(opts); + } +} + +async function createDataStream(opts: CreateConcreteWriteIndexOpts): Promise { + const { logger, esClient, indexPatterns, totalFieldsLimit } = opts; + logger.info(`Creating data stream - ${indexPatterns.alias}`); + + // check if data stream exists + let dataStreamExists = false; + try { + const response = await retryTransientEsErrors( + () => esClient.indices.getDataStream({ name: indexPatterns.alias, expand_wildcards: 'all' }), + { logger } + ); + dataStreamExists = response.data_streams.length > 0; + } catch (error) { + if (error?.statusCode !== 404) { + logger.error(`Error fetching data stream for ${indexPatterns.alias} - ${error.message}`); + throw error; + } + } + + // if a data stream exists, update the underlying mapping + if (dataStreamExists) { + await updateIndexMappings({ + logger, + esClient, + totalFieldsLimit, + concreteIndices: [ + { alias: indexPatterns.alias, index: indexPatterns.alias, isWriteIndex: true }, + ], + }); + } else { + try { + await retryTransientEsErrors( + () => + esClient.indices.createDataStream({ + name: indexPatterns.alias, + }), + { logger } + ); + } catch (error) { + logger.error(`Error creating data stream ${indexPatterns.alias} - ${error.message}`); + throw error; + } + } +} + +async function createAliasStream(opts: CreateConcreteWriteIndexOpts): Promise { + const { logger, esClient, indexPatterns, totalFieldsLimit } = opts; + logger.info(`Creating concrete write index - ${indexPatterns.name}`); + + // check if a concrete write index already exists + let concreteIndices: ConcreteIndexInfo[] = []; + try { + // Specify both the index pattern for the backing indices and their aliases + // The alias prevents the request from finding other namespaces that could match the -* pattern + const response = await retryTransientEsErrors( + () => + esClient.indices.getAlias({ + index: indexPatterns.pattern, + name: indexPatterns.basePattern, + }), + { logger } + ); + + concreteIndices = Object.entries(response).flatMap(([index, { aliases }]) => + Object.entries(aliases).map(([aliasName, aliasProperties]) => ({ + index, + alias: aliasName, + isWriteIndex: aliasProperties.is_write_index ?? false, + })) + ); + + logger.debug( + `Found ${concreteIndices.length} concrete indices for ${ + indexPatterns.name + } - ${JSON.stringify(concreteIndices)}` + ); + } catch (error) { + // 404 is expected if no concrete write indices have been created + if (error.statusCode !== 404) { + logger.error( + `Error fetching concrete indices for ${indexPatterns.pattern} pattern - ${error.message}` + ); + throw error; + } + } + + let concreteWriteIndicesExist = false; + // if a concrete write index already exists, update the underlying mapping + if (concreteIndices.length > 0) { + await updateIndexMappings({ logger, esClient, totalFieldsLimit, concreteIndices }); + + const concreteIndicesExist = concreteIndices.some( + (index) => index.alias === indexPatterns.alias + ); + concreteWriteIndicesExist = concreteIndices.some( + (index) => index.alias === indexPatterns.alias && index.isWriteIndex + ); + + // If there are some concrete indices but none of them are the write index, we'll throw an error + // because one of the existing indices should have been the write target. + if (concreteIndicesExist && !concreteWriteIndicesExist) { + throw new Error( + `Indices matching pattern ${indexPatterns.pattern} exist but none are set as the write index for alias ${indexPatterns.alias}` + ); + } + } + + // check if a concrete write index already exists + if (!concreteWriteIndicesExist) { + try { + await retryTransientEsErrors( + () => + esClient.indices.create({ + index: indexPatterns.name, + body: { + aliases: { + [indexPatterns.alias]: { + is_write_index: true, + }, + }, + }, + }), + { logger } + ); + } catch (error) { + logger.error(`Error creating concrete write index - ${error.message}`); + // If the index already exists and it's the write index for the alias, + // something else created it so suppress the error. If it's not the write + // index, that's bad, throw an error. + if (error?.meta?.body?.error?.type === 'resource_already_exists_exception') { + const existingIndices = await retryTransientEsErrors( + () => esClient.indices.get({ index: indexPatterns.name }), + { logger } + ); + if (!existingIndices[indexPatterns.name]?.aliases?.[indexPatterns.alias]?.is_write_index) { + throw Error( + `Attempted to create index: ${indexPatterns.name} as the write index for alias: ${indexPatterns.alias}, but the index already exists and is not the write index for the alias` + ); + } + } else { + throw error; + } + } + } +} diff --git a/x-pack/plugins/alerting/server/config.test.ts b/x-pack/plugins/alerting/server/config.test.ts index 7df579771a91c..777d72efc699b 100644 --- a/x-pack/plugins/alerting/server/config.test.ts +++ b/x-pack/plugins/alerting/server/config.test.ts @@ -36,6 +36,7 @@ describe('config validation', () => { }, }, }, + "useDataStreamForAlerts": false, } `); }); diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index b1b9817ce1b9f..81ac0a0ce8308 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -66,6 +66,13 @@ export const configSchema = schema.object({ enableFrameworkAlerts: schema.boolean({ defaultValue: true }), cancelAlertsOnRuleTimeout: schema.boolean({ defaultValue: true }), rules: rulesSchema, + useDataStreamForAlerts: schema.boolean({ defaultValue: false }), + // useDataStreamForAlerts: schema.conditional( + // schema.contextRef('serverless'), + // true, + // schema.boolean({ defaultValue: true }), + // schema.never() + // ), }); export type AlertingConfig = TypeOf; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index ebfe1586031fa..ae42c665d73cf 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -32,6 +32,7 @@ export type { ExecutorType, IRuleTypeAlerts, GetViewInAppRelativeUrlFnOpts, + DataStreamAdapter, } from './types'; export { RuleNotifyWhen } from '../common'; export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; @@ -64,6 +65,7 @@ export { createConcreteWriteIndex, installWithTimeout, } from './alerts_service'; +export { getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index a4902fccb4f04..f6870ed228300 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -35,6 +35,7 @@ const createSetupMock = () => { enabled: jest.fn(), getContextInitializationPromise: jest.fn(), }, + getDataStreamAdapter: jest.fn(), }; return mock; }; @@ -189,3 +190,5 @@ export const alertsMock = { export const ruleMonitoringServiceMock = { create: createRuleMonitoringServiceMock }; export const ruleLastRunServiceMock = { create: createRuleLastRunServiceMock }; + +export { createDataStreamAdapterMock } from './alerts_service/lib/data_stream_adapter.mock'; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index e14ba71c6c1bf..0c15227370499 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -36,30 +36,7 @@ jest.mock('./alerts_service/alerts_service', () => ({ })); import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; - -const generateAlertingConfig = (): AlertingConfig => ({ - healthCheck: { - interval: '5m', - }, - enableFrameworkAlerts: false, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 10, - cancelAlertsOnRuleTimeout: true, - rules: { - minimumScheduleInterval: { value: '1m', enforce: false }, - run: { - actions: { - max: 1000, - }, - alerts: { - max: 1000, - }, - }, - }, -}); +import { generateAlertingConfig } from './test_utils'; const sampleRuleType: RuleType = { id: 'test', @@ -105,7 +82,8 @@ describe('Alerting Plugin', () => { plugin = new AlertingPlugin(context); // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() - await plugin.setup(setupMocks, mockPlugins); + plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); expect(setupMocks.status.set).toHaveBeenCalledTimes(1); expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); @@ -123,7 +101,8 @@ describe('Alerting Plugin', () => { const usageCollectionSetup = createUsageCollectionSetupMock(); // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() - await plugin.setup(setupMocks, { ...mockPlugins, usageCollection: usageCollectionSetup }); + plugin.setup(setupMocks, { ...mockPlugins, usageCollection: usageCollectionSetup }); + await waitForSetupComplete(setupMocks); expect(usageCollectionSetup.createUsageCounter).toHaveBeenCalled(); expect(usageCollectionSetup.registerCollector).toHaveBeenCalled(); @@ -137,7 +116,8 @@ describe('Alerting Plugin', () => { plugin = new AlertingPlugin(context); // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() - const setupContract = await plugin.setup(setupMocks, mockPlugins); + const setupContract = plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); expect(AlertsService).toHaveBeenCalled(); @@ -150,7 +130,8 @@ describe('Alerting Plugin', () => { ); plugin = new AlertingPlugin(context); - const setupContract = await plugin.setup(setupMocks, mockPlugins); + const setupContract = plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); expect(setupContract.getConfig()).toEqual({ isUsingSecurity: false, @@ -167,7 +148,8 @@ describe('Alerting Plugin', () => { generateAlertingConfig() ); plugin = new AlertingPlugin(context); - setup = await plugin.setup(setupMocks, mockPlugins); + setup = plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); }); it('should throw error when license type is invalid', async () => { @@ -428,3 +410,20 @@ function mockFeatures() { ]); return features; } + +type CoreSetupMocks = ReturnType; + +const WaitForSetupAttempts = 10; +const WaitForSetupDelay = 200; +const WaitForSetupSeconds = (WaitForSetupAttempts * WaitForSetupDelay) / 1000; + +export async function waitForSetupComplete(setupMocks: CoreSetupMocks) { + let attempts = 0; + while (setupMocks.status.set.mock.calls.length < 1) { + attempts++; + await new Promise((resolve) => setTimeout(resolve, WaitForSetupDelay)); + if (attempts > WaitForSetupAttempts) { + throw new Error(`setupMocks.status.set was not called within ${WaitForSetupSeconds} seconds`); + } + } +} diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 9e81e1f53e87d..3fa652851c917 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -97,6 +97,7 @@ import { } from './alerts_service'; import { rulesSettingsFeature } from './rules_settings_feature'; import { maintenanceWindowFeature } from './maintenance_window_feature'; +import { DataStreamAdapter, getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -138,6 +139,7 @@ export interface PluginSetupContract { getSecurityHealth: () => Promise; getConfig: () => AlertingRulesConfig; frameworkAlerts: PublicFrameworkAlertsService; + getDataStreamAdapter: () => DataStreamAdapter; } export interface PluginStartContract { @@ -205,6 +207,7 @@ export class AlertingPlugin { private inMemoryMetrics: InMemoryMetrics; private alertsService: AlertsService | null; private pluginStop$: Subject; + private dataStreamAdapter: DataStreamAdapter; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -219,6 +222,7 @@ export class AlertingPlugin { this.kibanaVersion = initializerContext.env.packageInfo.version; this.inMemoryMetrics = new InMemoryMetrics(initializerContext.logger.get('in_memory_metrics')); this.pluginStop$ = new ReplaySubject(1); + this.dataStreamAdapter = getDataStreamAdapter(this.config); } public setup( @@ -264,6 +268,7 @@ export class AlertingPlugin { logger: this.logger, pluginStop$: this.pluginStop$, kibanaVersion: this.kibanaVersion, + config: this.config, elasticsearchClientPromise: core .getStartServices() .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser), @@ -414,6 +419,7 @@ export class AlertingPlugin { return Promise.resolve(errorResult(`Framework alerts service not available`)); }, }, + getDataStreamAdapter: () => this.dataStreamAdapter, }; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts index 73d60259b8610..de4292e9fe9fc 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts @@ -68,6 +68,7 @@ import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock'; import { AlertsService } from '../alerts_service'; import { ReplaySubject } from 'rxjs'; import { IAlertsClient } from '../alerts_client/types'; +import { generateAlertingConfig } from '../test_utils'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -327,6 +328,7 @@ describe('Task Runner', () => { pluginStop$: new ReplaySubject(1), kibanaVersion: '8.8.0', elasticsearchClientPromise: Promise.resolve(clusterClient), + config: generateAlertingConfig(), }); const spy = jest .spyOn(alertsService, 'getContextInitializationPromise') @@ -423,6 +425,7 @@ describe('Task Runner', () => { pluginStop$: new ReplaySubject(1), kibanaVersion: '8.8.0', elasticsearchClientPromise: Promise.resolve(clusterClient), + config: generateAlertingConfig(), }); const spy = jest .spyOn(alertsService, 'getContextInitializationPromise') diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerting/server/test_utils/index.ts index 589dae529cee6..553a44b152520 100644 --- a/x-pack/plugins/alerting/server/test_utils/index.ts +++ b/x-pack/plugins/alerting/server/test_utils/index.ts @@ -6,6 +6,7 @@ */ import { RawAlertInstance } from '../../common'; +import { AlertingConfig } from '../config'; interface Resolvable { resolve: (arg: T) => void; @@ -45,3 +46,30 @@ export function alertsWithAnyUUID( } return newAlerts; } + +export function generateAlertingConfig(useDataStreamForAlerts = false): AlertingConfig { + return { + healthCheck: { + interval: '5m', + }, + enableFrameworkAlerts: false, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + maxEphemeralActionsPerAlert: 10, + cancelAlertsOnRuleTimeout: true, + rules: { + minimumScheduleInterval: { value: '1m', enforce: false }, + run: { + actions: { + max: 1000, + }, + alerts: { + max: 1000, + }, + }, + }, + useDataStreamForAlerts, + }; +} diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index c7e8294759657..bee42c98dc075 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -468,3 +468,5 @@ export interface RawRule extends SavedObjectAttributes { revision: number; running?: boolean | null; } + +export type { DataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 42ebef4c41f6f..fcd962e0f88be 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -100,6 +100,8 @@ export class RuleRegistryPlugin this.security = plugins.security; + const dataStreamAdapter = plugins.alerting.getDataStreamAdapter(); + this.ruleDataService = new RuleDataService({ logger, kibanaVersion, @@ -112,6 +114,7 @@ export class RuleRegistryPlugin }, frameworkAlerts: plugins.alerting.frameworkAlerts, pluginStop$: this.pluginStop$, + dataStreamAdapter, }); this.ruleDataService.initializeService(); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts index 751c21a08cf8d..dc6470c4739ce 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts @@ -44,5 +44,6 @@ export const createRuleDataClientMock = ( bulk, }) ), + isUsingDataStreams: jest.fn(() => false), }; }; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts index 9c280cbcd51a8..3d798b0104c71 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts @@ -55,6 +55,7 @@ function getRuleDataClientOptions({ waitUntilReadyForWriting: waitUntilReadyForWriting ?? Promise.resolve(right(scopedClusterClient) as WaitResult), logger, + isUsingDataStreams: false, }; } @@ -106,6 +107,7 @@ describe('RuleDataClient', () => { body: query, ignore_unavailable: true, index: `.alerts-observability.apm.alerts*`, + seq_no_primary_term: true, }); }); @@ -128,6 +130,7 @@ describe('RuleDataClient', () => { body: query, ignore_unavailable: true, index: `.alerts-observability.apm.alerts-test`, + seq_no_primary_term: true, }); }); @@ -345,9 +348,13 @@ describe('RuleDataClient', () => { await delay(); await expect(() => writer.bulk({})).rejects.toThrowErrorMatchingInlineSnapshot( - `"something went wrong!"` + `"error writing to index: something went wrong!"` + ); + expect(logger.error).toHaveBeenNthCalledWith( + 1, + 'error writing to index: something went wrong!', + error ); - expect(logger.error).toHaveBeenNthCalledWith(1, error); expect(ruleDataClient.isWriteEnabled()).toBe(true); }); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts index b56bc41efd292..0c2e0941a1ea6 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts @@ -32,6 +32,7 @@ export interface RuleDataClientConstructorOptions { waitUntilReadyForReading: Promise; waitUntilReadyForWriting: Promise; logger: Logger; + isUsingDataStreams: boolean; } export type WaitResult = Either; @@ -39,6 +40,7 @@ export type WaitResult = Either; export class RuleDataClient implements IRuleDataClient { private _isWriteEnabled: boolean = false; private _isWriterCacheEnabled: boolean = true; + private _isUsingDataStreams: boolean = false; // Writers cached by namespace private writerCache: Map; @@ -48,6 +50,7 @@ export class RuleDataClient implements IRuleDataClient { constructor(private readonly options: RuleDataClientConstructorOptions) { this.writeEnabled = this.options.isWriteEnabled; this.writerCacheEnabled = this.options.isWriterCacheEnabled; + this._isUsingDataStreams = this.options.isUsingDataStreams; this.writerCache = new Map(); } @@ -83,6 +86,10 @@ export class RuleDataClient implements IRuleDataClient { this._isWriterCacheEnabled = isEnabled; } + public isUsingDataStreams(): boolean { + return this._isUsingDataStreams; + } + public getReader(options: { namespace?: string } = {}): IRuleDataReader { const { indexInfo } = this.options; const indexPattern = indexInfo.getPatternForReading(options.namespace); @@ -109,6 +116,7 @@ export class RuleDataClient implements IRuleDataClient { ...request, index: indexPattern, ignore_unavailable: true, + seq_no_primary_term: true, })) as unknown as ESSearchResponse; } catch (err) { this.options.logger.error(`Error performing search in RuleDataClient - ${err.message}`); @@ -215,7 +223,7 @@ export class RuleDataClient implements IRuleDataClient { if (this.clusterClient) { const requestWithDefaultParameters = { ...request, - require_alias: true, + require_alias: !this._isUsingDataStreams, index: alias, }; @@ -223,17 +231,18 @@ export class RuleDataClient implements IRuleDataClient { meta: true, }); + // TODO: #160572 - add support for version conflict errors, in case alert was updated + // some other way between the time it was fetched and the time it was updated. if (response.body.errors) { - const error = new errors.ResponseError(response); - this.options.logger.error(error); + throw new errors.ResponseError(response); } return response; } else { this.options.logger.debug(`Writing is disabled, bulk() will not write any data.`); } } catch (error) { - this.options.logger.error(error); - throw error; + this.options.logger.error(`error writing to index: ${error.message}`, error); + throw new Error(`error writing to index: ${error.message}`, { cause: error }); } }, }; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index dc8199c1e2963..a7da8069739f4 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -18,6 +18,7 @@ export interface IRuleDataClient { indexNameWithNamespace(namespace: string): string; kibanaVersion: string; isWriteEnabled(): boolean; + isUsingDataStreams(): boolean; getReader(options?: { namespace?: string }): IRuleDataReader; getWriter(options?: { namespace?: string }): Promise; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts index b27b90713c99e..a6fc03568f28f 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts @@ -13,6 +13,9 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { Dataset } from './index_options'; import { IndexInfo } from './index_info'; import { ECS_COMPONENT_TEMPLATE_NAME } from '@kbn/alerting-plugin/server'; +import type { DataStreamAdapter } from '@kbn/alerting-plugin/server'; +import { getDataStreamAdapter } from '@kbn/alerting-plugin/server/alerts_service/lib/data_stream_adapter'; + import { elasticsearchServiceMock, ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../common/assets'; @@ -23,9 +26,11 @@ const frameworkAlertsService = { describe('resourceInstaller', () => { let pluginStop$: Subject; + let dataStreamAdapter: DataStreamAdapter; beforeEach(() => { pluginStop$ = new ReplaySubject(1); + dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: false }); }); afterEach(() => { @@ -45,6 +50,7 @@ describe('resourceInstaller', () => { getClusterClient, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); installer.installCommonResources(); expect(getClusterClient).not.toHaveBeenCalled(); @@ -62,6 +68,7 @@ describe('resourceInstaller', () => { getClusterClient, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); const indexOptions = { feature: AlertConsumers.LOGS, @@ -93,6 +100,7 @@ describe('resourceInstaller', () => { getClusterClient, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); await installer.installCommonResources(); @@ -123,6 +131,7 @@ describe('resourceInstaller', () => { enabled: () => true, }, pluginStop$, + dataStreamAdapter, }); await installer.installCommonResources(); @@ -148,6 +157,7 @@ describe('resourceInstaller', () => { getClusterClient, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); const indexOptions = { @@ -183,6 +193,7 @@ describe('resourceInstaller', () => { enabled: () => true, }, pluginStop$, + dataStreamAdapter, }); const indexOptions = { @@ -239,6 +250,7 @@ describe('resourceInstaller', () => { getClusterClient, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); const indexOptions = { @@ -290,6 +302,7 @@ describe('resourceInstaller', () => { getContextInitializationPromise: async () => ({ result: true }), }, pluginStop$, + dataStreamAdapter, }); const indexOptions = { @@ -326,6 +339,7 @@ describe('resourceInstaller', () => { enabled: () => true, }, pluginStop$, + dataStreamAdapter, }); const indexOptions = { @@ -367,6 +381,7 @@ describe('resourceInstaller', () => { getContextInitializationPromise: async () => ({ result: true }), }, pluginStop$, + dataStreamAdapter, }); const indexOptions = { @@ -440,6 +455,7 @@ describe('resourceInstaller', () => { getClusterClient: async () => mockClusterClient, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }; const indexOptions = { feature: AlertConsumers.OBSERVABILITY, @@ -496,10 +512,10 @@ describe('resourceInstaller', () => { expect(errorMessages).toMatchInlineSnapshot(` Array [ Array [ - "Ignored PUT mappings for alias alias_1; error generating simulated mappings: expecting simulateIndexTemplate() to throw", + "Ignored PUT mappings for alias_1; error generating simulated mappings: expecting simulateIndexTemplate() to throw", ], Array [ - "Ignored PUT mappings for alias alias_2; error generating simulated mappings: expecting simulateIndexTemplate() to throw", + "Ignored PUT mappings for alias_2; error generating simulated mappings: expecting simulateIndexTemplate() to throw", ], ] `); @@ -522,10 +538,10 @@ describe('resourceInstaller', () => { expect(errorMessages).toMatchInlineSnapshot(` Array [ Array [ - "Ignored PUT mappings for alias alias_1; simulated mappings were empty", + "Ignored PUT mappings for alias_1; simulated mappings were empty", ], Array [ - "Ignored PUT mappings for alias alias_2; simulated mappings were empty", + "Ignored PUT mappings for alias_2; simulated mappings were empty", ], ] `); diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 2956552bd78d2..225df2ffe1b89 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -20,6 +20,7 @@ import { installWithTimeout, TOTAL_FIELDS_LIMIT, type PublicFrameworkAlertsService, + type DataStreamAdapter, } from '@kbn/alerting-plugin/server'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../common/assets'; import { technicalComponentTemplate } from '../../common/assets/component_templates/technical_component_template'; @@ -34,6 +35,7 @@ interface ConstructorOptions { disabledRegistrationContexts: string[]; frameworkAlerts: PublicFrameworkAlertsService; pluginStop$: Observable; + dataStreamAdapter: DataStreamAdapter; } export type IResourceInstaller = PublicMethodsOf; @@ -78,6 +80,7 @@ export class ResourceInstaller { esClient: clusterClient, name: DEFAULT_ALERTS_ILM_POLICY_NAME, policy: DEFAULT_ALERTS_ILM_POLICY, + dataStreamAdapter: this.options.dataStreamAdapter, }), createOrUpdateComponentTemplate({ logger, @@ -143,6 +146,7 @@ export class ResourceInstaller { esClient: clusterClient, name: indexInfo.getIlmPolicyName(), policy: ilmPolicy, + dataStreamAdapter: this.options.dataStreamAdapter, }); } @@ -245,6 +249,7 @@ export class ResourceInstaller { kibanaVersion: indexInfo.kibanaVersion, namespace, totalFieldsLimit: TOTAL_FIELDS_LIMIT, + dataStreamAdapter: this.options.dataStreamAdapter, }), }); @@ -253,6 +258,7 @@ export class ResourceInstaller { esClient: clusterClient, totalFieldsLimit: TOTAL_FIELDS_LIMIT, indexPatterns, + dataStreamAdapter: this.options.dataStreamAdapter, }); } } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts index 2ba9146f4f2db..b2736ee7f3cfe 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts @@ -14,6 +14,9 @@ import { Dataset } from './index_options'; import { RuleDataClient } from '../rule_data_client/rule_data_client'; import { createRuleDataClientMock as mockCreateRuleDataClient } from '../rule_data_client/rule_data_client.mock'; +import { createDataStreamAdapterMock } from '@kbn/alerting-plugin/server/mocks'; +import type { DataStreamAdapter } from '@kbn/alerting-plugin/server'; + jest.mock('../rule_data_client/rule_data_client', () => ({ RuleDataClient: jest.fn().mockImplementation(() => mockCreateRuleDataClient()), })); @@ -25,10 +28,12 @@ const frameworkAlertsService = { describe('ruleDataPluginService', () => { let pluginStop$: Subject; + let dataStreamAdapter: DataStreamAdapter; beforeEach(() => { jest.resetAllMocks(); pluginStop$ = new ReplaySubject(1); + dataStreamAdapter = createDataStreamAdapterMock(); }); afterEach(() => { @@ -50,6 +55,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); expect(ruleDataService.isRegistrationContextDisabled('observability.logs')).toBe(true); }); @@ -67,6 +73,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); expect(ruleDataService.isRegistrationContextDisabled('observability.apm')).toBe(false); }); @@ -86,6 +93,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); expect(ruleDataService.isWriteEnabled('observability.logs')).toBe(false); @@ -106,6 +114,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); const indexOptions = { feature: AlertConsumers.LOGS, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts index 62f8cc88ca221..b17b10d5b7d26 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts @@ -11,7 +11,7 @@ import type { ValidFeatureId } from '@kbn/rule-data-utils'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { type PublicFrameworkAlertsService } from '@kbn/alerting-plugin/server'; +import type { PublicFrameworkAlertsService, DataStreamAdapter } from '@kbn/alerting-plugin/server'; import { INDEX_PREFIX } from '../config'; import { type IRuleDataClient, RuleDataClient, WaitResult } from '../rule_data_client'; import { IndexInfo } from './index_info'; @@ -94,6 +94,7 @@ interface ConstructorOptions { disabledRegistrationContexts: string[]; frameworkAlerts: PublicFrameworkAlertsService; pluginStop$: Observable; + dataStreamAdapter: DataStreamAdapter; } export class RuleDataService implements IRuleDataService { @@ -116,6 +117,7 @@ export class RuleDataService implements IRuleDataService { isWriteEnabled: options.isWriteEnabled, frameworkAlerts: options.frameworkAlerts, pluginStop$: options.pluginStop$, + dataStreamAdapter: options.dataStreamAdapter, }); this.installCommonResources = Promise.resolve(right('ok')); @@ -222,6 +224,7 @@ export class RuleDataService implements IRuleDataService { waitUntilReadyForReading, waitUntilReadyForWriting, logger: this.options.logger, + isUsingDataStreams: this.options.dataStreamAdapter.isUsingDataStreams(), }); } diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 993405dd33e1f..7e8e0ac73f907 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -97,7 +97,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert documents - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -105,7 +105,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], }), - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -120,7 +120,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // evaluation documents - { index: {} }, + { create: {} }, expect.objectContaining({ [EVENT_KIND]: 'event', }), @@ -151,6 +151,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -168,6 +171,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 1, + _primary_term: 3, }, ], }, @@ -222,7 +228,15 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { + index: { + _id: 'TEST_ALERT_0_UUID', + _index: 'alerts-index-name', + if_primary_term: 2, + if_seq_no: 4, + require_alias: false, + }, + }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_WORKFLOW_STATUS]: 'closed', @@ -232,7 +246,15 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { + index: { + _id: 'TEST_ALERT_1_UUID', + _index: 'alerts-index-name', + if_primary_term: 3, + if_seq_no: 1, + require_alias: false, + }, + }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_WORKFLOW_STATUS]: 'open', @@ -279,6 +301,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc [TAGS]: ['source-tag1', 'source-tag2'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -296,6 +321,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc [TAGS]: ['source-tag3', 'source-tag4'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -347,7 +375,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -356,7 +384,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'close', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -510,6 +538,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -527,6 +558,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -622,6 +656,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -639,6 +676,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -733,6 +773,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -749,6 +792,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -841,6 +887,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -857,6 +906,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -957,7 +1009,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert documents - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -966,7 +1018,7 @@ describe('createLifecycleExecutor', () => { [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], [ALERT_MAINTENANCE_WINDOW_IDS]: maintenanceWindowIds, }), - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -1013,6 +1065,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1030,6 +1085,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1086,7 +1144,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_WORKFLOW_STATUS]: 'closed', @@ -1095,7 +1153,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1141,6 +1199,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc [TAGS]: ['source-tag1', 'source-tag2'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1158,6 +1219,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc [TAGS]: ['source-tag3', 'source-tag4'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1210,7 +1274,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -1219,7 +1283,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'close', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -1269,6 +1333,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'closed', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1285,6 +1352,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1301,6 +1371,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1317,6 +1390,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1432,7 +1508,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_WORKFLOW_STATUS]: 'closed', @@ -1441,7 +1517,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1450,7 +1526,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: false, }), - { index: { _id: 'TEST_ALERT_2_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1459,7 +1535,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: true, }), - { index: { _id: 'TEST_ALERT_3_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1493,6 +1569,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1508,6 +1587,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1523,6 +1605,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1538,6 +1623,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1637,7 +1725,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -1645,7 +1733,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: false, }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -1653,7 +1741,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: false, }), - { index: { _id: 'TEST_ALERT_2_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -1661,7 +1749,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: true, }), - { index: { _id: 'TEST_ALERT_3_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index ce2570f7e5bcc..c72e810ce2d81 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -216,10 +216,14 @@ export const createLifecycleExecutor = `[Rule Registry] Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` ); - const trackedAlertsDataMap: Record< - string, - { indexName: string; fields: Partial } - > = {}; + interface TrackedAlertData { + indexName: string; + fields: Partial; + seqNo: number | undefined; + primaryTerm: number | undefined; + } + + const trackedAlertsDataMap: Record = {}; if (trackedAlertStates.length) { const result = await fetchExistingAlerts( @@ -230,10 +234,18 @@ export const createLifecycleExecutor = result.forEach((hit) => { const alertInstanceId = hit._source ? hit._source[ALERT_INSTANCE_ID] : void 0; if (alertInstanceId && hit._source) { - trackedAlertsDataMap[alertInstanceId] = { - indexName: hit._index, - fields: hit._source, - }; + if (hit._seq_no == null) { + logger.error(`missing _seq_no on alert instance ${alertInstanceId}`); + } else if (hit._primary_term == null) { + logger.error(`missing _primary_term on alert instance ${alertInstanceId}`); + } else { + trackedAlertsDataMap[alertInstanceId] = { + indexName: hit._index, + fields: hit._source, + seqNo: hit._seq_no, + primaryTerm: hit._primary_term, + }; + } } }); } @@ -308,6 +320,8 @@ export const createLifecycleExecutor = return { indexName: alertData?.indexName, + seqNo: alertData?.seqNo, + primaryTerm: alertData?.primaryTerm, event, flappingHistory, flapping, @@ -335,10 +349,22 @@ export const createLifecycleExecutor = logger.debug(`[Rule Registry] Preparing to index ${allEventsToIndex.length} alerts.`); await ruleDataClientWriter.bulk({ - body: allEventsToIndex.flatMap(({ event, indexName }) => [ + body: allEventsToIndex.flatMap(({ event, indexName, seqNo, primaryTerm }) => [ indexName - ? { index: { _id: event[ALERT_UUID]!, _index: indexName, require_alias: false } } - : { index: { _id: event[ALERT_UUID]! } }, + ? { + index: { + _id: event[ALERT_UUID]!, + _index: indexName, + if_seq_no: seqNo, + if_primary_term: primaryTerm, + require_alias: false, + }, + } + : { + create: { + _id: event[ALERT_UUID]!, + }, + }, event, ]), refresh: 'wait_for', diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 971b4ec735086..bbdd4806b55e7 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -16,7 +16,6 @@ import { } from '@kbn/rule-data-utils'; import { loggerMock } from '@kbn/logging-mocks'; import { castArray, omit } from 'lodash'; -import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_factory'; import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; @@ -30,7 +29,7 @@ function createRule(shouldWriteAlerts: boolean = true) { const ruleDataClientMock = createRuleDataClientMock(); const factory = createLifecycleRuleTypeFactory({ - ruleDataClient: ruleDataClientMock as unknown as RuleDataClient, + ruleDataClient: ruleDataClientMock, logger: loggerMock.create(), }); @@ -227,7 +226,7 @@ describe('createLifecycleRuleTypeFactory', () => { const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[0][0].body!; - const documents = body.filter((op: any) => !('index' in op)) as any[]; + const documents: any[] = body.filter((op: any) => !isOpDoc(op)); const evaluationDocuments = documents.filter((doc) => doc['event.kind'] === 'event'); const alertDocuments = documents.filter((doc) => doc['event.kind'] === 'signal'); @@ -347,9 +346,10 @@ describe('createLifecycleRuleTypeFactory', () => { ).bulk.mock.calls[0][0].body ?.concat() .reverse() - .find( - (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' - ) as Record; + .find((doc: any) => !isOpDoc(doc) && doc['service.name'] === 'opbeans-node') as Record< + string, + any + >; // @ts-ignore 4.3.5 upgrade helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ @@ -390,7 +390,7 @@ describe('createLifecycleRuleTypeFactory', () => { expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(2); const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[1][0].body!; - const documents = body.filter((op: any) => !('index' in op)) as any[]; + const documents: any[] = body.filter((op: any) => !isOpDoc(op)); const evaluationDocuments = documents.filter((doc) => doc['event.kind'] === 'event'); const alertDocuments = documents.filter((doc) => doc['event.kind'] === 'signal'); @@ -429,13 +429,16 @@ describe('createLifecycleRuleTypeFactory', () => { ).bulk.mock.calls[0][0].body ?.concat() .reverse() - .find( - (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' - ) as Record; + .find((doc: any) => !isOpDoc(doc) && doc['service.name'] === 'opbeans-node') as Record< + string, + any + >; helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ hits: { - hits: [{ _source: lastOpbeansNodeDoc } as any], + hits: [ + { _source: lastOpbeansNodeDoc, _index: 'a', _primary_term: 4, _seq_no: 2 } as any, + ], total: { value: 1, relation: 'eq', @@ -465,7 +468,7 @@ describe('createLifecycleRuleTypeFactory', () => { const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[1][0].body!; - const documents = body.filter((op: any) => !('index' in op)) as any[]; + const documents: any[] = body.filter((op: any) => !isOpDoc(op)); const opbeansJavaAlertDoc = documents.find( (doc) => castArray(doc['service.name'])[0] === 'opbeans-java' @@ -487,3 +490,9 @@ describe('createLifecycleRuleTypeFactory', () => { }); }); }); + +function isOpDoc(doc: any) { + if (doc?.index?._id) return true; + if (doc?.create?._id) return true; + return false; +} diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts index 3d38293626e16..e3f9e5c24240e 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts @@ -9,6 +9,7 @@ import { createOrUpdateComponentTemplate, createOrUpdateIlmPolicy, createOrUpdateIndexTemplate, + getDataStreamAdapter, } from '@kbn/alerting-plugin/server'; import { loggingSystemMock, @@ -59,6 +60,7 @@ jest.mock('@kbn/alerting-plugin/server', () => ({ createOrUpdateComponentTemplate: jest.fn(), createOrUpdateIlmPolicy: jest.fn(), createOrUpdateIndexTemplate: jest.fn(), + getDataStreamAdapter: jest.fn(), })); jest.mock('./utils/create_datastream', () => ({ @@ -114,6 +116,8 @@ describe('RiskEngineDataClient', () => { it('should initialize risk engine resources', async () => { await riskEngineDataClient.initializeResources({ namespace: 'default' }); + expect(getDataStreamAdapter).toHaveBeenCalledWith({ useDataStreamForAlerts: false }); + expect(createOrUpdateIlmPolicy).toHaveBeenCalledWith({ logger, esClient, diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts index f338686f3ceac..4d887d3c6f602 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts @@ -12,11 +12,11 @@ import { createOrUpdateComponentTemplate, createOrUpdateIlmPolicy, createOrUpdateIndexTemplate, + getDataStreamAdapter, } from '@kbn/alerting-plugin/server'; import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import type { Logger, ElasticsearchClient } from '@kbn/core/server'; - import { riskScoreFieldMap, getIndexPattern, @@ -217,6 +217,8 @@ export class RiskEngineDataClient { esClient, name: ilmPolicyName, policy: ilmPolicy, + // this will change w/serverless, since it doesn't have ILM + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), }), createOrUpdateComponentTemplate({ logger: this.options.logger, diff --git a/x-pack/test/rule_registry/common/config.ts b/x-pack/test/rule_registry/common/config.ts index 703e71a8613b3..9f2f17523509d 100644 --- a/x-pack/test/rule_registry/common/config.ts +++ b/x-pack/test/rule_registry/common/config.ts @@ -17,6 +17,7 @@ interface CreateTestConfigOptions { disabledPlugins?: string[]; ssl?: boolean; testFiles?: string[]; + useDataStreamForAlerts?: boolean; } // test.not-enabled is specifically not enabled @@ -80,6 +81,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.alerting.rules.minimumScheduleInterval.value="1s"`, + `--xpack.alerting.useDataStreamForAlerts=${options.useDataStreamForAlerts ?? false}`, '--xpack.eventLog.logEntries=true', ...disabledPlugins .filter((k) => k !== 'security') diff --git a/x-pack/test/rule_registry/common/lib/helpers/cleanup_registry_indices.ts b/x-pack/test/rule_registry/common/lib/helpers/cleanup_registry_indices.ts index 82652e5726fcf..9bae7fbf9daf5 100644 --- a/x-pack/test/rule_registry/common/lib/helpers/cleanup_registry_indices.ts +++ b/x-pack/test/rule_registry/common/lib/helpers/cleanup_registry_indices.ts @@ -7,16 +7,37 @@ import expect from '@kbn/expect'; import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; + import { GetService } from '../../types'; +import { isUsingDataStreamForAlerts } from './use_data_stream_for_alerts'; export const cleanupRegistryIndices = async (getService: GetService, client: IRuleDataClient) => { const es = getService('es'); + const useDataStream = isUsingDataStreamForAlerts(getService); + const dataStreams = await es.indices.getDataStream({ + name: '.alerts-*', + expand_wildcards: 'all', + include_defaults: true, + }); + const dataStreamNames = dataStreams.data_streams.map((dataStream) => dataStream.name); + if (useDataStream) { + expect(dataStreamNames.length).to.be.greaterThan(0); + await es.indices.deleteDataStream({ name: dataStreamNames }, { ignore: [404] }); + } else { + expect(dataStreamNames.length).to.be(0); + } + const aliasMap = await es.indices.get({ index: `${client.indexName}*`, allow_no_indices: true, expand_wildcards: 'open', }); const indices = Object.keys(aliasMap); - expect(indices.length > 0).to.be(true); - return es.indices.delete({ index: indices }, { ignore: [404] }); + + if (useDataStream) { + expect(indices.length).to.be(0); + } else { + expect(indices.length).to.be.greaterThan(0); + await es.indices.delete({ index: indices }, { ignore: [404] }); + } }; diff --git a/x-pack/test/rule_registry/common/lib/helpers/index.ts b/x-pack/test/rule_registry/common/lib/helpers/index.ts index 25128a5807320..2c53374cc1cff 100644 --- a/x-pack/test/rule_registry/common/lib/helpers/index.ts +++ b/x-pack/test/rule_registry/common/lib/helpers/index.ts @@ -14,3 +14,4 @@ export * from './cleanup_target_indices'; export * from './cleanup_registry_indices'; export * from './delete_alert'; export * from './mock_alert_factory'; +export * from './use_data_stream_for_alerts'; diff --git a/x-pack/test/rule_registry/common/lib/helpers/use_data_stream_for_alerts.ts b/x-pack/test/rule_registry/common/lib/helpers/use_data_stream_for_alerts.ts new file mode 100644 index 0000000000000..f9035fe3260cd --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/use_data_stream_for_alerts.ts @@ -0,0 +1,18 @@ +/* + * 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 { GetService } from '../../types'; + +export function isUsingDataStreamForAlerts(getService: GetService): boolean { + const configService = getService('config'); + + const serverArgStrings: string[] = configService.getAll().kbnTestServer.serverArgs; + const serverArgs = new Set(serverArgStrings); + const result = serverArgs.has('--xpack.alerting.useDataStreamForAlerts=true'); + + return result; +} diff --git a/x-pack/test/rule_registry/spaces_only/config_basic.ts b/x-pack/test/rule_registry/spaces_only/config_trial_datastream.ts similarity index 82% rename from x-pack/test/rule_registry/spaces_only/config_basic.ts rename to x-pack/test/rule_registry/spaces_only/config_trial_datastream.ts index 5a2ee4c1c1178..7ab7c708597a2 100644 --- a/x-pack/test/rule_registry/spaces_only/config_basic.ts +++ b/x-pack/test/rule_registry/spaces_only/config_trial_datastream.ts @@ -9,8 +9,9 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export export default createTestConfig('spaces_only', { - license: 'basic', + license: 'trial', disabledPlugins: ['security'], ssl: false, - testFiles: [require.resolve('./tests/basic')], + testFiles: [require.resolve('./tests/trial')], + useDataStreamForAlerts: true, }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts deleted file mode 100644 index 01be475d18132..0000000000000 --- a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('rule registry spaces only: trial', function () { - before(async () => { - await createSpaces(getService); - }); - - after(async () => { - await deleteSpaces(getService); - }); - }); -}; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts index 80334e09f6999..25ba57f46e364 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts @@ -29,6 +29,7 @@ import { } from '@kbn/rule-registry-plugin/server/utils/create_lifecycle_executor'; import { Dataset, IRuleDataClient, RuleDataService } from '@kbn/rule-registry-plugin/server'; import { RuleExecutorOptions } from '@kbn/alerting-plugin/server'; +import { getDataStreamAdapter } from '@kbn/alerting-plugin/server/alerts_service/lib/data_stream_adapter'; import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { MockRuleParams, @@ -37,13 +38,17 @@ import { MockAlertState, MockAllowedActionGroups, } from '../../../common/types'; -import { cleanupRegistryIndices, getMockAlertFactory } from '../../../common/lib/helpers'; +import { + cleanupRegistryIndices, + getMockAlertFactory, + isUsingDataStreamForAlerts, +} from '../../../common/lib/helpers'; // eslint-disable-next-line import/no-default-export export default function createLifecycleExecutorApiTest({ getService }: FtrProviderContext) { const es = getService('es'); - const log = getService('log'); + const useDataStreamForAlerts = isUsingDataStreamForAlerts(getService); const fakeLogger = (msg: string, meta?: Meta) => meta ? log.debug(msg, meta) : log.debug(msg); @@ -65,6 +70,8 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid return Promise.resolve(client); }; + const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts }); + describe('createLifecycleExecutor', () => { let ruleDataClient: IRuleDataClient; let pluginStop$: Subject; @@ -86,6 +93,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid getContextInitializationPromise: async () => ({ result: false }), }, pluginStop$, + dataStreamAdapter, }); // This initializes the service. This happens immediately after the creation @@ -201,6 +209,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid lookBackWindow: 20, statusChangeThreshold: 4, }, + dataStreamAdapter, } as unknown as RuleExecutorOptions< MockRuleParams, WrappedLifecycleRuleState, diff --git a/x-pack/test_serverless/api_integration/config.base.ts b/x-pack/test_serverless/api_integration/config.base.ts index a60591c4008cf..b2e2042874801 100644 --- a/x-pack/test_serverless/api_integration/config.base.ts +++ b/x-pack/test_serverless/api_integration/config.base.ts @@ -29,6 +29,7 @@ export function createTestConfig(options: CreateTestConfigOptions) { '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', '--xpack.observability.unsafe.thresholdRule.enabled=true', '--server.publicBaseUrl=https://localhost:5601', + `--xpack.alerting.useDataStreamForAlerts=true`, ], }, testFiles: options.testFiles,