[8.17] [ResponseOps][MW] Fix bug when creating repeating Maintenance Window (#207084) (#208312)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[ResponseOps][MW] Fix bug when creating repeating Maintenance Window
(#207084)](https://github.com/elastic/kibana/pull/207084)

<!--- Backport version: 9.6.4 -->

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

<!--BACKPORT
[{"author":{"name":"Antonio","email":"antonio.coelho@elastic.co"},"sourceCommit":{"committedDate":"2025-01-24T11:40:25Z","message":"[ResponseOps][MW]
Fix bug when creating repeating Maintenance Window (#207084)\n\nCloses
#198774\r\n\r\n## Summary\r\n\r\n- There was a bug when submitting
`rrule` with a `byweekday` I fixed\r\nthat validation to use a more
inclusive regex. `byweekday` can be the\r\nexpected `MO`, `TU`, etc but
also `-1FR` or `+3SA` where the number\r\ncorresponds to the week in a
month.\r\n- The model version for the maintenance window was incorrect
so when\r\nsaving the SO the validation was failing. I fixed that and
now we are\r\nallowed to save `number[]` as expected.\r\n- I removed
some duplicated code and we now use the `rrule` schema from\r\nthe
`common`
folder","sha":"0df78e629b429f6007f559aca339b4323b71e4c0","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Alerting","release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-major","v8.18.0"],"title":"[ResponseOps][MW]
Fix bug when creating repeating Maintenance
Window","number":207084,"url":"https://github.com/elastic/kibana/pull/207084","mergeCommit":{"message":"[ResponseOps][MW]
Fix bug when creating repeating Maintenance Window (#207084)\n\nCloses
#198774\r\n\r\n## Summary\r\n\r\n- There was a bug when submitting
`rrule` with a `byweekday` I fixed\r\nthat validation to use a more
inclusive regex. `byweekday` can be the\r\nexpected `MO`, `TU`, etc but
also `-1FR` or `+3SA` where the number\r\ncorresponds to the week in a
month.\r\n- The model version for the maintenance window was incorrect
so when\r\nsaving the SO the validation was failing. I fixed that and
now we are\r\nallowed to save `number[]` as expected.\r\n- I removed
some duplicated code and we now use the `rrule` schema from\r\nthe
`common`
folder","sha":"0df78e629b429f6007f559aca339b4323b71e4c0"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/207084","number":207084,"mergeCommit":{"message":"[ResponseOps][MW]
Fix bug when creating repeating Maintenance Window (#207084)\n\nCloses
#198774\r\n\r\n## Summary\r\n\r\n- There was a bug when submitting
`rrule` with a `byweekday` I fixed\r\nthat validation to use a more
inclusive regex. `byweekday` can be the\r\nexpected `MO`, `TU`, etc but
also `-1FR` or `+3SA` where the number\r\ncorresponds to the week in a
month.\r\n- The model version for the maintenance window was incorrect
so when\r\nsaving the SO the validation was failing. I fixed that and
now we are\r\nallowed to save `number[]` as expected.\r\n- I removed
some duplicated code and we now use the `rrule` schema from\r\nthe
`common`
folder","sha":"0df78e629b429f6007f559aca339b4323b71e4c0"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/208181","number":208181,"state":"MERGED","mergeCommit":{"sha":"8759f0286675744937e4ed8f5ad4880e35f6547e","message":"[8.x]
[ResponseOps][MW] Fix bug when creating repeating Maintenance Window
(#207084) (#208181)\n\n# Backport\r\n\r\nThis will backport the
following commits from `main` to `8.x`:\r\n- [[ResponseOps][MW] Fix bug
when creating repeating Maintenance
Window\r\n(#207084)](https://github.com/elastic/kibana/pull/207084)\r\n\r\n<!---
Backport version: 9.6.4 -->\r\n\r\n### Questions ?\r\nPlease refer to
the [Backport
tool\r\ndocumentation](https://github.com/sorenlouv/backport)\r\n\r\n<!--BACKPORT\r\n[{\"author\":{\"name\":\"Antonio\",\"email\":\"antonio.coelho@elastic.co\"},\"sourceCommit\":{\"committedDate\":\"2025-01-24T11:40:25Z\",\"message\":\"[ResponseOps][MW]\r\nFix
bug when creating repeating Maintenance Window
(#207084)\\n\\nCloses\r\n#198774\\r\\n\\r\\n## Summary\\r\\n\\r\\n-
There was a bug when submitting\r\n`rrule` with a `byweekday` I
fixed\\r\\nthat validation to use a more\r\ninclusive regex. `byweekday`
can be the\\r\\nexpected `MO`, `TU`, etc but\r\nalso `-1FR` or `+3SA`
where the number\\r\\ncorresponds to the week in a\r\nmonth.\\r\\n- The
model version for the maintenance window was incorrect\r\nso
when\\r\\nsaving the SO the validation was failing. I fixed that
and\r\nnow we are\\r\\nallowed to save `number[]` as expected.\\r\\n- I
removed\r\nsome duplicated code and we now use the `rrule` schema
from\\r\\nthe\r\n`common`\r\nfolder\",\"sha\":\"0df78e629b429f6007f559aca339b4323b71e4c0\",\"branchLabelMapping\":{\"^v9.0.0$\":\"main\",\"^v8.18.0$\":\"8.x\",\"^v(\\\\d+).(\\\\d+).\\\\d+$\":\"$1.$2\"}},\"sourcePullRequest\":{\"labels\":[\"Feature:Alerting\",\"release_note:skip\",\"Team:ResponseOps\",\"v9.0.0\",\"backport:prev-major\"],\"title\":\"[ResponseOps][MW]\r\nFix
bug when creating repeating
Maintenance\r\nWindow\",\"number\":207084,\"url\":\"https://github.com/elastic/kibana/pull/207084\",\"mergeCommit\":{\"message\":\"[ResponseOps][MW]\r\nFix
bug when creating repeating Maintenance Window
(#207084)\\n\\nCloses\r\n#198774\\r\\n\\r\\n## Summary\\r\\n\\r\\n-
There was a bug when submitting\r\n`rrule` with a `byweekday` I
fixed\\r\\nthat validation to use a more\r\ninclusive regex. `byweekday`
can be the\\r\\nexpected `MO`, `TU`, etc but\r\nalso `-1FR` or `+3SA`
where the number\\r\\ncorresponds to the week in a\r\nmonth.\\r\\n- The
model version for the maintenance window was incorrect\r\nso
when\\r\\nsaving the SO the validation was failing. I fixed that
and\r\nnow we are\\r\\nallowed to save `number[]` as expected.\\r\\n- I
removed\r\nsome duplicated code and we now use the `rrule` schema
from\\r\\nthe\r\n`common`\r\nfolder\",\"sha\":\"0df78e629b429f6007f559aca339b4323b71e4c0\"}},\"sourceBranch\":\"main\",\"suggestedTargetBranches\":[],\"targetPullRequestStates\":[{\"branch\":\"main\",\"label\":\"v9.0.0\",\"branchLabelMappingKey\":\"^v9.0.0$\",\"isSourceBranch\":true,\"state\":\"MERGED\",\"url\":\"https://github.com/elastic/kibana/pull/207084\",\"number\":207084,\"mergeCommit\":{\"message\":\"[ResponseOps][MW]\r\nFix
bug when creating repeating Maintenance Window
(#207084)\\n\\nCloses\r\n#198774\\r\\n\\r\\n## Summary\\r\\n\\r\\n-
There was a bug when submitting\r\n`rrule` with a `byweekday` I
fixed\\r\\nthat validation to use a more\r\ninclusive regex. `byweekday`
can be the\\r\\nexpected `MO`, `TU`, etc but\r\nalso `-1FR` or `+3SA`
where the number\\r\\ncorresponds to the week in a\r\nmonth.\\r\\n- The
model version for the maintenance window was incorrect\r\nso
when\\r\\nsaving the SO the validation was failing. I fixed that
and\r\nnow we are\\r\\nallowed to save `number[]` as expected.\\r\\n- I
removed\r\nsome duplicated code and we now use the `rrule` schema
from\\r\\nthe\r\n`common`
folder\",\"sha\":\"0df78e629b429f6007f559aca339b4323b71e4c0\"}}]}]\r\nBACKPORT-->"}}]}]
BACKPORT-->
This commit is contained in:
Antonio 2025-01-27 12:26:15 +01:00 committed by GitHub
parent f5ba3e18e3
commit 52fddd8394
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 158 additions and 133 deletions

View file

@ -10,6 +10,7 @@ import {
validateStartDateV1,
validateEndDateV1,
createValidateRecurrenceByV1,
validateRecurrenceByWeekdayV1,
} from '../../validation';
export const rRuleRequestSchema = schema.object({
@ -34,20 +35,9 @@ export const rRuleRequestSchema = schema.object({
})
),
byweekday: schema.maybe(
schema.arrayOf(
schema.oneOf([
schema.literal('MO'),
schema.literal('TU'),
schema.literal('WE'),
schema.literal('TH'),
schema.literal('FR'),
schema.literal('SA'),
schema.literal('SU'),
]),
{
validate: createValidateRecurrenceByV1('byweekday'),
}
)
schema.arrayOf(schema.string(), {
validate: validateRecurrenceByWeekdayV1,
})
),
bymonthday: schema.maybe(
schema.arrayOf(schema.number({ min: 1, max: 31 }), {

View file

@ -8,7 +8,9 @@
export { validateStartDate } from './validate_start_date/latest';
export { validateEndDate } from './validate_end_date/latest';
export { createValidateRecurrenceBy } from './validate_recurrence_by/latest';
export { validateRecurrenceByWeekday } from './validate_recurrence_by_weekday/latest';
export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1';
export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1';
export { createValidateRecurrenceBy as createValidateRecurrenceByV1 } from './validate_recurrence_by/v1';
export { validateRecurrenceByWeekday as validateRecurrenceByWeekdayV1 } from './validate_recurrence_by_weekday/v1';

View file

@ -4,7 +4,5 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TypeOf } from '@kbn/config-schema';
import { rRuleRequestSchema } from '../schemas/r_rule_request_schema';
export type RRuleRequest = TypeOf<typeof rRuleRequestSchema>;
export * from './v1';

View file

@ -0,0 +1,29 @@
/*
* 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 { validateRecurrenceByWeekday } from './v1';
describe('validateRecurrenceByWeekday', () => {
it('validates empty array', () => {
expect(validateRecurrenceByWeekday([])).toEqual('rRule byweekday cannot be empty');
});
it('validates properly formed byweekday strings', () => {
const weekdays = ['+1MO', '+2TU', '+3WE', '+4TH', '-4FR', '-3SA', '-2SU', '-1MO'];
expect(validateRecurrenceByWeekday(weekdays)).toBeUndefined();
});
it('validates improperly formed byweekday strings', () => {
expect(validateRecurrenceByWeekday(['+1MO', 'FOO', '+3WE', 'BAR', '-4FR'])).toEqual(
'invalid byweekday values in rRule byweekday: FOO,BAR'
);
});
it('validates byweekday strings without recurrence', () => {
expect(validateRecurrenceByWeekday(['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'])).toBeUndefined();
});
});

View file

@ -0,0 +1,25 @@
/*
* 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 validateRecurrenceByWeekday = (array: string[]) => {
if (array.length === 0) {
return 'rRule byweekday cannot be empty';
}
const byWeekDayRegex = new RegExp('^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$');
const invalidDays: string[] = [];
array.forEach((day) => {
if (!byWeekDayRegex.test(day)) {
invalidDays.push(day);
}
});
if (invalidDays.length > 0) {
return `invalid byweekday values in rRule byweekday: ${invalidDays.join(',')}`;
}
};

View file

@ -6,21 +6,24 @@
*/
import moment from 'moment-timezone';
import { createMaintenanceWindow } from './create_maintenance_window';
import { CreateMaintenanceWindowParams } from './types';
import {
savedObjectsClientMock,
loggingSystemMock,
uiSettingsServiceMock,
} from '@kbn/core/server/mocks';
import { SavedObject } from '@kbn/core/server';
import { FilterStateStore } from '@kbn/es-query';
import { Frequency } from '@kbn/rrule';
import {
MaintenanceWindowClientContext,
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
} from '../../../../../common';
import { getMockMaintenanceWindow } from '../../../../data/maintenance_window/test_helpers';
import type { MaintenanceWindow } from '../../types';
import { FilterStateStore } from '@kbn/es-query';
import { createMaintenanceWindow } from './create_maintenance_window';
import { CreateMaintenanceWindowParams } from './types';
const savedObjectsClient = savedObjectsClientMock.create();
const uiSettings = uiSettingsServiceMock.createClient();
@ -57,6 +60,13 @@ describe('MaintenanceWindowClient - create', () => {
const mockMaintenanceWindow = getMockMaintenanceWindow({
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
rRule: {
tzid: 'UTC',
dtstart: '2023-02-26T00:00:00.000Z',
freq: Frequency.WEEKLY,
byweekday: ['-4MO', 'TU'],
count: 2,
},
});
savedObjectsClient.create.mockResolvedValueOnce({

View file

@ -6,8 +6,9 @@
*/
import { schema } from '@kbn/config-schema';
import { rRuleRequestSchema } from '../../../../../../common/routes/r_rule';
import { maintenanceWindowCategoryIdsSchema } from '../../../schemas';
import { rRuleRequestSchema } from '../../../../r_rule/schemas';
import { alertsFilterQuerySchema } from '../../../../alerts_filter_query/schemas';
export const createMaintenanceWindowParamsSchema = schema.object({

View file

@ -7,7 +7,7 @@
import { schema } from '@kbn/config-schema';
import { maintenanceWindowCategoryIdsSchema } from '../../../schemas';
import { rRuleRequestSchema } from '../../../../r_rule/schemas';
import { rRuleRequestSchema } from '../../../../../../common/routes/r_rule';
import { alertsFilterQuerySchema } from '../../../../alerts_filter_query/schemas';
export const updateMaintenanceWindowParamsSchema = schema.object({

View file

@ -38,6 +38,7 @@ const updatedAttributes = {
dtstart: '2023-03-26T00:00:00.000Z',
freq: Frequency.WEEKLY,
count: 2,
byweekday: ['-1MO', 'WE'],
},
};
@ -110,8 +111,8 @@ describe('MaintenanceWindowClient - update', () => {
{
...updatedAttributes,
events: [
{ gte: '2023-03-26T00:00:00.000Z', lte: '2023-03-26T02:00:00.000Z' },
{ gte: '2023-04-01T23:00:00.000Z', lte: '2023-04-02T01:00:00.000Z' }, // Daylight savings
{ gte: '2023-03-26T23:00:00.000Z', lte: '2023-03-27T01:00:00.000Z' },
{ gte: '2023-03-28T23:00:00.000Z', lte: '2023-03-29T01:00:00.000Z' }, // Daylight savings
],
expirationDate: moment(new Date(secondTimestamp)).tz('UTC').add(1, 'year').toISOString(),
createdAt: '2023-02-26T00:00:00.000Z',

View file

@ -6,4 +6,3 @@
*/
export { rRuleSchema } from './r_rule_schema';
export { rRuleRequestSchema } from './r_rule_request_schema';

View file

@ -1,57 +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 { schema } from '@kbn/config-schema';
import { validateStartDate, validateEndDate, createValidateRecurrenceBy } from '../validation';
export const rRuleRequestSchema = schema.object({
dtstart: schema.string({ validate: validateStartDate }),
tzid: schema.string(),
freq: schema.maybe(
schema.oneOf([schema.literal(0), schema.literal(1), schema.literal(2), schema.literal(3)])
),
interval: schema.maybe(
schema.number({
validate: (interval: number) => {
if (interval < 1) return 'rRule interval must be > 0';
},
})
),
until: schema.maybe(schema.string({ validate: validateEndDate })),
count: schema.maybe(
schema.number({
validate: (count: number) => {
if (count < 1) return 'rRule count must be > 0';
},
})
),
byweekday: schema.maybe(
schema.arrayOf(
schema.oneOf([
schema.literal('MO'),
schema.literal('TU'),
schema.literal('WE'),
schema.literal('TH'),
schema.literal('FR'),
schema.literal('SA'),
schema.literal('SU'),
]),
{
validate: createValidateRecurrenceBy('byweekday'),
}
)
),
bymonthday: schema.maybe(
schema.arrayOf(schema.number({ min: 1, max: 31 }), {
validate: createValidateRecurrenceBy('bymonthday'),
})
),
bymonth: schema.maybe(
schema.arrayOf(schema.number({ min: 1, max: 12 }), {
validate: createValidateRecurrenceBy('bymonth'),
})
),
});

View file

@ -6,4 +6,3 @@
*/
export type { RRule } from './r_rule';
export type { RRuleRequest } from './r_rule_request';

View file

@ -1,10 +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 { validateStartDate } from './validate_start_date';
export { validateEndDate } from './validate_end_date';
export { validateRecurrenceBy, createValidateRecurrenceBy } from './validate_recurrence_by';

View file

@ -1,13 +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 validateEndDate = (date: string) => {
const parsedValue = Date.parse(date);
if (isNaN(parsedValue)) return `Invalid date: ${date}`;
if (parsedValue <= Date.now()) return `Invalid snooze date as it is in the past: ${date}`;
return;
};

View file

@ -1,16 +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 validateRecurrenceBy = <T>(name: string, array: T[]) => {
if (array.length === 0) {
return `rRule ${name} cannot be empty`;
}
};
export const createValidateRecurrenceBy = <T>(name: string) => {
return (array: T[]) => validateRecurrenceBy(name, array);
};

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { rRuleRequestSchema } from '../../../../r_rule/schemas';
import { notifyWhenSchema, actionRequestSchema, systemActionRequestSchema } from '../../../schemas';
import { rRuleRequestSchema } from '../../../../../../common/routes/r_rule';
import { validateDuration } from '../../../validation';
import { validateSnoozeSchedule } from '../validation';

View file

@ -27,7 +27,7 @@ export const alertsFilterQuerySchema = schema.object({
dsl: schema.maybe(schema.string()),
});
const rRuleSchema = schema.object({
export const rRuleSchema = schema.object({
dtstart: schema.string(),
tzid: schema.string(),
freq: schema.maybe(
@ -55,15 +55,17 @@ const rRuleSchema = schema.object({
schema.literal('SU'),
])
),
byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))),
bymonth: schema.maybe(schema.number()),
bysetpos: schema.maybe(schema.number()),
bymonthday: schema.maybe(schema.number()),
byyearday: schema.maybe(schema.number()),
byweekno: schema.maybe(schema.number()),
byhour: schema.maybe(schema.number()),
byminute: schema.maybe(schema.number()),
bysecond: schema.maybe(schema.number()),
byweekday: schema.maybe(
schema.nullable(schema.arrayOf(schema.oneOf([schema.string(), schema.number()])))
),
bymonth: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
bysetpos: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
bymonthday: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
byyearday: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
byweekno: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
byhour: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
byminute: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
bysecond: schema.maybe(schema.nullable(schema.arrayOf(schema.number()))),
});
const rawMaintenanceWindowEventsSchema = schema.object({

View file

@ -110,6 +110,71 @@ export default function createMaintenanceWindowTests({ getService }: FtrProvider
});
}
describe('rRuleSchema validation', () => {
it('should create maintenance window with byweekday', async () => {
const rrule = {
dtstart: new Date().toISOString(),
tzid: 'UTC',
byweekday: ['+1MO', 'TH'],
};
const response = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: rrule,
})
.expect(200);
objectRemover.add('space1', response.body.id, 'rules/maintenance_window', 'alerting', true);
expect(response.body.r_rule.byweekday).to.eql(rrule.byweekday);
});
it('should create maintenance window with bymonth', async () => {
const rrule = {
dtstart: new Date().toISOString(),
tzid: 'UTC',
bymonth: [9, 4],
};
const response = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: rrule,
})
.expect(200);
objectRemover.add('space1', response.body.id, 'rules/maintenance_window', 'alerting', true);
expect(response.body.r_rule.bymonth).to.eql(rrule.bymonth);
});
it('should create maintenance window with bymonthday', async () => {
const rrule = {
dtstart: new Date().toISOString(),
tzid: 'UTC',
bymonthday: [1, 30],
};
const response = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: rrule,
})
.expect(200);
objectRemover.add('space1', response.body.id, 'rules/maintenance_window', 'alerting', true);
expect(response.body.r_rule.bymonthday).to.eql(rrule.bymonthday);
});
});
it('should create maintenance window with category ids', async () => {
const response = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)