[Response Ops][Alerting] Update alerts-as-data ILM policy name to be consistent between framework & rule registry (#150334)

Resolves https://github.com/elastic/kibana/issues/150331

## Summary

In a previous [PR](https://github.com/elastic/kibana/pull/145581) we
started installing an ILM policy for framework alerts as data when the
`xpack.alerting.enableFrameworkAlerts` config flag is set to true. In
that PR we used a different name than what is used by the rule registry
even though the policy bodies were the same.

In this PR, we are consolidating the naming of the two ILM policies so
that we are only ever installing 1 policy. The
`xpack.alerting.enableFrameworkAlerts` config is used to determine which
plugin is responsible for installing the policy. When set to true, the
alerting plugin installs the policy. When set the false, the rule
registry installs the policy. This is an incremental step toward the
alerting framework absorbing all of the resource installation
functionality of the rule registry

## To Verify

A few things to verify:

1. Verify that the alerting plugin installs the policy when
`xpack.alerting.enableFrameworkAlerts=true`
* Set `xpack.alerting.enableFrameworkAlerts: true` in your Kibana config
  * Start a fresh ES and Kibana instance
* Verify that an ILM policy with name `.alerts-ilm-policy` is installed
  * Create a metric threshold rule that creates an alert
* Verify that there is an index template called
`.alerts-observability.metrics.alerts-default-index-template` that uses
the `.alerts-ilm-policy` policy

2. Verify that the rule registry installs the policy when
`xpack.alerting.enableFrameworkAlerts=false`
* Set `xpack.alerting.enableFrameworkAlerts: false` in your Kibana
config
  * Start a fresh ES and Kibana instance
* Verify that an ILM policy with name `.alerts-ilm-policy` is installed
  * Create a metric threshold rule that creates an alert
* Verify that there is an index template called
`.alerts-observability.metrics.alerts-default-index-template` that uses
the `.alerts-ilm-policy` policy

3. Verify that we can switch between configurations
* Set `xpack.alerting.enableFrameworkAlerts: false` in your Kibana
config
  * Start a fresh ES and Kibana instance
* Verify that an ILM policy with name `.alerts-ilm-policy` is installed
  * Create a metric threshold rule that creates an alert
* Verify that there is an index template called
`.alerts-observability.metrics.alerts-default-index-template` that uses
the `.alerts-ilm-policy` policy
  * Change `xpack.alerting.enableFrameworkAlerts: true`
  * Restart Kibana
  * Verify there are no errors, and the rule can still write alerts

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2023-02-13 18:31:02 -05:00 committed by GitHub
parent 5e3056f4a0
commit e8c18a121e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 100 additions and 55 deletions

View file

@ -72,7 +72,7 @@ const IlmPutBody = {
},
},
},
name: 'alerts-default-ilm-policy',
name: '.alerts-ilm-policy',
};
const getIndexTemplatePutBody = (context?: string) => ({
@ -88,7 +88,7 @@ const getIndexTemplatePutBody = (context?: string) => ({
auto_expand_replicas: '0-1',
hidden: true,
'index.lifecycle': {
name: 'alerts-default-ilm-policy',
name: '.alerts-ilm-policy',
rollover_alias: `.alerts-${context ? context : 'test'}-default`,
},
'index.mapping.total_fields.limit': 2500,
@ -165,7 +165,7 @@ describe('Alerts Service', () => {
expect(alertsService.isInitialized()).toEqual(false);
expect(logger.error).toHaveBeenCalledWith(
`Error installing ILM policy alerts-default-ilm-policy - fail`
`Error installing ILM policy .alerts-ilm-policy - fail`
);
expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled();
@ -342,7 +342,7 @@ describe('Alerts Service', () => {
auto_expand_replicas: '0-1',
hidden: true,
'index.lifecycle': {
name: 'alerts-default-ilm-policy',
name: '.alerts-ilm-policy',
rollover_alias: `.alerts-empty-default`,
},
'index.mapping.total_fields.limit': 2500,

View file

@ -15,7 +15,10 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server';
import { firstValueFrom, Observable } from 'rxjs';
import { FieldMap } from '../../common/alert_schema/field_maps/types';
import { alertFieldMap } from '../../common/alert_schema';
import { ILM_POLICY_NAME, DEFAULT_ILM_POLICY } from './default_lifecycle_policy';
import {
DEFAULT_ALERTS_ILM_POLICY_NAME,
DEFAULT_ALERTS_ILM_POLICY,
} from './default_lifecycle_policy';
import {
getComponentTemplate,
getComponentTemplateName,
@ -178,19 +181,21 @@ export class AlertsService implements IAlertsService {
* Creates ILM policy if it doesn't already exist, updates it if it does
*/
private async createOrUpdateIlmPolicy(esClient: ElasticsearchClient) {
this.options.logger.info(`Installing ILM policy ${ILM_POLICY_NAME}`);
this.options.logger.info(`Installing ILM policy ${DEFAULT_ALERTS_ILM_POLICY_NAME}`);
try {
await retryTransientEsErrors(
() =>
esClient.ilm.putLifecycle({
name: ILM_POLICY_NAME,
body: DEFAULT_ILM_POLICY,
name: DEFAULT_ALERTS_ILM_POLICY_NAME,
body: DEFAULT_ALERTS_ILM_POLICY,
}),
{ logger: this.options.logger }
);
} catch (err) {
this.options.logger.error(`Error installing ILM policy ${ILM_POLICY_NAME} - ${err.message}`);
this.options.logger.error(
`Error installing ILM policy ${DEFAULT_ALERTS_ILM_POLICY_NAME} - ${err.message}`
);
throw err;
}
}
@ -236,7 +241,7 @@ export class AlertsService implements IAlertsService {
auto_expand_replicas: '0-1',
hidden: true,
'index.lifecycle': {
name: ILM_POLICY_NAME,
name: DEFAULT_ALERTS_ILM_POLICY_NAME,
rollover_alias: indexPatterns.alias,
},
'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT,

View file

@ -14,8 +14,8 @@
* This should be used by all alerts-as-data indices
*/
export const ILM_POLICY_NAME = 'alerts-default-ilm-policy';
export const DEFAULT_ILM_POLICY = {
export const DEFAULT_ALERTS_ILM_POLICY_NAME = '.alerts-ilm-policy';
export const DEFAULT_ALERTS_ILM_POLICY = {
policy: {
_meta: {
managed: true,

View file

@ -53,6 +53,10 @@ export {
WriteOperations,
AlertingAuthorizationEntity,
} from './authorization';
export {
DEFAULT_ALERTS_ILM_POLICY,
DEFAULT_ALERTS_ILM_POLICY_NAME,
} from './alerts_service/default_lifecycle_policy';
export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);

View file

@ -30,6 +30,7 @@ const createSetupMock = () => {
registerType: jest.fn(),
getSecurityHealth: jest.fn(),
getConfig: jest.fn(),
getFrameworkAlertsEnabled: jest.fn(),
};
return mock;
};

View file

@ -132,10 +132,12 @@ 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);
const setupContract = await plugin.setup(setupMocks, mockPlugins);
expect(AlertsService).toHaveBeenCalled();
expect(mockAlertService.initialize).toHaveBeenCalled();
expect(setupContract.getFrameworkAlertsEnabled()).toEqual(true);
});
it(`exposes configured minimumScheduleInterval()`, async () => {
@ -150,6 +152,8 @@ describe('Alerting Plugin', () => {
isUsingSecurity: false,
minimumScheduleInterval: { value: '1m', enforce: false },
});
expect(setupContract.getFrameworkAlertsEnabled()).toEqual(false);
});
describe('registerType()', () => {

View file

@ -126,6 +126,7 @@ export interface PluginSetupContract {
): void;
getSecurityHealth: () => Promise<SecurityHealth>;
getConfig: () => AlertingRulesConfig;
getFrameworkAlertsEnabled: () => boolean;
}
export interface PluginStartContract {
@ -385,6 +386,7 @@ export class AlertingPlugin {
isUsingSecurity: this.licenseState ? !!this.licenseState.getIsSecurityEnabled() : false,
};
},
getFrameworkAlertsEnabled: () => this.config.enableFrameworkAlerts,
};
}

View file

@ -7,4 +7,3 @@
export const TECHNICAL_COMPONENT_TEMPLATE_NAME = `technical-mappings`;
export const ECS_COMPONENT_TEMPLATE_NAME = `ecs-mappings`;
export const DEFAULT_ILM_POLICY_ID = 'ilm-policy';

View file

@ -1,24 +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.
*/
export const defaultLifecyclePolicy = {
policy: {
_meta: {
managed: true,
},
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_primary_shard_size: '50gb',
},
},
},
},
},
};

View file

@ -16,7 +16,10 @@ import type {
IContextProvider,
} from '@kbn/core/server';
import type { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/server';
import type {
PluginSetupContract as AlertingSetup,
PluginStartContract as AlertingStart,
} from '@kbn/alerting-plugin/server';
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/server';
import type {
@ -36,6 +39,7 @@ import { ruleRegistrySearchStrategyProvider, RULE_SEARCH_STRATEGY_NAME } from '.
export interface RuleRegistryPluginSetupDependencies {
security?: SecurityPluginSetup;
data: DataPluginSetup;
alerting: AlertingSetup;
}
export interface RuleRegistryPluginStartDependencies {
@ -106,6 +110,7 @@ export class RuleRegistryPlugin
const deps = await startDependencies;
return deps.core.elasticsearch.client.asInternalUser;
},
areFrameworkAlertsEnabled: plugins.alerting.getFrameworkAlertsEnabled(),
pluginStop$: this.pluginStop$,
});

View file

@ -14,7 +14,6 @@ import { Dataset } from './index_options';
import { IndexInfo } from './index_info';
import { elasticsearchServiceMock, ElasticsearchClientMock } from '@kbn/core/server/mocks';
import {
DEFAULT_ILM_POLICY_ID,
ECS_COMPONENT_TEMPLATE_NAME,
TECHNICAL_COMPONENT_TEMPLATE_NAME,
} from '../../common/assets';
@ -41,6 +40,7 @@ describe('resourceInstaller', () => {
disabledRegistrationContexts: [],
getResourceName: jest.fn(),
getClusterClient,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
installer.installCommonResources();
@ -57,6 +57,7 @@ describe('resourceInstaller', () => {
disabledRegistrationContexts: [],
getResourceName: jest.fn(),
getClusterClient,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
const indexOptions = {
@ -83,7 +84,6 @@ describe('resourceInstaller', () => {
const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient));
const getResourceNameMock = jest
.fn()
.mockReturnValueOnce(DEFAULT_ILM_POLICY_ID)
.mockReturnValueOnce(TECHNICAL_COMPONENT_TEMPLATE_NAME)
.mockReturnValueOnce(ECS_COMPONENT_TEMPLATE_NAME);
const installer = new ResourceInstaller({
@ -92,6 +92,7 @@ describe('resourceInstaller', () => {
disabledRegistrationContexts: [],
getResourceName: getResourceNameMock,
getClusterClient,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
@ -108,6 +109,38 @@ describe('resourceInstaller', () => {
expect.objectContaining({ name: ECS_COMPONENT_TEMPLATE_NAME })
);
});
it('should install common resources when framework alerts are enabled', async () => {
const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient();
const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient));
const getResourceNameMock = jest
.fn()
.mockReturnValueOnce(TECHNICAL_COMPONENT_TEMPLATE_NAME)
.mockReturnValueOnce(ECS_COMPONENT_TEMPLATE_NAME);
const installer = new ResourceInstaller({
logger: loggerMock.create(),
isWriteEnabled: true,
disabledRegistrationContexts: [],
getResourceName: getResourceNameMock,
getClusterClient,
areFrameworkAlertsEnabled: true,
pluginStop$,
});
await installer.installCommonResources();
// ILM policy should be handled by framework
expect(mockClusterClient.ilm.putLifecycle).not.toHaveBeenCalled();
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(2);
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ name: TECHNICAL_COMPONENT_TEMPLATE_NAME })
);
expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ name: ECS_COMPONENT_TEMPLATE_NAME })
);
});
it('should install index level resources', async () => {
const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient();
const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient));
@ -117,6 +150,7 @@ describe('resourceInstaller', () => {
disabledRegistrationContexts: [],
getResourceName: jest.fn(),
getClusterClient,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
@ -188,6 +222,7 @@ describe('resourceInstaller', () => {
disabledRegistrationContexts: [],
getResourceName: jest.fn(),
getClusterClient: async () => mockClusterClient,
areFrameworkAlertsEnabled: false,
pluginStop$,
};
const indexOptions = {

View file

@ -13,13 +13,15 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { PublicMethodsOf } from '@kbn/utility-types';
import {
DEFAULT_ILM_POLICY_ID,
DEFAULT_ALERTS_ILM_POLICY,
DEFAULT_ALERTS_ILM_POLICY_NAME,
} from '@kbn/alerting-plugin/server';
import {
ECS_COMPONENT_TEMPLATE_NAME,
TECHNICAL_COMPONENT_TEMPLATE_NAME,
} from '../../common/assets';
import { technicalComponentTemplate } from '../../common/assets/component_templates/technical_component_template';
import { ecsComponentTemplate } from '../../common/assets/component_templates/ecs_component_template';
import { defaultLifecyclePolicy } from '../../common/assets/lifecycle_policies/default_lifecycle_policy';
import type { IndexInfo } from './index_info';
@ -31,6 +33,7 @@ interface ConstructorOptions {
logger: Logger;
isWriteEnabled: boolean;
disabledRegistrationContexts: string[];
areFrameworkAlertsEnabled: boolean;
pluginStop$: Observable<void>;
}
@ -95,16 +98,21 @@ export class ResourceInstaller {
*/
public async installCommonResources(): Promise<void> {
await this.installWithTimeout('common resources shared between all indices', async () => {
const { getResourceName, logger } = this.options;
const { getResourceName, logger, areFrameworkAlertsEnabled } = this.options;
try {
// We can install them in parallel
await Promise.all([
this.createOrUpdateLifecyclePolicy({
name: getResourceName(DEFAULT_ILM_POLICY_ID),
body: defaultLifecyclePolicy,
}),
// Install ILM policy only if framework alerts are not enabled
// If framework alerts are enabled, the alerting framework will install this ILM policy
...(areFrameworkAlertsEnabled
? []
: [
this.createOrUpdateLifecyclePolicy({
name: DEFAULT_ALERTS_ILM_POLICY_NAME,
body: DEFAULT_ALERTS_ILM_POLICY,
}),
]),
this.createOrUpdateComponentTemplate({
name: getResourceName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
body: technicalComponentTemplate,
@ -326,9 +334,7 @@ export class ResourceInstaller {
const ownComponentNames = componentTemplates.map((template) =>
indexInfo.getComponentTemplateName(template.name)
);
const ilmPolicyName = ilmPolicy
? indexInfo.getIlmPolicyName()
: getResourceName(DEFAULT_ILM_POLICY_ID);
const ilmPolicyName = ilmPolicy ? indexInfo.getIlmPolicyName() : DEFAULT_ALERTS_ILM_POLICY_NAME;
const indexMetadata: estypes.Metadata = {
...indexTemplate._meta,

View file

@ -43,6 +43,7 @@ describe('ruleDataPluginService', () => {
isWriteEnabled: true,
disabledRegistrationContexts: ['observability.logs'],
isWriterCacheEnabled: true,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
expect(ruleDataService.isRegistrationContextDisabled('observability.logs')).toBe(true);
@ -59,6 +60,7 @@ describe('ruleDataPluginService', () => {
isWriteEnabled: true,
disabledRegistrationContexts: ['observability.logs'],
isWriterCacheEnabled: true,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
expect(ruleDataService.isRegistrationContextDisabled('observability.apm')).toBe(false);
@ -77,6 +79,7 @@ describe('ruleDataPluginService', () => {
isWriteEnabled: true,
disabledRegistrationContexts: ['observability.logs'],
isWriterCacheEnabled: true,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
@ -96,6 +99,7 @@ describe('ruleDataPluginService', () => {
isWriteEnabled: true,
disabledRegistrationContexts: ['observability.logs'],
isWriterCacheEnabled: true,
areFrameworkAlertsEnabled: false,
pluginStop$,
});
const indexOptions = {

View file

@ -91,6 +91,7 @@ interface ConstructorOptions {
isWriteEnabled: boolean;
isWriterCacheEnabled: boolean;
disabledRegistrationContexts: string[];
areFrameworkAlertsEnabled: boolean;
pluginStop$: Observable<void>;
}
@ -112,6 +113,7 @@ export class RuleDataService implements IRuleDataService {
logger: options.logger,
disabledRegistrationContexts: options.disabledRegistrationContexts,
isWriteEnabled: options.isWriteEnabled,
areFrameworkAlertsEnabled: options.areFrameworkAlertsEnabled,
pluginStop$: options.pluginStop$,
});

View file

@ -17,7 +17,7 @@ export default function createAlertsAsDataTest({ getService }: FtrProviderContex
describe('alerts as data', () => {
it('should install common alerts as data resources on startup', async () => {
const ilmPolicyName = 'alerts-default-ilm-policy';
const ilmPolicyName = '.alerts-ilm-policy';
const componentTemplateName = 'alerts-common-component-template';
const commonIlmPolicy = await es.ilm.getLifecycle({
@ -123,7 +123,7 @@ export default function createAlertsAsDataTest({ getService }: FtrProviderContex
expect(contextIndexTemplate.index_template.template!.settings).to.eql({
index: {
lifecycle: {
name: 'alerts-default-ilm-policy',
name: '.alerts-ilm-policy',
rollover_alias: '.alerts-test.always-firing-default',
},
mapping: {
@ -155,7 +155,7 @@ export default function createAlertsAsDataTest({ getService }: FtrProviderContex
});
expect(contextIndex[indexName].settings?.index?.lifecycle).to.eql({
name: 'alerts-default-ilm-policy',
name: '.alerts-ilm-policy',
rollover_alias: '.alerts-test.always-firing-default',
});

View file

@ -80,6 +80,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide
isWriteEnabled: true,
isWriterCacheEnabled: false,
disabledRegistrationContexts: [] as string[],
areFrameworkAlertsEnabled: false,
pluginStop$,
});

View file

@ -74,6 +74,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid
isWriteEnabled: true,
isWriterCacheEnabled: false,
disabledRegistrationContexts: [] as string[],
areFrameworkAlertsEnabled: false,
pluginStop$,
});