Skip to content

Commit

Permalink
[RAM] Maintenance Window - Expose Active Maintenance Window Route (#1…
Browse files Browse the repository at this point in the history
…55321)

## Summary
Resolves: #155306

Exposes the `/_active` route to get all currently active maintenance
windows

```
GET `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_active`
body: {}

Response: MaintenanceWindow[] 
```
### Checklist

Delete any items that are not applicable to this PR.
- [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
  • Loading branch information
JiaweiWu authored Apr 20, 2023
1 parent 80fb939 commit 4d89152
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface MaintenanceWindowAggregationResult {

export interface ActiveParams {
start?: string;
interval: string;
interval?: string;
}

export async function getActiveMaintenanceWindows(
Expand All @@ -40,7 +40,7 @@ export async function getActiveMaintenanceWindows(
const { start, interval } = params;

const startDate = start ? new Date(start) : new Date();
const duration = parseDuration(interval);
const duration = interval ? parseDuration(interval) : 0;
const endDate = moment.utc(startDate).add(duration, 'ms').toDate();

const startDateISO = startDate.toISOString();
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { deleteMaintenanceWindowRoute } from './maintenance_window/delete_mainte
import { findMaintenanceWindowsRoute } from './maintenance_window/find_maintenance_windows';
import { archiveMaintenanceWindowRoute } from './maintenance_window/archive_maintenance_window';
import { finishMaintenanceWindowRoute } from './maintenance_window/finish_maintenance_window';
import { activeMaintenanceWindowsRoute } from './maintenance_window/active_maintenance_windows';

export interface RouteOptions {
router: IRouter<AlertingRequestHandlerContext>;
Expand Down Expand Up @@ -108,4 +109,5 @@ export function defineRoutes(opts: RouteOptions) {
findMaintenanceWindowsRoute(router, licenseState);
archiveMaintenanceWindowRoute(router, licenseState);
finishMaintenanceWindowRoute(router, licenseState);
activeMaintenanceWindowsRoute(router, licenseState);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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 { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../lib/license_state.mock';
import { verifyApiAccess } from '../../lib/license_api_access';
import { mockHandlerArguments } from '../_mock_handler_arguments';
import { maintenanceWindowClientMock } from '../../maintenance_window_client.mock';
import { activeMaintenanceWindowsRoute } from './active_maintenance_windows';
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
import { MaintenanceWindowStatus } from '../../../common';
import { rewriteMaintenanceWindowRes } from '../lib';

const maintenanceWindowClient = maintenanceWindowClientMock.create();

jest.mock('../../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));

const mockMaintenanceWindows = [
{
...getMockMaintenanceWindow(),
eventStartTime: new Date().toISOString(),
eventEndTime: new Date().toISOString(),
status: MaintenanceWindowStatus.Running,
id: 'test-id1',
},
{
...getMockMaintenanceWindow(),
eventStartTime: new Date().toISOString(),
eventEndTime: new Date().toISOString(),
status: MaintenanceWindowStatus.Running,
id: 'test-id2',
},
];

describe('activeMaintenanceWindowsRoute', () => {
beforeEach(() => {
jest.resetAllMocks();
});

test('should get the currently active maintenance windows', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

activeMaintenanceWindowsRoute(router, licenseState);

maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(
mockMaintenanceWindows
);
const [config, handler] = router.get.mock.calls[0];
const [context, req, res] = mockHandlerArguments({ maintenanceWindowClient }, { body: {} });

expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/_active');
expect(config.options?.tags?.[0]).toEqual('access:read-maintenance-window');

await handler(context, req, res);

expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalled();
expect(res.ok).toHaveBeenLastCalledWith({
body: mockMaintenanceWindows.map((data) => rewriteMaintenanceWindowRes(data)),
});
});

test('ensures the license allows for getting active maintenance windows', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

activeMaintenanceWindowsRoute(router, licenseState);

maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(
mockMaintenanceWindows
);
const [, handler] = router.get.mock.calls[0];
const [context, req, res] = mockHandlerArguments({ maintenanceWindowClient }, { body: {} });
await handler(context, req, res);
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});

test('ensures the license check prevents for getting active maintenance windows', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

activeMaintenanceWindowsRoute(router, licenseState);

(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('Failure');
});
const [, handler] = router.get.mock.calls[0];
const [context, req, res] = mockHandlerArguments({ maintenanceWindowClient }, { body: {} });
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { IRouter } from '@kbn/core/server';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';

export const activeMaintenanceWindowsRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_active`,
validate: {},
options: {
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`],
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
const result = await maintenanceWindowClient.getActiveMaintenanceWindows({});

return res.ok({
body: result.map((maintenanceWindow) => rewriteMaintenanceWindowRes(maintenanceWindow)),
});
})
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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 moment from 'moment';
import expect from '@kbn/expect';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix, ObjectRemover } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function activeMaintenanceWindowTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');

describe('activeMaintenanceWindow', () => {
const objectRemover = new ObjectRemover(supertest);
const createParams = {
title: 'test-maintenance-window',
duration: 60 * 60 * 1000, // 1 hr
r_rule: {
dtstart: new Date().toISOString(),
tzid: 'UTC',
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());

for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
afterEach(() => objectRemover.removeAll());
it('should handle update maintenance window request appropriately', async () => {
// Create 2 active and 1 inactive maintenance window
const { body: createdMaintenanceWindow1 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send(createParams);

const { body: createdMaintenanceWindow2 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send(createParams);

const { body: createdMaintenanceWindow3 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: {
...createParams.r_rule,
dtstart: moment.utc().add(1, 'day').toISOString(),
},
});

objectRemover.add(
space.id,
createdMaintenanceWindow1.id,
'rules/maintenance_window',
'alerting',
true
);
objectRemover.add(
space.id,
createdMaintenanceWindow2.id,
'rules/maintenance_window',
'alerting',
true
);
objectRemover.add(
space.id,
createdMaintenanceWindow3.id,
'rules/maintenance_window',
'alerting',
true
);

const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/_active`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({});

switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all_with_restricted_fixture at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: 'Forbidden',
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.body.length).to.eql(2);
expect(response.statusCode).to.eql(200);
expect(response.body[0].title).to.eql('test-maintenance-window');
expect(response.body[0].duration).to.eql(3600000);
expect(response.body[0].r_rule.dtstart).to.eql(createParams.r_rule.dtstart);
expect(response.body[0].events.length).to.be.greaterThan(0);
expect(response.body[0].status).to.eql('running');

const ids = response.body.map(
(maintenanceWindow: { id: string }) => maintenanceWindow.id
);
expect(ids.sort()).to.eql(
[createdMaintenanceWindow1.id, createdMaintenanceWindow2.id].sort()
);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}

it('should return an empty array if there are no active maintenance windows', async () => {
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: {
...createParams.r_rule,
dtstart: moment.utc().add(1, 'day').toISOString(),
},
});

objectRemover.add(
'space1',
createdMaintenanceWindow.id,
'rules/maintenance_window',
'alerting',
true
);

const response = await supertest
.get(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/_active`)
.set('kbn-xsrf', 'foo')
.send({})
.expect(200);

expect(response.body).to.eql([]);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function maintenanceWindowTests({ loadTestFile, getService }: Ftr
loadTestFile(require.resolve('./archive_maintenance_window'));
loadTestFile(require.resolve('./finish_maintenance_window'));
loadTestFile(require.resolve('./find_maintenance_windows'));
loadTestFile(require.resolve('./active_maintenance_windows'));
});
});
}

0 comments on commit 4d89152

Please sign in to comment.