Skip to content

Commit 59cce68

Browse files
js-jankisalviNicholasPeretti
authored andcommitted
[ResponseOps][MaintenanceWindow] Update schedule duration API response to be same as request (elastic#238389)
## Summary Fixes elastic#234019 This PR returns schedule duration in the API response same as request, to avoid issue in terraform <img width="685" height="791" alt="Screenshot 2025-10-10 at 12 34 14" src="https://github.com/user-attachments/assets/2d52bd7d-9680-4b67-83e6-d10d89341fc7" /> ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels.
1 parent 2325bee commit 59cce68

File tree

7 files changed

+122
-14
lines changed

7 files changed

+122
-14
lines changed

x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ export {
1717
export { transformCustomScheduleToRRule as transformCustomScheduleToRRuleV1 } from './transforms/custom_to_rrule/v1';
1818
export { transformRRuleToCustomSchedule as transformRRuleToCustomScheduleV1 } from './transforms/rrule_to_custom/v1';
1919
export type { ScheduleRequest as ScheduleRequestV1 } from './types/v1';
20+
21+
export { getDurationInMilliseconds } from './transforms/custom_to_rrule/util';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import moment from 'moment-timezone';
9+
import { DURATION_REGEX } from '../../constants';
10+
11+
export const getDurationInMilliseconds = (duration: string): number => {
12+
const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? [];
13+
14+
return moment
15+
.duration(durationNumber, durationUnit as moment.unitOfTime.DurationConstructor)
16+
.asMilliseconds();
17+
};

x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
* 2.0.
66
*/
77

8-
import moment from 'moment-timezone';
98
import { Frequency } from '@kbn/rrule';
109
import type { RRuleRequestV1 } from '../../../r_rule';
1110
import type { ScheduleRequest } from '../../types/v1';
12-
import { DEFAULT_TIMEZONE, DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../../constants';
11+
import { DEFAULT_TIMEZONE, INTERVAL_FREQUENCY_REGEXP } from '../../constants';
12+
import { getDurationInMilliseconds } from './util';
1313

1414
const transformEveryToFrequency = (frequency?: string) => {
1515
switch (frequency) {
@@ -26,14 +26,6 @@ const transformEveryToFrequency = (frequency?: string) => {
2626
}
2727
};
2828

29-
const getDurationInMilliseconds = (duration: string): number => {
30-
const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? [];
31-
32-
return moment
33-
.duration(durationNumber, durationUnit as moment.unitOfTime.DurationConstructor)
34-
.asMilliseconds();
35-
};
36-
3729
export const transformCustomScheduleToRRule = (
3830
schedule: ScheduleRequest
3931
): {

x-pack/platform/plugins/shared/alerting/server/routes/maintenance_window/apis/external/create/create_maintenance_window_route.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const mockMaintenanceWindow = {
2828
eventEndTime: new Date().toISOString(),
2929
status: MaintenanceWindowStatus.Running,
3030
id: 'test-id',
31+
duration: 864000000,
3132
} as MaintenanceWindow;
3233

3334
const createParams = {
@@ -124,7 +125,7 @@ describe('createMaintenanceWindowRoute', () => {
124125
id: 'test-id',
125126
schedule: {
126127
custom: {
127-
duration: '60m',
128+
duration: '10d',
128129
recurring: {
129130
occurrences: 2,
130131
},
@@ -140,6 +141,28 @@ describe('createMaintenanceWindowRoute', () => {
140141
});
141142
});
142143

144+
test('throws error if request duration does not match response duration', async () => {
145+
const licenseState = licenseStateMock.create();
146+
const router = httpServiceMock.createRouter();
147+
148+
createMaintenanceWindowRoute(router, licenseState);
149+
150+
maintenanceWindowClient.create.mockResolvedValueOnce({
151+
...mockMaintenanceWindow, // it has 60m duration
152+
duration: 3600000,
153+
} as MaintenanceWindow);
154+
155+
const [, handler] = router.post.mock.calls[0];
156+
const [context, req, res] = mockHandlerArguments(
157+
{ maintenanceWindowClient },
158+
{ body: createParams }
159+
);
160+
161+
await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(
162+
`[Error: Request duration does not match response duration.]`
163+
);
164+
});
165+
143166
test('ensures the license allows for creating maintenance windows', async () => {
144167
const licenseState = licenseStateMock.create();
145168
const router = httpServiceMock.createRouter();

x-pack/platform/plugins/shared/alerting/server/routes/maintenance_window/apis/external/create/create_maintenance_window_route.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
} from '../../../../../../common/routes/maintenance_window/external/apis/create';
1919
import { createMaintenanceWindowRequestBodySchemaV1 } from '../../../../../../common/routes/maintenance_window/external/apis/create';
2020
import { maintenanceWindowResponseSchemaV1 } from '../../../../../../common/routes/maintenance_window/external/response';
21+
import { getDurationInMilliseconds } from '../../../../../../common/routes/schedule';
2122
import { transformInternalMaintenanceWindowToExternalV1 } from '../common/transforms';
2223
import { transformCreateBodyV1 } from './transform_create_body';
2324

@@ -65,6 +66,7 @@ export const createMaintenanceWindowRoute = (
6566
licenseState.ensureLicenseForMaintenanceWindow();
6667

6768
const body: CreateMaintenanceWindowRequestBodyV1 = req.body;
69+
const customSchedule = body.schedule.custom;
6870

6971
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
7072

@@ -74,9 +76,24 @@ export const createMaintenanceWindowRoute = (
7476

7577
const response: CreateMaintenanceWindowResponseV1 =
7678
transformInternalMaintenanceWindowToExternalV1(maintenanceWindow);
79+
// Return request duration in response when both are same otherwise throw an error
80+
const requestDurationInMilliseconds = getDurationInMilliseconds(customSchedule.duration);
81+
82+
const responseDurationInMilliseconds = getDurationInMilliseconds(
83+
response.schedule.custom.duration
84+
);
85+
86+
if (requestDurationInMilliseconds !== responseDurationInMilliseconds) {
87+
throw new Error('Request duration does not match response duration.');
88+
}
7789

7890
return res.ok({
79-
body: response,
91+
body: {
92+
...response,
93+
schedule: {
94+
custom: { ...response.schedule.custom, duration: customSchedule.duration },
95+
},
96+
},
8097
});
8198
})
8299
)

x-pack/platform/plugins/shared/alerting/server/routes/maintenance_window/apis/external/update/update_maintenance_window_route.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const mockMaintenanceWindow = {
3030
eventStartTime: new Date().toISOString(),
3131
eventEndTime: new Date().toISOString(),
3232
status: MaintenanceWindowStatus.Running,
33+
duration: 864000000,
3334
id: 'test-id',
3435
} as MaintenanceWindow;
3536

@@ -132,7 +133,7 @@ describe('updateMaintenanceWindowRoute', () => {
132133
id: 'test-id',
133134
schedule: {
134135
custom: {
135-
duration: '60m',
136+
duration: '10d',
136137
recurring: {
137138
occurrences: 2,
138139
},
@@ -148,6 +149,30 @@ describe('updateMaintenanceWindowRoute', () => {
148149
});
149150
});
150151

152+
test('throws error if request duration does not match response duration', async () => {
153+
const licenseState = licenseStateMock.create();
154+
const router = httpServiceMock.createRouter();
155+
156+
updateMaintenanceWindowRoute(router, licenseState);
157+
158+
maintenanceWindowClient.update.mockResolvedValueOnce({
159+
...getMockMaintenanceWindow(), // it has 60m duration
160+
eventStartTime: new Date().toISOString(),
161+
eventEndTime: new Date().toISOString(),
162+
status: MaintenanceWindowStatus.Running,
163+
id: 'test-id',
164+
});
165+
const [, handler] = router.patch.mock.calls[0];
166+
const [context, req, res] = mockHandlerArguments(
167+
{ maintenanceWindowClient },
168+
{ params: updateRequestParams, body: updateRequestBody }
169+
);
170+
171+
await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(
172+
`[Error: Request duration does not match response duration.]`
173+
);
174+
});
175+
151176
test('ensures the license allows for creating maintenance windows', async () => {
152177
const licenseState = licenseStateMock.create();
153178
const router = httpServiceMock.createRouter();

x-pack/platform/plugins/shared/alerting/server/routes/maintenance_window/apis/external/update/update_maintenance_window_route.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../../../../../../common/routes/maintenance_window/external/apis/update';
2424
import { maintenanceWindowResponseSchemaV1 } from '../../../../../../common/routes/maintenance_window/external/response';
2525
import { transformInternalMaintenanceWindowToExternalV1 } from '../common/transforms';
26+
import { getDurationInMilliseconds } from '../../../../../../common/routes/schedule';
2627

2728
import { transformUpdateBodyV1 } from './transform_update_body';
2829

@@ -79,6 +80,7 @@ export const updateMaintenanceWindowRoute = (
7980

8081
const body: UpdateMaintenanceWindowRequestBodyV1 = req.body;
8182
const params: UpdateMaintenanceWindowRequestParamsV1 = req.params;
83+
const customSchedule = body?.schedule?.custom;
8284

8385
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
8486

@@ -90,8 +92,38 @@ export const updateMaintenanceWindowRoute = (
9092
const response: UpdateMaintenanceWindowResponseV1 =
9193
transformInternalMaintenanceWindowToExternalV1(maintenanceWindow);
9294

95+
// Return request duration in response when both are same otherwise throw an error
96+
const requestDurationInMilliseconds = customSchedule?.duration
97+
? getDurationInMilliseconds(customSchedule.duration)
98+
: undefined;
99+
100+
const responseDurationInMilliseconds = getDurationInMilliseconds(
101+
response.schedule.custom.duration
102+
);
103+
104+
if (
105+
requestDurationInMilliseconds &&
106+
requestDurationInMilliseconds !== responseDurationInMilliseconds
107+
) {
108+
throw new Error('Request duration does not match response duration.');
109+
}
110+
111+
const responseDuration = customSchedule?.duration
112+
? customSchedule.duration
113+
: response.schedule.custom.duration;
114+
93115
return res.ok({
94-
body: response,
116+
body: {
117+
...response,
118+
schedule: {
119+
custom: {
120+
...response.schedule.custom,
121+
...{
122+
duration: responseDuration,
123+
},
124+
},
125+
},
126+
},
95127
});
96128
})
97129
)

0 commit comments

Comments
 (0)