diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 2c7d540132139d..5bef3698d1f243 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -57,7 +57,7 @@ describe('checking migration metadata changes on all registered SO types', () => Object { "action": "cc93fe2c0c76e57c2568c63170e05daea897c136", "action_task_params": "96e27e7f4e8273ffcd87060221e2b75e81912dd5", - "alert": "dc710bc17dfc98a9a703d388569abccce5f8bf07", + "alert": "3a67d3f1db80af36bd57aaea47ecfef87e43c58f", "api_key_pending_invalidation": "1399e87ca37b3d3a65d269c924eda70726cfe886", "apm-custom-dashboards": "b67128f78160c288bd7efe25b2da6e2afd5e82fc", "apm-indices": "8a2d68d415a4b542b26b0d292034a28ffac6fed4", diff --git a/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts b/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts index 9a8109977962c0..32fac87b7485ba 100644 --- a/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts @@ -33,6 +33,7 @@ describe('createGetAlertIndicesAliasFn', () => { licensing: licensingMock.createSetup(), minimumScheduleInterval: { value: '1m', enforce: false }, inMemoryMetrics, + latestRuleVersion: 1, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); registry.register({ diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 03431c2804c6e1..64bc8ca05281f3 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -77,7 +77,7 @@ import { } from './types'; import { registerAlertingUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; -import { setupSavedObjects } from './saved_objects'; +import { setupSavedObjects, getLatestRuleVersion } from './saved_objects'; import { initializeApiKeyInvalidator, scheduleApiKeyInvalidatorTask, @@ -305,6 +305,7 @@ export class AlertingPlugin { alertsService: this.alertsService, minimumScheduleInterval: this.config.rules.minimumScheduleInterval, inMemoryMetrics: this.inMemoryMetrics, + latestRuleVersion: getLatestRuleVersion(), }); this.ruleTypeRegistry = ruleTypeRegistry; diff --git a/x-pack/plugins/alerting/server/rule_type_registry.mock.ts b/x-pack/plugins/alerting/server/rule_type_registry.mock.ts index 706484fdd92f64..0aa7e5b68b40cf 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.mock.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.mock.ts @@ -18,6 +18,7 @@ const createRuleTypeRegistryMock = () => { list: jest.fn(), getAllTypes: jest.fn(), ensureRuleTypeEnabled: jest.fn(), + getLatestRuleVersion: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 709533bb898f2f..2059f1e7548e62 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -40,6 +40,7 @@ beforeEach(() => { licensing: licensingMock.createSetup(), minimumScheduleInterval: { value: '1m', enforce: false }, inMemoryMetrics, + latestRuleVersion: 1, }; }); @@ -912,6 +913,16 @@ describe('Create Lifecycle', () => { ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); }); + + describe('getLatestRuleVersion', () => { + test('should return the latest rule version', async () => { + const ruleTypeRegistry = new RuleTypeRegistry({ + ...ruleTypeRegistryParams, + latestRuleVersion: 5, + }); + expect(ruleTypeRegistry.getLatestRuleVersion()).toBe(5); + }); + }); }); function ruleTypeWithVariables( diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index fd32e58335e388..2ed57e878291ad 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -14,7 +14,6 @@ import { Logger } from '@kbn/core/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { RunContext, TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { stateSchemaByVersion } from '@kbn/alerting-state-types'; -import { rawRuleSchema } from './raw_rule_schema'; import { TaskRunnerFactory } from './task_runner'; import { RuleType, @@ -40,6 +39,7 @@ import { AlertingRulesConfig } from '.'; import { AlertsService } from './alerts_service/alerts_service'; import { getRuleTypeIdValidLegacyConsumers } from './rule_type_registry_deprecated_consumers'; import { AlertingConfig } from './config'; +import { rawRuleSchemaV1 } from './saved_objects/schemas/raw_rule'; export interface ConstructorOptions { config: AlertingConfig; @@ -51,6 +51,7 @@ export interface ConstructorOptions { minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; inMemoryMetrics: InMemoryMetrics; alertsService: AlertsService | null; + latestRuleVersion: number; } export interface RegistryRuleType @@ -160,6 +161,7 @@ export class RuleTypeRegistry { private readonly licensing: LicensingPluginSetup; private readonly inMemoryMetrics: InMemoryMetrics; private readonly alertsService: AlertsService | null; + private readonly latestRuleVersion: number; constructor({ config, @@ -171,6 +173,7 @@ export class RuleTypeRegistry { minimumScheduleInterval, inMemoryMetrics, alertsService, + latestRuleVersion, }: ConstructorOptions) { this.config = config; this.logger = logger; @@ -181,6 +184,7 @@ export class RuleTypeRegistry { this.minimumScheduleInterval = minimumScheduleInterval; this.inMemoryMetrics = inMemoryMetrics; this.alertsService = alertsService; + this.latestRuleVersion = latestRuleVersion; } public has(id: string) { @@ -311,7 +315,7 @@ export class RuleTypeRegistry { spaceId: schema.string(), consumer: schema.maybe(schema.string()), }), - indirectParamsSchema: rawRuleSchema, + indirectParamsSchema: rawRuleSchemaV1, }, }); @@ -434,6 +438,10 @@ export class RuleTypeRegistry { public getAllTypes(): string[] { return [...this.ruleTypes.keys()]; } + + public getLatestRuleVersion() { + return this.latestRuleVersion; + } } function normalizedActionVariables(actionVariables: RuleType['actionVariables']) { diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index c16356193fd8d7..9006bf1cab1b61 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -24,10 +24,12 @@ import { getImportWarnings } from './get_import_warnings'; import { isRuleExportable } from './is_rule_exportable'; import { RuleTypeRegistry } from '../rule_type_registry'; export { partiallyUpdateAlert } from './partially_update_alert'; +export { getLatestRuleVersion, getMinimumCompatibleVersion } from './rule_model_versions'; import { RULES_SETTINGS_SAVED_OBJECT_TYPE, MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, } from '../../common'; +import { ruleModelVersions } from './rule_model_versions'; // Use caution when removing items from this array! Any field which has // ever existed in the rule SO must be included in this array to prevent @@ -106,6 +108,7 @@ export function setupSavedObjects( return isRuleExportable(ruleSavedObject, ruleTypeRegistry, logger); }, }, + modelVersions: ruleModelVersions, }); savedObjects.registerType({ diff --git a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts index 442dcf0a9469d5..bf330be87257a3 100644 --- a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts @@ -37,6 +37,7 @@ beforeEach(() => { licensing: licensingMock.createSetup(), minimumScheduleInterval: { value: '1m', enforce: false }, inMemoryMetrics, + latestRuleVersion: 1, }; }); diff --git a/x-pack/plugins/alerting/server/saved_objects/rule_model_versions.test.ts b/x-pack/plugins/alerting/server/saved_objects/rule_model_versions.test.ts new file mode 100644 index 00000000000000..9afcdaad8e2f4a --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/rule_model_versions.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { + CustomSavedObjectsModelVersionMap, + getLatestRuleVersion, + getMinimumCompatibleVersion, +} from './rule_model_versions'; +import { schema } from '@kbn/config-schema'; +import { RawRule } from '../types'; + +describe('rule model versions', () => { + const ruleModelVersions: CustomSavedObjectsModelVersionMap = { + '1': { + changes: [], + schemas: { + create: schema.object({ + name: schema.string(), + }), + }, + isCompatibleWithPreviousVersion: (rawRule) => true, + }, + '2': { + changes: [], + schemas: { + create: schema.object({ + name: schema.string(), + }), + }, + isCompatibleWithPreviousVersion: (rawRule) => false, + }, + '3': { + changes: [], + schemas: { + create: schema.object({ + name: schema.string(), + }), + }, + isCompatibleWithPreviousVersion: (rawRule) => rawRule.name === 'test', + }, + '4': { + changes: [], + schemas: { + create: schema.object({ + name: schema.string(), + }), + }, + isCompatibleWithPreviousVersion: (rawRule) => rawRule.name === 'test', + }, + }; + + const rawRule = { name: 'test' } as RawRule; + const mismatchingRawRule = { enabled: true } as RawRule; + + describe('getMinimumCompatibleVersion', () => { + it('should return the minimum compatible version for the matching rawRule', () => { + expect(getMinimumCompatibleVersion(ruleModelVersions, 1, rawRule)).toBe(1); + expect(getMinimumCompatibleVersion(ruleModelVersions, 2, rawRule)).toBe(2); + expect(getMinimumCompatibleVersion(ruleModelVersions, 3, rawRule)).toBe(2); + expect(getMinimumCompatibleVersion(ruleModelVersions, 4, rawRule)).toBe(2); + }); + it('should return the minimum compatible version for the mismatching rawRule', () => { + expect(getMinimumCompatibleVersion(ruleModelVersions, 3, mismatchingRawRule)).toBe(3); + expect(getMinimumCompatibleVersion(ruleModelVersions, 4, mismatchingRawRule)).toBe(4); + }); + }); + + describe('getLatestRuleVersion', () => { + it('should return the latest rule model version', () => { + expect(getLatestRuleVersion()).toBe(1); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/rule_model_versions.ts b/x-pack/plugins/alerting/server/saved_objects/rule_model_versions.ts new file mode 100644 index 00000000000000..38adc17389b233 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/rule_model_versions.ts @@ -0,0 +1,49 @@ +/* + * 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 { + SavedObjectsModelVersion, + SavedObjectsModelVersionMap, +} from '@kbn/core-saved-objects-server'; +import { RawRule } from '../types'; +import { rawRuleSchemaV1 } from './schemas/raw_rule'; + +interface CustomSavedObjectsModelVersion extends SavedObjectsModelVersion { + isCompatibleWithPreviousVersion: (param: RawRule) => boolean; +} + +export interface CustomSavedObjectsModelVersionMap extends SavedObjectsModelVersionMap { + [modelVersion: string]: CustomSavedObjectsModelVersion; +} + +export const ruleModelVersions: CustomSavedObjectsModelVersionMap = { + '1': { + changes: [], + schemas: { + create: rawRuleSchemaV1, + }, + isCompatibleWithPreviousVersion: (rawRule) => true, + }, +}; + +export const getLatestRuleVersion = () => Math.max(...Object.keys(ruleModelVersions).map(Number)); + +export function getMinimumCompatibleVersion( + modelVersions: CustomSavedObjectsModelVersionMap, + version: number, + rawRule: RawRule +): number { + if (version === 1) { + return 1; + } + + if (modelVersions[version].isCompatibleWithPreviousVersion(rawRule)) { + return getMinimumCompatibleVersion(modelVersions, version - 1, rawRule); + } + + return version; +} diff --git a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_rule/index.ts b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_rule/index.ts new file mode 100644 index 00000000000000..d5778bcda4a7a2 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_rule/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { rawRuleSchema as rawRuleSchemaV1 } from './v1'; diff --git a/x-pack/plugins/alerting/server/raw_rule_schema.ts b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_rule/v1.ts similarity index 97% rename from x-pack/plugins/alerting/server/raw_rule_schema.ts rename to x-pack/plugins/alerting/server/saved_objects/schemas/raw_rule/v1.ts index 4072b15b19210d..76c6241396dc30 100644 --- a/x-pack/plugins/alerting/server/raw_rule_schema.ts +++ b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_rule/v1.ts @@ -168,6 +168,7 @@ const rawRuleAlertsFilterSchema = schema.object({ key: schema.maybe(schema.string()), params: schema.maybe(schema.recordOf(schema.string(), schema.any())), // better type? value: schema.maybe(schema.string()), + field: schema.maybe(schema.string()), }), $state: schema.maybe( schema.object({ @@ -209,6 +210,7 @@ const rawRuleActionSchema = schema.object({ }) ), alertsFilter: schema.maybe(rawRuleAlertsFilterSchema), + useAlertDataForTemplate: schema.maybe(schema.boolean()), }); export const rawRuleSchema = schema.object({ @@ -266,5 +268,6 @@ export const rawRuleSchema = schema.object({ severity: schema.maybe(schema.string()), }) ), - params: schema.recordOf(schema.string(), schema.any()), + params: schema.recordOf(schema.string(), schema.maybe(schema.any())), + typeVersion: schema.maybe(schema.number()), });