mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[RAM] Window Maintenance Client/Saved Object and Mapping/REST APIs (#153411)
## Summary Resolves: https://github.com/elastic/kibana/issues/152270 Specs: https://docs.google.com/document/u/1/d/1-QblF6P19W9o5-10Us3bfgN80GRfjSIhybHrvJS_ObA/edit This PR implements the following: - New maintenance window SO - New maintenance window client in the alerting plugin (generates and queries maintenance window events, and other CRUD functionality around the SO) - New maintenance window REST APIs - Kibana privileges for reading/writing maintenance window This PR does not include integration with task runner, a new PR will be created to do that work. ## APIs: ``` Find all maintenance windows in current space GET `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_find` body: {} ``` ``` Create maintenance window: POST `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window` body: { title: string, duration: number, r_rule: RRule } ``` ``` Update maintenance window by ID: POST `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`, body: { title?: string, duration?: number, enabled?: boolean, r_rule?: RRule, } ``` ``` Get maintenance window by ID: GET `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`, ``` ``` Delete maintenance window by ID: DELETE `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`, ``` ``` Archive maintenance window by ID: POST `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_archive`, body: { archive: boolean } ``` ``` Finish maintenance window by ID: POST `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_finish`, ``` ## Maintenance window response schema: ``` { id: string; title: string; enabled: boolean; duration: number; expirationDate: string; events: DateRange[]; rRule: RRuleParams; status: 'running' | 'upcoming' | 'finished' | 'archived'; startDate: string | null; endDate: string | null; createdBy: string | null; updatedBy: string | null; createdAt: string; updatedAt: string; } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
83f1fb4f26
commit
3b07f96b44
79 changed files with 4898 additions and 135 deletions
|
@ -112,6 +112,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"legacy-url-alias": "9b8cca3fbb2da46fd12823d3cd38fdf1c9f24bc8",
|
||||
"lens": "2f6a8231591e3d62a83506b19e165774d74588ea",
|
||||
"lens-ui-telemetry": "d6c4e330d170eefc6214dbf77a53de913fa3eebc",
|
||||
"maintenance-window": "a9777f4e71381c56b4422bf8d30f626bde301c79",
|
||||
"map": "7902b2e2a550e0b73fd5aa6c4e2ba3a4e6558877",
|
||||
"metrics-explorer-view": "713dbf1ab5e067791d19170f715eb82cf07ebbcc",
|
||||
"ml-job": "12e21f1b1adfcc1052dc0b10c7459de875653b94",
|
||||
|
|
|
@ -79,6 +79,7 @@ const previouslyRegisteredTypes = [
|
|||
'legacy-url-alias',
|
||||
'lens',
|
||||
'lens-ui-telemetry',
|
||||
'maintenance-window',
|
||||
'map',
|
||||
'maps-telemetry',
|
||||
'metrics-explorer-view',
|
||||
|
|
|
@ -41,6 +41,8 @@ export * from './rule_notify_when_type';
|
|||
export * from './parse_duration';
|
||||
export * from './execution_log_types';
|
||||
export * from './rule_snooze_type';
|
||||
export * from './rrule_type';
|
||||
export * from './maintenance_window';
|
||||
export * from './default_rule_aggregation';
|
||||
export * from './rule_tags_aggregation';
|
||||
|
||||
|
|
60
x-pack/plugins/alerting/common/maintenance_window.ts
Normal file
60
x-pack/plugins/alerting/common/maintenance_window.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { RRuleParams } from './rrule_type';
|
||||
|
||||
export enum MaintenanceWindowStatus {
|
||||
Running = 'running',
|
||||
Upcoming = 'upcoming',
|
||||
Finished = 'finished',
|
||||
Archived = 'archived',
|
||||
}
|
||||
|
||||
export interface MaintenanceWindowModificationMetadata {
|
||||
createdBy: string | null;
|
||||
updatedBy: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DateRange {
|
||||
gte: string;
|
||||
lte: string;
|
||||
}
|
||||
|
||||
export interface MaintenanceWindowSOProperties {
|
||||
title: string;
|
||||
enabled: boolean;
|
||||
duration: number;
|
||||
expirationDate: string;
|
||||
events: DateRange[];
|
||||
rRule: RRuleParams;
|
||||
}
|
||||
|
||||
export type MaintenanceWindowSOAttributes = MaintenanceWindowSOProperties &
|
||||
MaintenanceWindowModificationMetadata;
|
||||
|
||||
export type MaintenanceWindow = MaintenanceWindowSOAttributes & {
|
||||
status: MaintenanceWindowStatus;
|
||||
eventStartTime: string | null;
|
||||
eventEndTime: string | null;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface MaintenanceWindowClientContext {
|
||||
getModificationMetadata: () => Promise<MaintenanceWindowModificationMetadata>;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export const MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE = 'maintenance-window';
|
||||
export const MAINTENANCE_WINDOW_FEATURE_ID = 'maintenanceWindow';
|
||||
export const MAINTENANCE_WINDOW_API_PRIVILEGES = {
|
||||
READ_MAINTENANCE_WINDOW: 'read-maintenance-window',
|
||||
WRITE_MAINTENANCE_WINDOW: 'write-maintenance-window',
|
||||
};
|
30
x-pack/plugins/alerting/common/rrule_type.ts
Normal file
30
x-pack/plugins/alerting/common/rrule_type.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 type { WeekdayStr } from 'rrule';
|
||||
|
||||
export type RRuleParams = Partial<RRuleRecord> & Pick<RRuleRecord, 'dtstart' | 'tzid'>;
|
||||
|
||||
// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec
|
||||
export interface RRuleRecord {
|
||||
dtstart: string;
|
||||
tzid: string;
|
||||
freq?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
until?: string;
|
||||
count?: number;
|
||||
interval?: number;
|
||||
wkst?: WeekdayStr;
|
||||
byweekday?: Array<string | number>;
|
||||
bymonth?: number[];
|
||||
bysetpos?: number[];
|
||||
bymonthday: number[];
|
||||
byyearday: number[];
|
||||
byweekno: number[];
|
||||
byhour: number[];
|
||||
byminute: number[];
|
||||
bysecond: number[];
|
||||
}
|
|
@ -5,13 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { WeekdayStr } from 'rrule';
|
||||
|
||||
type SnoozeRRule = Partial<RRuleRecord> & Pick<RRuleRecord, 'dtstart' | 'tzid'>;
|
||||
import { RRuleParams } from './rrule_type';
|
||||
|
||||
export interface RuleSnoozeSchedule {
|
||||
duration: number;
|
||||
rRule: SnoozeRRule;
|
||||
rRule: RRuleParams;
|
||||
// For scheduled/recurring snoozes, `id` uniquely identifies them so that they can be displayed, modified, and deleted individually
|
||||
id?: string;
|
||||
skipRecurrences?: string[];
|
||||
|
@ -21,27 +19,7 @@ export interface RuleSnoozeSchedule {
|
|||
// RuleSnooze = RuleSnoozeSchedule[] throws typescript errors across the whole lib
|
||||
export type RuleSnooze = Array<{
|
||||
duration: number;
|
||||
rRule: SnoozeRRule;
|
||||
rRule: RRuleParams;
|
||||
id?: string;
|
||||
skipRecurrences?: string[];
|
||||
}>;
|
||||
|
||||
// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec
|
||||
export interface RRuleRecord {
|
||||
dtstart: string;
|
||||
tzid: string;
|
||||
freq?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
until?: string;
|
||||
count?: number;
|
||||
interval?: number;
|
||||
wkst?: WeekdayStr;
|
||||
byweekday?: Array<string | number>;
|
||||
bymonth?: number[];
|
||||
bysetpos?: number[];
|
||||
bymonthday: number[];
|
||||
byyearday: number[];
|
||||
byweekno: number[];
|
||||
byhour: number[];
|
||||
byminute: number[];
|
||||
bysecond: number[];
|
||||
}
|
||||
|
|
8
x-pack/plugins/alerting/server/lib/rrule/index.ts
Normal file
8
x-pack/plugins/alerting/server/lib/rrule/index.ts
Normal file
|
@ -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 * from './parse_by_weekday';
|
13
x-pack/plugins/alerting/server/lib/rrule/parse_by_weekday.ts
Normal file
13
x-pack/plugins/alerting/server/lib/rrule/parse_by_weekday.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { ByWeekday, rrulestr } from 'rrule';
|
||||
|
||||
export function parseByWeekday(byweekday: Array<string | number>): ByWeekday[] {
|
||||
const rRuleString = `RRULE:BYDAY=${byweekday.join(',')}`;
|
||||
const parsedRRule = rrulestr(rRuleString);
|
||||
return parsedRRule.origOptions.byweekday as ByWeekday[];
|
||||
}
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RRule, ByWeekday, Weekday, rrulestr } from 'rrule';
|
||||
import { RRule, Weekday } from 'rrule';
|
||||
import { RuleSnoozeSchedule } from '../../types';
|
||||
import { parseByWeekday } from '../rrule';
|
||||
import { utcToLocalUtc, localUtcToUtc } from './timezone_helpers';
|
||||
|
||||
const MAX_TIMESTAMP = 8640000000000000;
|
||||
|
@ -63,9 +64,3 @@ export function isSnoozeActive(snooze: RuleSnoozeSchedule) {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseByWeekday(byweekday: Array<string | number>): ByWeekday[] {
|
||||
const rRuleString = `RRULE:BYDAY=${byweekday.join(',')}`;
|
||||
const parsedRRule = rrulestr(rRuleString);
|
||||
return parsedRRule.origOptions.byweekday as ByWeekday[];
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import { RRule, Weekday } from 'rrule';
|
||||
import { RuleSnoozeSchedule } from '../../types';
|
||||
import { isSnoozeActive, parseByWeekday } from './is_snooze_active';
|
||||
import { isSnoozeActive } from './is_snooze_active';
|
||||
import { parseByWeekday } from '../rrule';
|
||||
|
||||
export function isSnoozeExpired(snooze: RuleSnoozeSchedule) {
|
||||
if (isSnoozeActive(snooze)) {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { MaintenanceWindowClientApi } from './types';
|
||||
|
||||
type Schema = MaintenanceWindowClientApi;
|
||||
export type MaintenanceWindowClientMock = jest.Mocked<Schema>;
|
||||
|
||||
const createMaintenanceWindowClientMock = () => {
|
||||
const mocked: MaintenanceWindowClientMock = {
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
archive: jest.fn(),
|
||||
getActiveMaintenanceWindows: jest.fn(),
|
||||
finish: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const maintenanceWindowClientMock: {
|
||||
create: () => MaintenanceWindowClientMock;
|
||||
} = {
|
||||
create: createMaintenanceWindowClientMock,
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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-timezone';
|
||||
import { RRule } from 'rrule';
|
||||
import { generateMaintenanceWindowEvents } from './generate_maintenance_window_events';
|
||||
|
||||
describe('generateMaintenanceWindowEvents', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
it('should generate events for rrule repeating daily', () => {
|
||||
// Create rrule with expiration date at: 2023-03-13T00:00:00.000Z
|
||||
const result = generateMaintenanceWindowEvents({
|
||||
duration: 1 * 60 * 60 * 1000,
|
||||
expirationDate: moment(new Date('2023-02-27T00:00:00.000Z'))
|
||||
.tz('UTC')
|
||||
.add(2, 'weeks')
|
||||
.toISOString(),
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
freq: RRule.DAILY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-27T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.length).toEqual(15);
|
||||
|
||||
// Ensure start date is correct
|
||||
expect(result[0].gte).toEqual('2023-02-27T00:00:00.000Z');
|
||||
expect(result[0].lte).toEqual('2023-02-27T01:00:00.000Z');
|
||||
|
||||
// Ensure it is incrementing by the correct frequency (daily)
|
||||
expect(result[1].gte).toEqual('2023-02-28T00:00:00.000Z');
|
||||
expect(result[1].lte).toEqual('2023-02-28T01:00:00.000Z');
|
||||
|
||||
// Ensure end date is correct
|
||||
expect(result[result.length - 1].gte).toEqual('2023-03-13T00:00:00.000Z');
|
||||
expect(result[result.length - 1].lte).toEqual('2023-03-13T01:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should generate events for rrule repeating weekly', () => {
|
||||
// Create rrule with expiration date at: 2023-04-03T00:00:00.000Z
|
||||
const result = generateMaintenanceWindowEvents({
|
||||
duration: 1 * 60 * 60 * 1000,
|
||||
expirationDate: moment(new Date('2023-02-27T00:00:00.000Z'))
|
||||
.tz('UTC')
|
||||
.add(5, 'weeks')
|
||||
.toISOString(),
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
freq: RRule.WEEKLY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-27T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.length).toEqual(6);
|
||||
|
||||
// Ensure start date is correct
|
||||
expect(result[0].gte).toEqual('2023-02-27T00:00:00.000Z');
|
||||
expect(result[0].lte).toEqual('2023-02-27T01:00:00.000Z');
|
||||
|
||||
// Ensure it is incrementing by the correct frequency (weekly)
|
||||
expect(result[1].gte).toEqual('2023-03-06T00:00:00.000Z');
|
||||
expect(result[1].lte).toEqual('2023-03-06T01:00:00.000Z');
|
||||
|
||||
// Ensure end date is correct
|
||||
expect(result[result.length - 1].gte).toEqual('2023-04-03T00:00:00.000Z');
|
||||
expect(result[result.length - 1].lte).toEqual('2023-04-03T01:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should generate events for rrule repeating monthly', () => {
|
||||
// Create rrule with expiration date at: 2023-10-27T00:00:00.000Z
|
||||
const result = generateMaintenanceWindowEvents({
|
||||
duration: 1 * 60 * 60 * 1000,
|
||||
expirationDate: moment(new Date('2023-02-27T00:00:00.000Z'))
|
||||
.tz('UTC')
|
||||
.add(8, 'months')
|
||||
.toISOString(),
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
freq: RRule.MONTHLY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-27T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.length).toEqual(9);
|
||||
|
||||
// Ensure start date is correct
|
||||
expect(result[0].gte).toEqual('2023-02-27T00:00:00.000Z');
|
||||
expect(result[0].lte).toEqual('2023-02-27T01:00:00.000Z');
|
||||
|
||||
// Ensure it is incrementing by the correct frequency (monthly)
|
||||
expect(result[1].gte).toEqual('2023-03-27T00:00:00.000Z');
|
||||
expect(result[1].lte).toEqual('2023-03-27T01:00:00.000Z');
|
||||
|
||||
// Ensure end date is correct
|
||||
expect(result[result.length - 1].gte).toEqual('2023-10-27T00:00:00.000Z');
|
||||
expect(result[result.length - 1].lte).toEqual('2023-10-27T01:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should generate events for rrule repeating by specific days', () => {
|
||||
const result = generateMaintenanceWindowEvents({
|
||||
duration: 1 * 60 * 60 * 1000,
|
||||
expirationDate: moment(new Date('2023-02-27T00:00:00.000Z'))
|
||||
.tz('UTC')
|
||||
.add(5, 'weeks')
|
||||
.toISOString(),
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
freq: RRule.WEEKLY,
|
||||
interval: 1,
|
||||
byweekday: ['TU', 'TH'],
|
||||
dtstart: '2023-02-27T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.length).toEqual(10); // 5 weeks x 2 times a week = 10
|
||||
|
||||
// Ensure start date is correct (Tuesday)
|
||||
expect(result[0].gte).toEqual('2023-02-28T00:00:00.000Z');
|
||||
expect(result[0].lte).toEqual('2023-02-28T01:00:00.000Z');
|
||||
|
||||
// Ensure it is incrementing by the correct frequency (Thursday)
|
||||
expect(result[1].gte).toEqual('2023-03-02T00:00:00.000Z');
|
||||
expect(result[1].lte).toEqual('2023-03-02T01:00:00.000Z');
|
||||
|
||||
// Ensure end date is correct (Thursday)
|
||||
expect(result[result.length - 1].gte).toEqual('2023-03-30T00:00:00.000Z');
|
||||
expect(result[result.length - 1].lte).toEqual('2023-03-30T01:00:00.000Z');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import { RRule, Weekday } from 'rrule';
|
||||
import { parseByWeekday } from '../lib/rrule';
|
||||
import { RRuleParams, MaintenanceWindowSOAttributes, DateRange } from '../../common';
|
||||
import { utcToLocalUtc, localUtcToUtc } from '../lib/snooze';
|
||||
|
||||
export interface GenerateMaintenanceWindowEventsParams {
|
||||
rRule: RRuleParams;
|
||||
expirationDate: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export const generateMaintenanceWindowEvents = ({
|
||||
rRule,
|
||||
expirationDate,
|
||||
duration,
|
||||
}: GenerateMaintenanceWindowEventsParams) => {
|
||||
const { dtstart, until, wkst, byweekday, tzid, ...rest } = rRule;
|
||||
|
||||
const startDate = utcToLocalUtc(new Date(dtstart), tzid);
|
||||
const endDate = utcToLocalUtc(new Date(expirationDate), tzid);
|
||||
|
||||
const rRuleOptions = {
|
||||
...rest,
|
||||
dtstart: startDate,
|
||||
until: until ? utcToLocalUtc(new Date(until), tzid) : null,
|
||||
wkst: wkst ? Weekday.fromStr(wkst) : null,
|
||||
byweekday: byweekday ? parseByWeekday(byweekday) : null,
|
||||
};
|
||||
|
||||
try {
|
||||
const recurrenceRule = new RRule(rRuleOptions);
|
||||
const occurrenceDates = recurrenceRule.between(startDate, endDate, true);
|
||||
|
||||
return occurrenceDates.map((date) => {
|
||||
const utcDate = localUtcToUtc(date, tzid);
|
||||
return {
|
||||
gte: utcDate.toISOString(),
|
||||
lte: moment.utc(utcDate).add(duration, 'ms').toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to process RRule ${rRule}. Error: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if we should regenerate maintenance window events.
|
||||
* Don't regenerate old events if the underlying RRule/duration did not change.
|
||||
*/
|
||||
export const shouldRegenerateEvents = ({
|
||||
maintenanceWindow,
|
||||
rRule,
|
||||
duration,
|
||||
}: {
|
||||
maintenanceWindow: MaintenanceWindowSOAttributes;
|
||||
rRule?: RRuleParams;
|
||||
duration?: number;
|
||||
}): boolean => {
|
||||
// If the rRule fails a deep equality check (there is a change), we should regenerate events
|
||||
if (rRule && !_.isEqual(rRule, maintenanceWindow.rRule)) {
|
||||
return true;
|
||||
}
|
||||
// If the duration changes, we should regenerate events
|
||||
if (typeof duration === 'number' && duration !== maintenanceWindow.duration) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates and merges the old events with the new events to preserve old modified events,
|
||||
* Unless the maintenance window was archived, then the old events are trimmed.
|
||||
*/
|
||||
export const mergeEvents = ({
|
||||
oldEvents,
|
||||
newEvents,
|
||||
}: {
|
||||
oldEvents: DateRange[];
|
||||
newEvents: DateRange[];
|
||||
}) => {
|
||||
// If new events have more entries (expiration date got pushed), we merge the old into the new
|
||||
if (newEvents.length > oldEvents.length) {
|
||||
return [...oldEvents, ...newEvents.slice(-(newEvents.length - oldEvents.length))];
|
||||
}
|
||||
// If new events have less entries (maintenance window got archived), we trim the old events
|
||||
if (oldEvents.length > newEvents.length) {
|
||||
return oldEvents.slice(0, newEvents.length);
|
||||
}
|
||||
return oldEvents;
|
||||
};
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* 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 {
|
||||
getMaintenanceWindowDateAndStatus,
|
||||
findRecentEventWithStatus,
|
||||
} from './get_maintenance_window_date_and_status';
|
||||
import { DateRange, MaintenanceWindowStatus } from '../../common';
|
||||
|
||||
const events: DateRange[] = [
|
||||
{
|
||||
gte: '2023-03-25T00:00:00.000Z',
|
||||
lte: '2023-03-25T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-26T00:00:00.000Z',
|
||||
lte: '2023-03-26T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-27T00:00:00.000Z',
|
||||
lte: '2023-03-27T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-28T00:00:00.000Z',
|
||||
lte: '2023-03-28T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-29T00:00:00.000Z',
|
||||
lte: '2023-03-29T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-30T00:00:00.000Z',
|
||||
lte: '2023-03-30T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
describe('getMaintenanceWindowDateAndStatus', () => {
|
||||
it('should return finished if there is are no events', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-25T00:30:00.000Z'));
|
||||
const result = getMaintenanceWindowDateAndStatus({
|
||||
events: [],
|
||||
dateToCompare: new Date(),
|
||||
expirationDate: moment().add(1, 'year').toDate(),
|
||||
});
|
||||
|
||||
expect(result.eventStartTime).toEqual(null);
|
||||
expect(result.eventEndTime).toEqual(null);
|
||||
expect(result.status).toEqual('finished');
|
||||
});
|
||||
|
||||
it('should return archived if expiration date is before or equal to now', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-23T00:30:00.000Z'));
|
||||
let result = getMaintenanceWindowDateAndStatus({
|
||||
events,
|
||||
dateToCompare: new Date(),
|
||||
expirationDate: moment().subtract(1, 'minute').toDate(),
|
||||
});
|
||||
|
||||
expect(result.eventStartTime).toEqual('2023-03-25T00:00:00.000Z');
|
||||
expect(result.eventEndTime).toEqual('2023-03-25T01:00:00.000Z');
|
||||
expect(result.status).toEqual('archived');
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-28T00:30:00.000Z'));
|
||||
result = getMaintenanceWindowDateAndStatus({
|
||||
events,
|
||||
dateToCompare: new Date(),
|
||||
expirationDate: moment().subtract(1, 'minute').toDate(),
|
||||
});
|
||||
|
||||
expect(result.eventStartTime).toEqual('2023-03-28T00:00:00.000Z');
|
||||
expect(result.eventEndTime).toEqual('2023-03-28T01:00:00.000Z');
|
||||
expect(result.status).toEqual('archived');
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-04-28T00:30:00.000Z'));
|
||||
result = getMaintenanceWindowDateAndStatus({
|
||||
events,
|
||||
dateToCompare: new Date(),
|
||||
expirationDate: moment().subtract(1, 'minute').toDate(),
|
||||
});
|
||||
|
||||
expect(result.eventStartTime).toEqual('2023-03-31T00:00:00.000Z');
|
||||
expect(result.eventEndTime).toEqual('2023-03-31T01:00:00.000Z');
|
||||
expect(result.status).toEqual('archived');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRecentEventWithStatus', () => {
|
||||
it('should find the status if event is running', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-25T00:30:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-25T00:00:00.000Z',
|
||||
lte: '2023-03-25T01:00:00.000Z',
|
||||
},
|
||||
index: 0,
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
});
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-27T00:30:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-27T00:00:00.000Z',
|
||||
lte: '2023-03-27T01:00:00.000Z',
|
||||
},
|
||||
index: 2,
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
});
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-29T00:30:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-29T00:00:00.000Z',
|
||||
lte: '2023-03-29T01:00:00.000Z',
|
||||
},
|
||||
index: 4,
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
});
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:30:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-30T00:00:00.000Z',
|
||||
lte: '2023-03-30T01:00:00.000Z',
|
||||
},
|
||||
index: 5,
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the status if event is upcoming', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-24T05:00:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-25T00:00:00.000Z',
|
||||
lte: '2023-03-25T01:00:00.000Z',
|
||||
},
|
||||
index: 0,
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
});
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-26T05:00:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-27T00:00:00.000Z',
|
||||
lte: '2023-03-27T01:00:00.000Z',
|
||||
},
|
||||
index: 2,
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
});
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-27T05:00:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-28T00:00:00.000Z',
|
||||
lte: '2023-03-28T01:00:00.000Z',
|
||||
},
|
||||
index: 3,
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
});
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-29T05:00:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-30T00:00:00.000Z',
|
||||
lte: '2023-03-30T01:00:00.000Z',
|
||||
},
|
||||
index: 5,
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the status if event is finished', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-04-01T05:00:00.000Z'));
|
||||
expect(findRecentEventWithStatus(events, new Date())).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
index: 6,
|
||||
status: MaintenanceWindowStatus.Finished,
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the status if there is only 1 event', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-01-01T00:00:00.000Z'));
|
||||
expect(
|
||||
findRecentEventWithStatus(
|
||||
[
|
||||
{
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
],
|
||||
new Date()
|
||||
)
|
||||
).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
index: 0,
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
});
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-31T00:30:00.000Z'));
|
||||
expect(
|
||||
findRecentEventWithStatus(
|
||||
[
|
||||
{
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
],
|
||||
new Date()
|
||||
)
|
||||
).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
index: 0,
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
});
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-04-20T00:00:00.000Z'));
|
||||
expect(
|
||||
findRecentEventWithStatus(
|
||||
[
|
||||
{
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
],
|
||||
new Date()
|
||||
)
|
||||
).toEqual({
|
||||
event: {
|
||||
gte: '2023-03-31T00:00:00.000Z',
|
||||
lte: '2023-03-31T01:00:00.000Z',
|
||||
},
|
||||
index: 0,
|
||||
status: MaintenanceWindowStatus.Finished,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 { DateRange, MaintenanceWindowStatus } from '../../common';
|
||||
|
||||
export interface DateSearchResult {
|
||||
event: DateRange;
|
||||
index: number;
|
||||
status: MaintenanceWindowStatus;
|
||||
}
|
||||
|
||||
export interface MaintenanceWindowDateAndStatus {
|
||||
eventStartTime: string | null;
|
||||
eventEndTime: string | null;
|
||||
status: MaintenanceWindowStatus;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
// Returns the most recent/relevant event and the status for a maintenance window
|
||||
export const getMaintenanceWindowDateAndStatus = ({
|
||||
events,
|
||||
dateToCompare,
|
||||
expirationDate,
|
||||
}: {
|
||||
events: DateRange[];
|
||||
dateToCompare: Date;
|
||||
expirationDate: Date;
|
||||
}): MaintenanceWindowDateAndStatus => {
|
||||
// No events, status is finished
|
||||
if (!events.length) {
|
||||
return {
|
||||
eventStartTime: null,
|
||||
eventEndTime: null,
|
||||
status: MaintenanceWindowStatus.Finished,
|
||||
};
|
||||
}
|
||||
|
||||
const { event, status, index } = findRecentEventWithStatus(events, dateToCompare);
|
||||
// Past expiration, show the last event, but status is now archived
|
||||
if (moment.utc(expirationDate).isBefore(dateToCompare)) {
|
||||
return {
|
||||
eventStartTime: event.gte,
|
||||
eventEndTime: event.lte,
|
||||
status: MaintenanceWindowStatus.Archived,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
eventStartTime: event.gte,
|
||||
eventEndTime: event.lte,
|
||||
status,
|
||||
index,
|
||||
};
|
||||
};
|
||||
|
||||
// Binary date search to find the closest (or running) event relative to an arbitrary date
|
||||
export const findRecentEventWithStatus = (
|
||||
events: DateRange[],
|
||||
dateToCompare: Date
|
||||
): DateSearchResult => {
|
||||
const result = binaryDateSearch(events, dateToCompare, 0, events.length - 1)!;
|
||||
// Has running or upcoming event, just return the event
|
||||
if (
|
||||
result.status === MaintenanceWindowStatus.Running ||
|
||||
result.status === MaintenanceWindowStatus.Upcoming
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
// At the last event and it's finished, no more events are schedule so just return
|
||||
if (result.status === MaintenanceWindowStatus.Finished && result.index === events.length - 1) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
event: events[result.index + 1],
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
index: result.index + 1,
|
||||
};
|
||||
};
|
||||
|
||||
// Get the maintenance window status of any particular event relative to an arbitrary date
|
||||
const getEventStatus = (event: DateRange, dateToCompare: Date): MaintenanceWindowStatus => {
|
||||
if (moment.utc(event.gte).isAfter(dateToCompare)) {
|
||||
return MaintenanceWindowStatus.Upcoming;
|
||||
}
|
||||
if (moment.utc(event.lte).isSameOrBefore(dateToCompare)) {
|
||||
return MaintenanceWindowStatus.Finished;
|
||||
}
|
||||
return MaintenanceWindowStatus.Running;
|
||||
};
|
||||
|
||||
const binaryDateSearch = (
|
||||
events: DateRange[],
|
||||
dateToCompare: Date,
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
lastIndex?: number
|
||||
): DateSearchResult | undefined => {
|
||||
// Base case, take the last event it checked to see what the relative status to that event is
|
||||
if (startIndex > endIndex && typeof lastIndex === 'number') {
|
||||
const event = events[lastIndex];
|
||||
if (event) {
|
||||
return {
|
||||
event,
|
||||
status: getEventStatus(event, dateToCompare),
|
||||
index: lastIndex!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const midIndex = startIndex + Math.floor((endIndex - startIndex) / 2);
|
||||
const midEvent = events[midIndex];
|
||||
const midEventStatus = getEventStatus(midEvent, dateToCompare);
|
||||
|
||||
switch (midEventStatus) {
|
||||
case MaintenanceWindowStatus.Running:
|
||||
return {
|
||||
event: midEvent,
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
index: midIndex,
|
||||
};
|
||||
case MaintenanceWindowStatus.Upcoming:
|
||||
return binaryDateSearch(events, dateToCompare, startIndex, midIndex - 1, midIndex);
|
||||
case MaintenanceWindowStatus.Finished:
|
||||
return binaryDateSearch(events, dateToCompare, midIndex + 1, endIndex, midIndex);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { MaintenanceWindowSOAttributes } from '../../common';
|
||||
import { getMaintenanceWindowDateAndStatus } from './get_maintenance_window_date_and_status';
|
||||
|
||||
export interface GetMaintenanceWindowFromRawParams {
|
||||
id: string;
|
||||
attributes: MaintenanceWindowSOAttributes;
|
||||
}
|
||||
|
||||
export const getMaintenanceWindowFromRaw = ({
|
||||
id,
|
||||
attributes,
|
||||
}: GetMaintenanceWindowFromRawParams) => {
|
||||
const { events, expirationDate } = attributes;
|
||||
const { eventStartTime, eventEndTime, status } = getMaintenanceWindowDateAndStatus({
|
||||
events,
|
||||
expirationDate: new Date(expirationDate),
|
||||
dateToCompare: new Date(),
|
||||
});
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
id,
|
||||
eventStartTime,
|
||||
eventEndTime,
|
||||
status,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 * from './maintenance_window_client';
|
||||
export * from './generate_maintenance_window_events';
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { create, CreateParams } from './methods/create';
|
||||
import { get, GetParams } from './methods/get';
|
||||
import { update, UpdateParams } from './methods/update';
|
||||
import { find, FindResult } from './methods/find';
|
||||
import { deleteMaintenanceWindow, DeleteParams } from './methods/delete';
|
||||
import { archive, ArchiveParams } from './methods/archive';
|
||||
import {
|
||||
getActiveMaintenanceWindows,
|
||||
ActiveParams,
|
||||
} from './methods/get_active_maintenance_windows';
|
||||
import { finish, FinishParams } from './methods/finish';
|
||||
|
||||
import {
|
||||
MaintenanceWindow,
|
||||
MaintenanceWindowModificationMetadata,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../common';
|
||||
|
||||
export interface MaintenanceWindowClientConstructorOptions {
|
||||
readonly logger: Logger;
|
||||
readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
readonly getUserName: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
export class MaintenanceWindowClient {
|
||||
private readonly logger: Logger;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly getUserName: () => Promise<string | null>;
|
||||
private readonly context: MaintenanceWindowClientContext;
|
||||
|
||||
constructor(options: MaintenanceWindowClientConstructorOptions) {
|
||||
this.logger = options.logger;
|
||||
this.savedObjectsClient = options.savedObjectsClient;
|
||||
this.getUserName = options.getUserName;
|
||||
this.context = {
|
||||
logger: this.logger,
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
getModificationMetadata: this.getModificationMetadata.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async getModificationMetadata(): Promise<MaintenanceWindowModificationMetadata> {
|
||||
const createTime = Date.now();
|
||||
const userName = await this.getUserName();
|
||||
|
||||
return {
|
||||
createdBy: userName,
|
||||
updatedBy: userName,
|
||||
createdAt: new Date(createTime).toISOString(),
|
||||
updatedAt: new Date(createTime).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
public create = (params: CreateParams): Promise<MaintenanceWindow> =>
|
||||
create(this.context, params);
|
||||
public get = (params: GetParams): Promise<MaintenanceWindow> => get(this.context, params);
|
||||
public update = (params: UpdateParams): Promise<MaintenanceWindow> =>
|
||||
update(this.context, params);
|
||||
public find = (): Promise<FindResult> => find(this.context);
|
||||
public delete = (params: DeleteParams): Promise<{}> =>
|
||||
deleteMaintenanceWindow(this.context, params);
|
||||
public archive = (params: ArchiveParams): Promise<MaintenanceWindow> =>
|
||||
archive(this.context, params);
|
||||
public finish = (params: FinishParams): Promise<MaintenanceWindow> =>
|
||||
finish(this.context, params);
|
||||
public getActiveMaintenanceWindows = (params: ActiveParams): Promise<MaintenanceWindow[]> =>
|
||||
getActiveMaintenanceWindows(this.context, params);
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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-timezone';
|
||||
import { RRule } from 'rrule';
|
||||
import { archive } from './archive';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const firstTimestamp = '2023-02-26T00:00:00.000Z';
|
||||
const secondTimestamp = '2023-03-26T00:00:00.000Z';
|
||||
|
||||
const updatedMetadata = {
|
||||
createdAt: '2023-03-26T00:00:00.000Z',
|
||||
updatedAt: '2023-03-26T00:00:00.000Z',
|
||||
createdBy: 'updated-user',
|
||||
updatedBy: 'updated-user',
|
||||
};
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - archive', () => {
|
||||
beforeEach(() => {
|
||||
mockContext.getModificationMetadata.mockResolvedValueOnce(updatedMetadata);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should archive maintenance windows', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedMetadata,
|
||||
},
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
// Move to some time in the future
|
||||
jest.useFakeTimers().setSystemTime(new Date(secondTimestamp));
|
||||
await archive(mockContext, { id: 'test-id', archive: true });
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id'
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
{
|
||||
...mockMaintenanceWindow,
|
||||
events: [
|
||||
{ gte: '2023-02-26T00:00:00.000Z', lte: '2023-02-26T01:00:00.000Z' },
|
||||
{ gte: '2023-03-05T00:00:00.000Z', lte: '2023-03-05T01:00:00.000Z' },
|
||||
],
|
||||
expirationDate: new Date().toISOString(),
|
||||
...updatedMetadata,
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should unarchive maintenance window', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
expirationDate: new Date().toISOString(),
|
||||
},
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedMetadata,
|
||||
},
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
// Move to some time in the future
|
||||
jest.useFakeTimers().setSystemTime(new Date(secondTimestamp));
|
||||
await archive(mockContext, { id: 'test-id', archive: false });
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id'
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
{
|
||||
...mockMaintenanceWindow,
|
||||
events: [
|
||||
{ gte: '2023-02-26T00:00:00.000Z', lte: '2023-02-26T01:00:00.000Z' },
|
||||
{ gte: '2023-03-05T00:00:00.000Z', lte: '2023-03-05T01:00:00.000Z' },
|
||||
],
|
||||
expirationDate: moment.utc().add(1, 'year').toISOString(),
|
||||
...updatedMetadata,
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve finished events when archiving', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const modifiedEvents = [
|
||||
{ gte: '2023-03-26T00:00:00.000Z', lte: '2023-03-26T00:12:34.000Z' },
|
||||
{ gte: '2023-04-01T23:00:00.000Z', lte: '2023-04-01T23:43:21.000Z' },
|
||||
{ gte: '2023-04-08T23:00:00.000Z', lte: '2023-04-09T00:00:00.000Z' },
|
||||
{ gte: '2023-04-15T23:00:00.000Z', lte: '2023-04-15T23:30:00.000Z' },
|
||||
{ gte: '2023-04-22T23:00:00.000Z', lte: '2023-04-23T00:00:00.000Z' },
|
||||
];
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
rRule: {
|
||||
tzid: 'CET',
|
||||
dtstart: '2023-03-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 5,
|
||||
},
|
||||
events: modifiedEvents,
|
||||
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').add(2, 'week').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValue({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedMetadata,
|
||||
},
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-04-16T00:00:00.000Z'));
|
||||
await archive(mockContext, { id: 'test-id', archive: true });
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
{
|
||||
...mockMaintenanceWindow,
|
||||
events: modifiedEvents.slice(0, 4),
|
||||
expirationDate: new Date().toISOString(),
|
||||
...updatedMetadata,
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import {
|
||||
generateMaintenanceWindowEvents,
|
||||
mergeEvents,
|
||||
} from '../generate_maintenance_window_events';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import {
|
||||
MaintenanceWindowSOAttributes,
|
||||
MaintenanceWindow,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
|
||||
export interface ArchiveParams {
|
||||
id: string;
|
||||
archive: boolean;
|
||||
}
|
||||
|
||||
const getArchivedExpirationDate = (shouldArchive: boolean) => {
|
||||
if (shouldArchive) {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
return moment.utc().add(1, 'year').toISOString();
|
||||
};
|
||||
|
||||
export async function archive(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: ArchiveParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
return await retryIfConflicts(
|
||||
context.logger,
|
||||
`maintenanceWindowClient.archive('${params.id})`,
|
||||
async () => {
|
||||
return await archiveWithOCC(context, params);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function archiveWithOCC(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: ArchiveParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
const { savedObjectsClient, getModificationMetadata, logger } = context;
|
||||
const { id, archive: shouldArchive } = params;
|
||||
|
||||
const modificationMetadata = await getModificationMetadata();
|
||||
const expirationDate = getArchivedExpirationDate(shouldArchive);
|
||||
|
||||
try {
|
||||
const { attributes, version } = await savedObjectsClient.get<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
id
|
||||
);
|
||||
|
||||
const events = mergeEvents({
|
||||
newEvents: generateMaintenanceWindowEvents({
|
||||
rRule: attributes.rRule,
|
||||
duration: attributes.duration,
|
||||
expirationDate,
|
||||
}),
|
||||
oldEvents: attributes.events,
|
||||
});
|
||||
|
||||
const result = await savedObjectsClient.update<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
id,
|
||||
{
|
||||
...attributes,
|
||||
events,
|
||||
expirationDate,
|
||||
...modificationMetadata,
|
||||
},
|
||||
{
|
||||
version,
|
||||
}
|
||||
);
|
||||
|
||||
return getMaintenanceWindowFromRaw({
|
||||
attributes: {
|
||||
...attributes,
|
||||
...result.attributes,
|
||||
},
|
||||
id,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to archive maintenance window by id: ${id}, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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-timezone';
|
||||
import { create } from './create';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const updatedMetadata = {
|
||||
createdAt: '2023-03-26T00:00:00.000Z',
|
||||
updatedAt: '2023-03-26T00:00:00.000Z',
|
||||
createdBy: 'updated-user',
|
||||
updatedBy: 'updated-user',
|
||||
};
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - create', () => {
|
||||
beforeEach(() => {
|
||||
mockContext.getModificationMetadata.mockResolvedValueOnce(updatedMetadata);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should create maintenance window with the correct parameters', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
const result = await create(mockContext, {
|
||||
title: mockMaintenanceWindow.title,
|
||||
duration: mockMaintenanceWindow.duration,
|
||||
rRule: mockMaintenanceWindow.rRule,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
expect.objectContaining({
|
||||
title: mockMaintenanceWindow.title,
|
||||
duration: mockMaintenanceWindow.duration,
|
||||
rRule: mockMaintenanceWindow.rRule,
|
||||
enabled: true,
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
...updatedMetadata,
|
||||
}),
|
||||
{
|
||||
id: expect.any(String),
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-id',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { SavedObjectsUtils } from '@kbn/core/server';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import { generateMaintenanceWindowEvents } from '../generate_maintenance_window_events';
|
||||
import {
|
||||
MaintenanceWindowSOAttributes,
|
||||
MaintenanceWindow,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
RRuleParams,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
|
||||
export interface CreateParams {
|
||||
title: string;
|
||||
duration: number;
|
||||
rRule: RRuleParams;
|
||||
}
|
||||
|
||||
export async function create(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: CreateParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
const { savedObjectsClient, getModificationMetadata, logger } = context;
|
||||
const { title, duration, rRule } = params;
|
||||
|
||||
const id = SavedObjectsUtils.generateId();
|
||||
const expirationDate = moment().utc().add(1, 'year').toISOString();
|
||||
const modificationMetadata = await getModificationMetadata();
|
||||
|
||||
try {
|
||||
const events = generateMaintenanceWindowEvents({ rRule, expirationDate, duration });
|
||||
const result = await savedObjectsClient.create<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
title,
|
||||
enabled: true,
|
||||
expirationDate,
|
||||
rRule,
|
||||
duration,
|
||||
events,
|
||||
...modificationMetadata,
|
||||
},
|
||||
{
|
||||
id,
|
||||
}
|
||||
);
|
||||
return getMaintenanceWindowFromRaw({
|
||||
attributes: result.attributes,
|
||||
id: result.id,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to create maintenance window, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { deleteMaintenanceWindow } from './delete';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - delete', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should delete maintenance window by id', async () => {
|
||||
savedObjectsClient.delete.mockResolvedValueOnce({});
|
||||
|
||||
const result = await deleteMaintenanceWindow(mockContext, { id: 'test-id' });
|
||||
|
||||
expect(savedObjectsClient.delete).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id'
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should errors when deletion fails', async () => {
|
||||
savedObjectsClient.delete.mockRejectedValueOnce('something went wrong');
|
||||
|
||||
await expect(async () => {
|
||||
await deleteMaintenanceWindow(mockContext, { id: 'test-id' });
|
||||
}).rejects.toThrowError();
|
||||
|
||||
expect(mockContext.logger.error).toHaveBeenLastCalledWith(
|
||||
'Failed to delete maintenance window by id: test-id, Error: something went wrong'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import {
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
|
||||
export interface DeleteParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function deleteMaintenanceWindow(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: DeleteParams
|
||||
): Promise<{}> {
|
||||
return await retryIfConflicts(
|
||||
context.logger,
|
||||
`maintenanceWindowClient.delete('${params.id}')`,
|
||||
async () => await deleteWithOCC(context, params)
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteWithOCC(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: DeleteParams
|
||||
): Promise<{}> {
|
||||
const { savedObjectsClient, logger } = context;
|
||||
const { id } = params;
|
||||
try {
|
||||
return await savedObjectsClient.delete(MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, id);
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to delete maintenance window by id: ${id}, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { find } from './find';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - find', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should find maintenance windows', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }),
|
||||
id: 'test-1',
|
||||
},
|
||||
{
|
||||
attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }),
|
||||
id: 'test-2',
|
||||
},
|
||||
],
|
||||
} as unknown as SavedObjectsFindResponse);
|
||||
|
||||
const result = await find(mockContext);
|
||||
|
||||
expect(savedObjectsClient.find).toHaveBeenLastCalledWith({
|
||||
type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(result.data.length).toEqual(2);
|
||||
expect(result.data[0].id).toEqual('test-1');
|
||||
expect(result.data[1].id).toEqual('test-2');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import {
|
||||
MaintenanceWindowSOAttributes,
|
||||
MaintenanceWindow,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
|
||||
export interface FindResult {
|
||||
data: MaintenanceWindow[];
|
||||
}
|
||||
|
||||
export async function find(context: MaintenanceWindowClientContext): Promise<FindResult> {
|
||||
const { savedObjectsClient, logger } = context;
|
||||
|
||||
try {
|
||||
const result = await savedObjectsClient.find<MaintenanceWindowSOAttributes>({
|
||||
type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return {
|
||||
data: result.saved_objects.map((so) =>
|
||||
getMaintenanceWindowFromRaw({
|
||||
attributes: so.attributes,
|
||||
id: so.id,
|
||||
})
|
||||
),
|
||||
};
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to find maintenance window, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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-timezone';
|
||||
import { RRule } from 'rrule';
|
||||
import { finish } from './finish';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const firstTimestamp = '2023-02-26T00:00:00.000Z';
|
||||
|
||||
const updatedMetadata = {
|
||||
createdAt: '2023-03-26T00:00:00.000Z',
|
||||
updatedAt: '2023-03-26T00:00:00.000Z',
|
||||
createdBy: 'updated-user',
|
||||
updatedBy: 'updated-user',
|
||||
};
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - finish', () => {
|
||||
beforeEach(() => {
|
||||
mockContext.getModificationMetadata.mockResolvedValueOnce(updatedMetadata);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should finish the currently running maintenance window event', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
duration: 60 * 60 * 1000,
|
||||
expirationDate: moment.utc().add(1, 'year').toISOString(),
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
dtstart: moment().utc().toISOString(),
|
||||
freq: RRule.WEEKLY,
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedMetadata,
|
||||
events: [
|
||||
{ gte: '2023-02-26T00:00:00.000Z', lte: '2023-02-26T00:30:00.000Z' },
|
||||
{ gte: '2023-03-05T00:00:00.000Z', lte: '2023-03-05T01:00:00.000Z' },
|
||||
],
|
||||
},
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
// Move 30 mins into the future
|
||||
jest.useFakeTimers().setSystemTime(moment.utc(firstTimestamp).add(30, 'minute').toDate());
|
||||
|
||||
const result = await finish(mockContext, { id: 'test-id' });
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
expect.objectContaining({
|
||||
expirationDate: moment.utc().add(1, 'year').toISOString(),
|
||||
events: [
|
||||
// Event ends 30 mins earlier, just like expected}
|
||||
{ gte: '2023-02-26T00:00:00.000Z', lte: '2023-02-26T00:30:00.000Z' },
|
||||
{ gte: '2023-03-05T00:00:00.000Z', lte: '2023-03-05T01:00:00.000Z' },
|
||||
],
|
||||
}),
|
||||
{ version: '123' }
|
||||
);
|
||||
expect(result.status).toEqual('upcoming');
|
||||
expect(result.eventStartTime).toEqual('2023-03-05T00:00:00.000Z');
|
||||
expect(result.eventEndTime).toEqual('2023-03-05T01:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should keep events that were finished in the past', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const modifiedEvents = [
|
||||
{ gte: '2023-03-26T00:00:00.000Z', lte: '2023-03-26T00:12:34.000Z' },
|
||||
{ gte: '2023-04-01T23:00:00.000Z', lte: '2023-04-01T23:43:21.000Z' },
|
||||
];
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
rRule: {
|
||||
tzid: 'CET',
|
||||
dtstart: '2023-03-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 5,
|
||||
},
|
||||
events: modifiedEvents,
|
||||
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').add(2, 'week').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValue({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedMetadata,
|
||||
},
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-04-15T23:30:00.000Z'));
|
||||
|
||||
await finish(mockContext, { id: 'test-id' });
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
expect.objectContaining({
|
||||
events: [
|
||||
...modifiedEvents,
|
||||
{ gte: '2023-04-08T23:00:00.000Z', lte: '2023-04-09T00:00:00.000Z' },
|
||||
{ gte: '2023-04-15T23:00:00.000Z', lte: '2023-04-15T23:30:00.000Z' },
|
||||
{ gte: '2023-04-22T23:00:00.000Z', lte: '2023-04-23T00:00:00.000Z' },
|
||||
],
|
||||
}),
|
||||
{ version: '123' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if trying to finish a maintenance window that is not running', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
duration: 60 * 60 * 1000, //
|
||||
expirationDate: moment.utc().add(1, 'year').toISOString(),
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
dtstart: moment().utc().toISOString(),
|
||||
freq: RRule.WEEKLY,
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
// Move 2 hours into the future
|
||||
jest.useFakeTimers().setSystemTime(moment.utc(firstTimestamp).add(2, 'hours').toDate());
|
||||
|
||||
await expect(async () => {
|
||||
await finish(mockContext, { id: 'test-id' });
|
||||
}).rejects.toThrowError();
|
||||
|
||||
expect(mockContext.logger.error).toHaveBeenLastCalledWith(
|
||||
'Failed to finish maintenance window by id: test-id, Error: Error: Cannot finish maintenance window that is not running'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import {
|
||||
generateMaintenanceWindowEvents,
|
||||
mergeEvents,
|
||||
} from '../generate_maintenance_window_events';
|
||||
import { getMaintenanceWindowDateAndStatus } from '../get_maintenance_window_date_and_status';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import {
|
||||
MaintenanceWindowSOAttributes,
|
||||
MaintenanceWindow,
|
||||
DateRange,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
MaintenanceWindowClientContext,
|
||||
MaintenanceWindowStatus,
|
||||
} from '../../../common';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
|
||||
export interface FinishParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function finish(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: FinishParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
return await retryIfConflicts(
|
||||
context.logger,
|
||||
`maintenanceWindowClient.finish('${params.id})`,
|
||||
async () => {
|
||||
return await finishWithOCC(context, params);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function finishWithOCC(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: FinishParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
const { savedObjectsClient, getModificationMetadata, logger } = context;
|
||||
const { id } = params;
|
||||
|
||||
const modificationMetadata = await getModificationMetadata();
|
||||
const now = new Date();
|
||||
const expirationDate = moment.utc(now).add(1, 'year').toDate();
|
||||
|
||||
try {
|
||||
const {
|
||||
attributes,
|
||||
version,
|
||||
id: fetchedId,
|
||||
} = await savedObjectsClient.get<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
id
|
||||
);
|
||||
|
||||
// Generate new events with new expiration date
|
||||
const newEvents = generateMaintenanceWindowEvents({
|
||||
rRule: attributes.rRule,
|
||||
duration: attributes.duration,
|
||||
expirationDate: expirationDate.toISOString(),
|
||||
});
|
||||
|
||||
// Merge it with the old events
|
||||
const events = mergeEvents({
|
||||
newEvents,
|
||||
oldEvents: attributes.events,
|
||||
});
|
||||
|
||||
// Find the current event and status of the maintenance window
|
||||
const { status, index } = getMaintenanceWindowDateAndStatus({
|
||||
events,
|
||||
dateToCompare: now,
|
||||
expirationDate,
|
||||
});
|
||||
|
||||
// Throw if the maintenance window is not running, or event doesn't exist
|
||||
if (status !== MaintenanceWindowStatus.Running) {
|
||||
throw Boom.badRequest('Cannot finish maintenance window that is not running');
|
||||
}
|
||||
if (typeof index !== 'number' || !events[index]) {
|
||||
throw Boom.badRequest('Cannot find maintenance window event to finish');
|
||||
}
|
||||
|
||||
// Update the running event to finish now
|
||||
const eventToFinish: DateRange = {
|
||||
gte: events[index].gte,
|
||||
lte: now.toISOString(),
|
||||
};
|
||||
|
||||
// Update the events with the new finished event
|
||||
const eventsWithFinishedEvent = [...events];
|
||||
eventsWithFinishedEvent[index] = eventToFinish;
|
||||
|
||||
const result = await savedObjectsClient.update<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
fetchedId,
|
||||
{
|
||||
events: eventsWithFinishedEvent,
|
||||
expirationDate: expirationDate.toISOString(),
|
||||
...modificationMetadata,
|
||||
},
|
||||
{
|
||||
version,
|
||||
}
|
||||
);
|
||||
|
||||
return getMaintenanceWindowFromRaw({
|
||||
attributes: {
|
||||
...attributes,
|
||||
...result.attributes,
|
||||
},
|
||||
id: result.id,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to finish maintenance window by id: ${id}, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { get } from './get';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - get', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should get maintenance window by id', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
const result = await get(mockContext, { id: 'test-id' });
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id'
|
||||
);
|
||||
expect(result.id).toEqual('test-id');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import {
|
||||
MaintenanceWindowSOAttributes,
|
||||
MaintenanceWindow,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
|
||||
export interface GetParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function get(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: GetParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
const { savedObjectsClient, logger } = context;
|
||||
const { id } = params;
|
||||
try {
|
||||
const result = await savedObjectsClient.get<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
id
|
||||
);
|
||||
|
||||
return getMaintenanceWindowFromRaw({
|
||||
attributes: result.attributes,
|
||||
id: result.id,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to get maintenance window by id: ${id}, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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 { getActiveMaintenanceWindows } from './get_active_maintenance_windows';
|
||||
import { toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - getActiveMaintenanceWindows', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return active maintenance windows', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }),
|
||||
id: 'test-1',
|
||||
},
|
||||
{
|
||||
attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }),
|
||||
id: 'test-2',
|
||||
},
|
||||
],
|
||||
} as unknown as SavedObjectsFindResponse);
|
||||
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
const result = await getActiveMaintenanceWindows(mockContext, {
|
||||
start: startDate,
|
||||
interval: '1h',
|
||||
});
|
||||
|
||||
const findCallParams = savedObjectsClient.find.mock.calls[0][0];
|
||||
|
||||
expect(findCallParams.type).toEqual(MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE);
|
||||
|
||||
expect(toElasticsearchQuery(findCallParams.filter)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"maintenance-window.attributes.events": Object {
|
||||
"gte": "2023-02-26T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"maintenance-window.attributes.events": Object {
|
||||
"lte": "2023-02-26T01:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"maintenance-window.attributes.enabled": "true",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'test-1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'test-2',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array if there are no active maintenance windows', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
} as unknown as SavedObjectsFindResponse);
|
||||
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
const result = await getActiveMaintenanceWindows(mockContext, {
|
||||
start: startDate,
|
||||
interval: '4d',
|
||||
});
|
||||
|
||||
const findCallParams = savedObjectsClient.find.mock.calls[0][0];
|
||||
|
||||
expect(findCallParams.type).toEqual(MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE);
|
||||
expect(toElasticsearchQuery(findCallParams.filter)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"maintenance-window.attributes.events": Object {
|
||||
"gte": "2023-02-26T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"maintenance-window.attributes.events": Object {
|
||||
"lte": "2023-03-02T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"maintenance-window.attributes.enabled": "true",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should log and throw if an error is thrown', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
savedObjectsClient.find.mockRejectedValueOnce('something went wrong');
|
||||
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
await expect(async () => {
|
||||
await getActiveMaintenanceWindows(mockContext, {
|
||||
start: startDate,
|
||||
interval: '4d',
|
||||
});
|
||||
}).rejects.toThrowError();
|
||||
|
||||
expect(mockContext.logger.error).toHaveBeenLastCalledWith(
|
||||
'Failed to find active maintenance window by interval: 4d with start date: 2023-02-26T00:00:00.000Z, Error: something went wrong'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import moment from 'moment';
|
||||
import { nodeBuilder, fromKueryExpression } from '@kbn/es-query';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import {
|
||||
MaintenanceWindow,
|
||||
MaintenanceWindowSOAttributes,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
parseDuration,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
|
||||
export interface MaintenanceWindowAggregationResult {
|
||||
maintenanceWindow: {
|
||||
buckets: Array<{
|
||||
key_as_string: string;
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ActiveParams {
|
||||
start?: string;
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export async function getActiveMaintenanceWindows(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: ActiveParams
|
||||
): Promise<MaintenanceWindow[]> {
|
||||
const { savedObjectsClient, logger } = context;
|
||||
const { start, interval } = params;
|
||||
|
||||
const startDate = start ? new Date(start) : new Date();
|
||||
const duration = parseDuration(interval);
|
||||
const endDate = moment.utc(startDate).add(duration, 'ms').toDate();
|
||||
|
||||
const startDateISO = startDate.toISOString();
|
||||
const endDateISO = endDate.toISOString();
|
||||
|
||||
const filter = nodeBuilder.and([
|
||||
nodeBuilder.and([
|
||||
fromKueryExpression(`maintenance-window.attributes.events >= "${startDateISO}"`),
|
||||
fromKueryExpression(`maintenance-window.attributes.events <= "${endDateISO}"`),
|
||||
]),
|
||||
nodeBuilder.is('maintenance-window.attributes.enabled', 'true'),
|
||||
]);
|
||||
|
||||
try {
|
||||
const result = await savedObjectsClient.find<MaintenanceWindowSOAttributes>({
|
||||
type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
filter,
|
||||
});
|
||||
|
||||
return result.saved_objects.map((so) =>
|
||||
getMaintenanceWindowFromRaw({
|
||||
attributes: so.attributes,
|
||||
id: so.id,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to find active maintenance window by interval: ${interval} with start date: ${startDate.toISOString()}, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { RRule } from 'rrule';
|
||||
import { MaintenanceWindowSOAttributes } from '../../../common';
|
||||
|
||||
export const getMockMaintenanceWindow = (
|
||||
overwrites?: Partial<MaintenanceWindowSOAttributes>
|
||||
): MaintenanceWindowSOAttributes => {
|
||||
return {
|
||||
title: 'test-title',
|
||||
duration: 60 * 60 * 1000,
|
||||
enabled: true,
|
||||
rRule: {
|
||||
tzid: 'UTC',
|
||||
dtstart: '2023-02-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 2,
|
||||
},
|
||||
events: [
|
||||
{
|
||||
gte: '2023-02-26T00:00:00.000Z',
|
||||
lte: '2023-02-26T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
gte: '2023-03-05T00:00:00.000Z',
|
||||
lte: '2023-03-05T01:00:00.000Z',
|
||||
},
|
||||
],
|
||||
createdAt: '2023-02-26T00:00:00.000Z',
|
||||
updatedAt: '2023-02-26T00:00:00.000Z',
|
||||
createdBy: 'test-user',
|
||||
updatedBy: 'test-user',
|
||||
expirationDate: new Date().toISOString(),
|
||||
...overwrites,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* 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-timezone';
|
||||
import { RRule } from 'rrule';
|
||||
import { update } from './update';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientContext,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../../common';
|
||||
import { getMockMaintenanceWindow } from './test_helpers';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
const firstTimestamp = '2023-02-26T00:00:00.000Z';
|
||||
const secondTimestamp = '2023-03-26T00:00:00.000Z';
|
||||
|
||||
const updatedAttributes = {
|
||||
title: 'updated-title',
|
||||
enabled: false,
|
||||
duration: 2 * 60 * 60 * 1000,
|
||||
rRule: {
|
||||
tzid: 'CET',
|
||||
dtstart: '2023-03-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedMetadata = {
|
||||
createdAt: '2023-03-26T00:00:00.000Z',
|
||||
updatedAt: '2023-03-26T00:00:00.000Z',
|
||||
createdBy: 'updated-user',
|
||||
updatedBy: 'updated-user',
|
||||
};
|
||||
|
||||
const mockContext: jest.Mocked<MaintenanceWindowClientContext> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getModificationMetadata: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
describe('MaintenanceWindowClient - update', () => {
|
||||
beforeEach(() => {
|
||||
mockContext.getModificationMetadata.mockResolvedValueOnce(updatedMetadata);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should call update with the correct parameters', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedAttributes,
|
||||
...updatedMetadata,
|
||||
},
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date(secondTimestamp));
|
||||
|
||||
const result = await update(mockContext, {
|
||||
id: 'test-id',
|
||||
...updatedAttributes,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id'
|
||||
);
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
{
|
||||
...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
|
||||
],
|
||||
expirationDate: moment(new Date(secondTimestamp)).tz('UTC').add(1, 'year').toISOString(),
|
||||
...updatedMetadata,
|
||||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
// Only these 3 properties are worth asserting since the rest come from mocks
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-id',
|
||||
status: 'finished',
|
||||
eventStartTime: '2023-03-05T00:00:00.000Z',
|
||||
eventEndTime: '2023-03-05T01:00:00.000Z',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not regenerate all events if rrule and duration did not change', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
|
||||
const modifiedEvents = [
|
||||
{ gte: '2023-03-26T00:00:00.000Z', lte: '2023-03-26T00:12:34.000Z' },
|
||||
{ gte: '2023-04-01T23:00:00.000Z', lte: '2023-04-01T23:43:21.000Z' },
|
||||
];
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
rRule: {
|
||||
tzid: 'CET',
|
||||
dtstart: '2023-03-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 5,
|
||||
},
|
||||
events: modifiedEvents,
|
||||
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').add(2, 'week').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
savedObjectsClient.update.mockResolvedValue({
|
||||
attributes: {
|
||||
...mockMaintenanceWindow,
|
||||
...updatedAttributes,
|
||||
...updatedMetadata,
|
||||
},
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObjectsUpdateResponse);
|
||||
|
||||
// Update without changing duration or rrule
|
||||
await update(mockContext, { id: 'test-id' });
|
||||
// Events keep the previous modified events, but adds on the new events
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
expect.objectContaining({
|
||||
events: [...modifiedEvents, expect.any(Object), expect.any(Object), expect.any(Object)],
|
||||
}),
|
||||
{ version: '123' }
|
||||
);
|
||||
|
||||
// Update with changing rrule
|
||||
await update(mockContext, {
|
||||
id: 'test-id',
|
||||
rRule: {
|
||||
tzid: 'CET',
|
||||
dtstart: '2023-03-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
// All events are regenerated
|
||||
expect(savedObjectsClient.update).toHaveBeenLastCalledWith(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
'test-id',
|
||||
expect.objectContaining({
|
||||
events: [
|
||||
{ gte: '2023-03-26T00:00:00.000Z', lte: '2023-03-26T01:00:00.000Z' },
|
||||
{ gte: '2023-04-01T23:00:00.000Z', lte: '2023-04-02T00:00:00.000Z' },
|
||||
],
|
||||
}),
|
||||
{ version: '123' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if updating a maintenance window that has expired', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').subtract(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
} as unknown as SavedObject);
|
||||
|
||||
await expect(async () => {
|
||||
await update(mockContext, { id: 'test-id', ...updatedAttributes });
|
||||
}).rejects.toThrowError();
|
||||
|
||||
expect(mockContext.logger.error).toHaveBeenLastCalledWith(
|
||||
'Failed to update maintenance window by id: test-id, Error: Error: Cannot edit archived maintenance windows'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { getMaintenanceWindowFromRaw } from '../get_maintenance_window_from_raw';
|
||||
import {
|
||||
generateMaintenanceWindowEvents,
|
||||
shouldRegenerateEvents,
|
||||
mergeEvents,
|
||||
} from '../generate_maintenance_window_events';
|
||||
import {
|
||||
MaintenanceWindow,
|
||||
MaintenanceWindowSOAttributes,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
RRuleParams,
|
||||
MaintenanceWindowClientContext,
|
||||
} from '../../../common';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
|
||||
export interface UpdateParams {
|
||||
id: string;
|
||||
title?: string;
|
||||
enabled?: boolean;
|
||||
duration?: number;
|
||||
rRule?: RRuleParams;
|
||||
}
|
||||
|
||||
export async function update(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: UpdateParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
return await retryIfConflicts(
|
||||
context.logger,
|
||||
`maintenanceWindowClient.update('${params.id})`,
|
||||
async () => {
|
||||
return await updateWithOCC(context, params);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function updateWithOCC(
|
||||
context: MaintenanceWindowClientContext,
|
||||
params: UpdateParams
|
||||
): Promise<MaintenanceWindow> {
|
||||
const { savedObjectsClient, getModificationMetadata, logger } = context;
|
||||
const { id, title, enabled, duration, rRule } = params;
|
||||
|
||||
try {
|
||||
const {
|
||||
attributes,
|
||||
version,
|
||||
id: fetchedId,
|
||||
} = await savedObjectsClient.get<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
id
|
||||
);
|
||||
|
||||
if (moment.utc(attributes.expirationDate).isBefore(new Date())) {
|
||||
throw Boom.badRequest('Cannot edit archived maintenance windows');
|
||||
}
|
||||
|
||||
const expirationDate = moment.utc().add(1, 'year').toISOString();
|
||||
const modificationMetadata = await getModificationMetadata();
|
||||
|
||||
let events = generateMaintenanceWindowEvents({
|
||||
rRule: rRule || attributes.rRule,
|
||||
duration: typeof duration === 'number' ? duration : attributes.duration,
|
||||
expirationDate,
|
||||
});
|
||||
|
||||
if (!shouldRegenerateEvents({ maintenanceWindow: attributes, rRule, duration })) {
|
||||
events = mergeEvents({ oldEvents: attributes.events, newEvents: events });
|
||||
}
|
||||
|
||||
const result = await savedObjectsClient.update<MaintenanceWindowSOAttributes>(
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
fetchedId,
|
||||
{
|
||||
...attributes,
|
||||
title,
|
||||
enabled: typeof enabled === 'boolean' ? enabled : attributes.enabled,
|
||||
expirationDate,
|
||||
duration,
|
||||
rRule,
|
||||
events,
|
||||
...modificationMetadata,
|
||||
},
|
||||
{
|
||||
version,
|
||||
}
|
||||
);
|
||||
|
||||
return getMaintenanceWindowFromRaw({
|
||||
attributes: {
|
||||
...attributes,
|
||||
...result.attributes,
|
||||
},
|
||||
id: result.id,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = `Failed to update maintenance window by id: ${id}, Error: ${e}`;
|
||||
logger.error(errorMessage);
|
||||
throw Boom.boomify(e, { message: errorMessage });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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 { Request } from '@hapi/hapi';
|
||||
import { CoreKibanaRequest } from '@kbn/core/server';
|
||||
import {
|
||||
MaintenanceWindowClientFactory,
|
||||
MaintenanceWindowClientFactoryOpts,
|
||||
} from './maintenance_window_client_factory';
|
||||
import {
|
||||
savedObjectsClientMock,
|
||||
savedObjectsServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin/common/model';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
|
||||
import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE } from '../common';
|
||||
|
||||
jest.mock('./maintenance_window_client');
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const savedObjectsService = savedObjectsServiceMock.createInternalStartContract();
|
||||
|
||||
const securityPluginStart = securityMock.createStart();
|
||||
|
||||
const maintenanceWindowClientFactoryParams: jest.Mocked<MaintenanceWindowClientFactoryOpts> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
savedObjectsService,
|
||||
};
|
||||
|
||||
const fakeRequest = {
|
||||
app: {},
|
||||
headers: {},
|
||||
getBasePath: () => '',
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
getSavedObjectsClient: () => savedObjectsClient,
|
||||
} as unknown as Request;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('creates a maintenance window client with proper constructor arguments when security is enabled', async () => {
|
||||
const factory = new MaintenanceWindowClientFactory();
|
||||
factory.initialize({
|
||||
securityPluginStart,
|
||||
...maintenanceWindowClientFactoryParams,
|
||||
});
|
||||
const request = CoreKibanaRequest.from(fakeRequest);
|
||||
|
||||
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
|
||||
|
||||
factory.createWithAuthorization(request);
|
||||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
includedHiddenTypes: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
|
||||
});
|
||||
|
||||
const { MaintenanceWindowClient } = jest.requireMock('./maintenance_window_client');
|
||||
|
||||
expect(MaintenanceWindowClient).toHaveBeenCalledWith({
|
||||
logger: maintenanceWindowClientFactoryParams.logger,
|
||||
savedObjectsClient,
|
||||
getUserName: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('creates a maintenance window client with proper constructor arguments', async () => {
|
||||
const factory = new MaintenanceWindowClientFactory();
|
||||
factory.initialize(maintenanceWindowClientFactoryParams);
|
||||
const request = CoreKibanaRequest.from(fakeRequest);
|
||||
|
||||
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
|
||||
|
||||
factory.createWithAuthorization(request);
|
||||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
includedHiddenTypes: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
|
||||
});
|
||||
|
||||
const { MaintenanceWindowClient } = jest.requireMock('./maintenance_window_client');
|
||||
|
||||
expect(MaintenanceWindowClient).toHaveBeenCalledWith({
|
||||
logger: maintenanceWindowClientFactoryParams.logger,
|
||||
savedObjectsClient,
|
||||
getUserName: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('creates an unauthorized maintenance window client', async () => {
|
||||
const factory = new MaintenanceWindowClientFactory();
|
||||
factory.initialize({
|
||||
securityPluginStart,
|
||||
...maintenanceWindowClientFactoryParams,
|
||||
});
|
||||
const request = CoreKibanaRequest.from(fakeRequest);
|
||||
|
||||
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
|
||||
|
||||
factory.create(request);
|
||||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
excludedExtensions: [SECURITY_EXTENSION_ID],
|
||||
includedHiddenTypes: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
|
||||
});
|
||||
|
||||
const { MaintenanceWindowClient } = jest.requireMock('./maintenance_window_client');
|
||||
|
||||
expect(MaintenanceWindowClient).toHaveBeenCalledWith({
|
||||
logger: maintenanceWindowClientFactoryParams.logger,
|
||||
savedObjectsClient,
|
||||
getUserName: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('getUserName() returns null when security is disabled', async () => {
|
||||
const factory = new MaintenanceWindowClientFactory();
|
||||
factory.initialize(maintenanceWindowClientFactoryParams);
|
||||
const request = CoreKibanaRequest.from(fakeRequest);
|
||||
|
||||
factory.createWithAuthorization(request);
|
||||
const constructorCall = jest.requireMock('./maintenance_window_client').MaintenanceWindowClient
|
||||
.mock.calls[0][0];
|
||||
|
||||
const userNameResult = await constructorCall.getUserName();
|
||||
expect(userNameResult).toEqual(null);
|
||||
});
|
||||
|
||||
test('getUserName() returns a name when security is enabled', async () => {
|
||||
const factory = new MaintenanceWindowClientFactory();
|
||||
factory.initialize({
|
||||
securityPluginStart,
|
||||
...maintenanceWindowClientFactoryParams,
|
||||
});
|
||||
const request = CoreKibanaRequest.from(fakeRequest);
|
||||
|
||||
factory.createWithAuthorization(request);
|
||||
|
||||
const constructorCall = jest.requireMock('./maintenance_window_client').MaintenanceWindowClient
|
||||
.mock.calls[0][0];
|
||||
|
||||
securityPluginStart.authc.getCurrentUser.mockReturnValueOnce({
|
||||
username: 'testname',
|
||||
} as unknown as AuthenticatedUser);
|
||||
const userNameResult = await constructorCall.getUserName();
|
||||
expect(userNameResult).toEqual('testname');
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 {
|
||||
KibanaRequest,
|
||||
Logger,
|
||||
SavedObjectsServiceStart,
|
||||
SECURITY_EXTENSION_ID,
|
||||
} from '@kbn/core/server';
|
||||
import { SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import { MaintenanceWindowClient } from './maintenance_window_client';
|
||||
import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE } from '../common';
|
||||
|
||||
export interface MaintenanceWindowClientFactoryOpts {
|
||||
logger: Logger;
|
||||
savedObjectsService: SavedObjectsServiceStart;
|
||||
securityPluginStart?: SecurityPluginStart;
|
||||
}
|
||||
|
||||
export class MaintenanceWindowClientFactory {
|
||||
private isInitialized = false;
|
||||
private logger!: Logger;
|
||||
private savedObjectsService!: SavedObjectsServiceStart;
|
||||
private securityPluginStart?: SecurityPluginStart;
|
||||
|
||||
public initialize(options: MaintenanceWindowClientFactoryOpts) {
|
||||
if (this.isInitialized) {
|
||||
throw new Error('MaintenanceWindowClientFactory already initialized');
|
||||
}
|
||||
this.isInitialized = true;
|
||||
this.logger = options.logger;
|
||||
this.savedObjectsService = options.savedObjectsService;
|
||||
this.securityPluginStart = options.securityPluginStart;
|
||||
}
|
||||
|
||||
private createMaintenanceWindowClient(request: KibanaRequest, withAuth: boolean) {
|
||||
const { securityPluginStart } = this;
|
||||
const savedObjectsClient = this.savedObjectsService.getScopedClient(request, {
|
||||
includedHiddenTypes: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
|
||||
...(withAuth ? {} : { excludedExtensions: [SECURITY_EXTENSION_ID] }),
|
||||
});
|
||||
|
||||
return new MaintenanceWindowClient({
|
||||
logger: this.logger,
|
||||
savedObjectsClient,
|
||||
async getUserName() {
|
||||
if (!securityPluginStart || !request) {
|
||||
return null;
|
||||
}
|
||||
const user = securityPluginStart.authc.getCurrentUser(request);
|
||||
return user ? user.username : null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public createWithAuthorization(request: KibanaRequest) {
|
||||
return this.createMaintenanceWindowClient(request, true);
|
||||
}
|
||||
|
||||
public create(request: KibanaRequest) {
|
||||
return this.createMaintenanceWindowClient(request, false);
|
||||
}
|
||||
}
|
56
x-pack/plugins/alerting/server/maintenance_window_feature.ts
Normal file
56
x-pack/plugins/alerting/server/maintenance_window_feature.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import {
|
||||
MAINTENANCE_WINDOW_FEATURE_ID,
|
||||
MAINTENANCE_WINDOW_API_PRIVILEGES,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../common';
|
||||
|
||||
export const maintenanceWindowFeature: KibanaFeatureConfig = {
|
||||
id: MAINTENANCE_WINDOW_FEATURE_ID,
|
||||
name: i18n.translate('xpack.alerting.feature.maintenanceWindowFeatureName', {
|
||||
defaultMessage: 'Maintenance Window',
|
||||
}),
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
app: [],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
privileges: {
|
||||
all: {
|
||||
app: [],
|
||||
api: [
|
||||
MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW,
|
||||
MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW,
|
||||
],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show', 'save'],
|
||||
},
|
||||
read: {
|
||||
app: [],
|
||||
api: [MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -59,6 +59,7 @@ import { RuleTypeRegistry } from './rule_type_registry';
|
|||
import { TaskRunnerFactory } from './task_runner';
|
||||
import { RulesClientFactory } from './rules_client_factory';
|
||||
import { RulesSettingsClientFactory } from './rules_settings_client_factory';
|
||||
import { MaintenanceWindowClientFactory } from './maintenance_window_client_factory';
|
||||
import { ILicenseState, LicenseState } from './lib/license_state';
|
||||
import { AlertingRequestHandlerContext, ALERTS_FEATURE_ID } from './types';
|
||||
import { defineRoutes } from './routes';
|
||||
|
@ -94,6 +95,7 @@ import {
|
|||
errorResult,
|
||||
} from './alerts_service';
|
||||
import { rulesSettingsFeature } from './rules_settings_feature';
|
||||
import { maintenanceWindowFeature } from './maintenance_window_feature';
|
||||
|
||||
export const EVENT_LOG_PROVIDER = 'alerting';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -187,6 +189,7 @@ export class AlertingPlugin {
|
|||
private readonly rulesClientFactory: RulesClientFactory;
|
||||
private readonly alertingAuthorizationClientFactory: AlertingAuthorizationClientFactory;
|
||||
private readonly rulesSettingsClientFactory: RulesSettingsClientFactory;
|
||||
private readonly maintenanceWindowClientFactory: MaintenanceWindowClientFactory;
|
||||
private readonly telemetryLogger: Logger;
|
||||
private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
private eventLogService?: IEventLogService;
|
||||
|
@ -205,6 +208,7 @@ export class AlertingPlugin {
|
|||
this.alertsService = null;
|
||||
this.alertingAuthorizationClientFactory = new AlertingAuthorizationClientFactory();
|
||||
this.rulesSettingsClientFactory = new RulesSettingsClientFactory();
|
||||
this.maintenanceWindowClientFactory = new MaintenanceWindowClientFactory();
|
||||
this.telemetryLogger = initializerContext.logger.get('usage');
|
||||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
this.inMemoryMetrics = new InMemoryMetrics(initializerContext.logger.get('in_memory_metrics'));
|
||||
|
@ -232,6 +236,8 @@ export class AlertingPlugin {
|
|||
|
||||
plugins.features.registerKibanaFeature(rulesSettingsFeature);
|
||||
|
||||
plugins.features.registerKibanaFeature(maintenanceWindowFeature);
|
||||
|
||||
this.isESOCanEncrypt = plugins.encryptedSavedObjects.canEncrypt;
|
||||
|
||||
if (!this.isESOCanEncrypt) {
|
||||
|
@ -417,6 +423,7 @@ export class AlertingPlugin {
|
|||
rulesClientFactory,
|
||||
alertingAuthorizationClientFactory,
|
||||
rulesSettingsClientFactory,
|
||||
maintenanceWindowClientFactory,
|
||||
security,
|
||||
licenseState,
|
||||
} = this;
|
||||
|
@ -471,6 +478,12 @@ export class AlertingPlugin {
|
|||
securityPluginStart: plugins.security,
|
||||
});
|
||||
|
||||
maintenanceWindowClientFactory.initialize({
|
||||
logger: this.logger,
|
||||
savedObjectsService: core.savedObjects,
|
||||
securityPluginStart: plugins.security,
|
||||
});
|
||||
|
||||
const getRulesClientWithRequest = (request: KibanaRequest) => {
|
||||
if (isESOCanEncrypt !== true) {
|
||||
throw new Error(
|
||||
|
@ -544,7 +557,12 @@ export class AlertingPlugin {
|
|||
private createRouteHandlerContext = (
|
||||
core: CoreSetup<AlertingPluginsStart, unknown>
|
||||
): IContextProvider<AlertingRequestHandlerContext, 'alerting'> => {
|
||||
const { ruleTypeRegistry, rulesClientFactory, rulesSettingsClientFactory } = this;
|
||||
const {
|
||||
ruleTypeRegistry,
|
||||
rulesClientFactory,
|
||||
rulesSettingsClientFactory,
|
||||
maintenanceWindowClientFactory,
|
||||
} = this;
|
||||
return async function alertsRouteHandlerContext(context, request) {
|
||||
const [{ savedObjects }] = await core.getStartServices();
|
||||
return {
|
||||
|
@ -554,6 +572,9 @@ export class AlertingPlugin {
|
|||
getRulesSettingsClient: () => {
|
||||
return rulesSettingsClientFactory.createWithAuthorization(request);
|
||||
},
|
||||
getMaintenanceWindowClient: () => {
|
||||
return maintenanceWindowClientFactory.createWithAuthorization(request);
|
||||
},
|
||||
listTypes: ruleTypeRegistry!.list.bind(ruleTypeRegistry!),
|
||||
getFrameworkHealth: async () =>
|
||||
await getHealth(savedObjects.createInternalRepository(['alert'])),
|
||||
|
|
|
@ -11,6 +11,10 @@ import type { MethodKeysOf } from '@kbn/utility-types';
|
|||
import { httpServerMock } from '@kbn/core/server/mocks';
|
||||
import { rulesClientMock, RulesClientMock } from '../rules_client.mock';
|
||||
import { rulesSettingsClientMock, RulesSettingsClientMock } from '../rules_settings_client.mock';
|
||||
import {
|
||||
maintenanceWindowClientMock,
|
||||
MaintenanceWindowClientMock,
|
||||
} from '../maintenance_window_client.mock';
|
||||
import { AlertsHealth, RuleType } from '../../common';
|
||||
import type { AlertingRequestHandlerContext } from '../types';
|
||||
|
||||
|
@ -18,12 +22,14 @@ export function mockHandlerArguments(
|
|||
{
|
||||
rulesClient = rulesClientMock.create(),
|
||||
rulesSettingsClient = rulesSettingsClientMock.create(),
|
||||
maintenanceWindowClient = maintenanceWindowClientMock.create(),
|
||||
listTypes: listTypesRes = [],
|
||||
getFrameworkHealth,
|
||||
areApiKeysEnabled,
|
||||
}: {
|
||||
rulesClient?: RulesClientMock;
|
||||
rulesSettingsClient?: RulesSettingsClientMock;
|
||||
maintenanceWindowClient?: MaintenanceWindowClientMock;
|
||||
listTypes?: RuleType[];
|
||||
getFrameworkHealth?: jest.MockInstance<Promise<AlertsHealth>, []> &
|
||||
(() => Promise<AlertsHealth>);
|
||||
|
@ -47,6 +53,9 @@ export function mockHandlerArguments(
|
|||
getRulesSettingsClient() {
|
||||
return rulesSettingsClient || rulesSettingsClientMock.create();
|
||||
},
|
||||
getMaintenanceWindowClient() {
|
||||
return maintenanceWindowClient || maintenanceWindowClientMock.create();
|
||||
},
|
||||
getFrameworkHealth,
|
||||
areApiKeysEnabled: areApiKeysEnabled ? areApiKeysEnabled : () => Promise.resolve(true),
|
||||
},
|
||||
|
|
|
@ -46,6 +46,14 @@ import { getFlappingSettingsRoute } from './get_flapping_settings';
|
|||
import { updateFlappingSettingsRoute } from './update_flapping_settings';
|
||||
import { getRuleTagsRoute } from './get_rule_tags';
|
||||
|
||||
import { createMaintenanceWindowRoute } from './maintenance_window/create_maintenance_window';
|
||||
import { getMaintenanceWindowRoute } from './maintenance_window/get_maintenance_window';
|
||||
import { updateMaintenanceWindowRoute } from './maintenance_window/update_maintenance_window';
|
||||
import { deleteMaintenanceWindowRoute } from './maintenance_window/delete_maintenance_window';
|
||||
import { findMaintenanceWindowsRoute } from './maintenance_window/find_maintenance_windows';
|
||||
import { archiveMaintenanceWindowRoute } from './maintenance_window/archive_maintenance_window';
|
||||
import { finishMaintenanceWindowRoute } from './maintenance_window/finish_maintenance_window';
|
||||
|
||||
export interface RouteOptions {
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
licenseState: ILicenseState;
|
||||
|
@ -93,4 +101,11 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
getFlappingSettingsRoute(router, licenseState);
|
||||
updateFlappingSettingsRoute(router, licenseState);
|
||||
getRuleTagsRoute(router, licenseState);
|
||||
createMaintenanceWindowRoute(router, licenseState);
|
||||
getMaintenanceWindowRoute(router, licenseState);
|
||||
updateMaintenanceWindowRoute(router, licenseState);
|
||||
deleteMaintenanceWindowRoute(router, licenseState);
|
||||
findMaintenanceWindowsRoute(router, licenseState);
|
||||
archiveMaintenanceWindowRoute(router, licenseState);
|
||||
finishMaintenanceWindowRoute(router, licenseState);
|
||||
}
|
||||
|
|
|
@ -22,3 +22,8 @@ export { rewriteActionsReq, rewriteActionsRes } from './rewrite_actions';
|
|||
export { actionsSchema } from './actions_schema';
|
||||
export { rewriteRule, rewriteRuleLastRun } from './rewrite_rule';
|
||||
export { rewriteNamespaces } from './rewrite_namespaces';
|
||||
export { rRuleSchema } from './rrule_schema';
|
||||
export {
|
||||
rewriteMaintenanceWindowRes,
|
||||
rewritePartialMaintenanceBodyRes,
|
||||
} from './rewrite_maintenance_window';
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { RewriteResponseCase } from '.';
|
||||
import { MaintenanceWindow } from '../../../common';
|
||||
|
||||
export const rewriteMaintenanceWindowRes: RewriteResponseCase<MaintenanceWindow> = ({
|
||||
expirationDate,
|
||||
rRule,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
eventStartTime,
|
||||
eventEndTime,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
expiration_date: expirationDate,
|
||||
r_rule: rRule,
|
||||
created_by: createdBy,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
event_start_time: eventStartTime,
|
||||
event_end_time: eventEndTime,
|
||||
});
|
||||
|
||||
export const rewritePartialMaintenanceBodyRes: RewriteResponseCase<Partial<MaintenanceWindow>> = ({
|
||||
expirationDate,
|
||||
rRule,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
eventStartTime,
|
||||
eventEndTime,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
expiration_date: expirationDate,
|
||||
r_rule: rRule,
|
||||
created_by: createdBy,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
event_start_time: eventStartTime,
|
||||
event_end_time: eventEndTime,
|
||||
});
|
48
x-pack/plugins/alerting/server/routes/lib/rrule_schema.ts
Normal file
48
x-pack/plugins/alerting/server/routes/lib/rrule_schema.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { createValidateRruleBy } from '../../lib/validate_rrule_by';
|
||||
import { validateSnoozeStartDate, validateSnoozeEndDate } from '../../lib/validate_snooze_date';
|
||||
|
||||
export const rRuleSchema = schema.object({
|
||||
dtstart: schema.string({ validate: validateSnoozeStartDate }),
|
||||
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: validateSnoozeEndDate })),
|
||||
count: schema.maybe(
|
||||
schema.number({
|
||||
validate: (count: number) => {
|
||||
if (count < 1) return 'rRule count must be > 0';
|
||||
},
|
||||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRruleBy('byweekday'),
|
||||
})
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRruleBy('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRruleBy('bymonth'),
|
||||
})
|
||||
),
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { archiveMaintenanceWindowRoute } from './archive_maintenance_window';
|
||||
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
|
||||
import { MaintenanceWindowStatus } from '../../../common';
|
||||
import { rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMaintenanceWindow = {
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
describe('archiveMaintenanceWindowRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should archive the maintenance window', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
archiveMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.archive.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{
|
||||
params: {
|
||||
id: 'test-id',
|
||||
},
|
||||
body: {
|
||||
archive: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/{id}/_archive');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:write-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.archive).toHaveBeenLastCalledWith({
|
||||
id: 'test-id',
|
||||
archive: true,
|
||||
});
|
||||
expect(res.ok).toHaveBeenLastCalledWith({
|
||||
body: rewritePartialMaintenanceBodyRes(mockMaintenanceWindow),
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures the license allows for archiving maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
archiveMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.archive.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{
|
||||
params: {
|
||||
id: 'test-id',
|
||||
},
|
||||
body: {
|
||||
archive: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
await handler(context, req, res);
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
test('ensures the license check prevents for archiving maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
archiveMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Failure');
|
||||
});
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{
|
||||
params: {
|
||||
id: 'test-id',
|
||||
},
|
||||
body: {
|
||||
archive: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../../lib';
|
||||
import { verifyAccessAndContext, rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
|
||||
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
const bodySchema = schema.object({
|
||||
archive: schema.boolean(),
|
||||
});
|
||||
|
||||
export const archiveMaintenanceWindowRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_archive`,
|
||||
validate: {
|
||||
params: paramSchema,
|
||||
body: bodySchema,
|
||||
},
|
||||
options: {
|
||||
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
|
||||
const { id } = req.params;
|
||||
const { archive } = req.body;
|
||||
const maintenanceWindow = await maintenanceWindowClient.archive({
|
||||
id,
|
||||
archive,
|
||||
});
|
||||
return res.ok({
|
||||
body: rewritePartialMaintenanceBodyRes(maintenanceWindow),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { createMaintenanceWindowRoute, rewriteQueryReq } from './create_maintenance_window';
|
||||
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
|
||||
import { MaintenanceWindowStatus } from '../../../common';
|
||||
import { rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMaintenanceWindow = {
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
const createParams = {
|
||||
title: 'test-title',
|
||||
duration: 1000,
|
||||
r_rule: mockMaintenanceWindow.rRule,
|
||||
};
|
||||
|
||||
describe('createMaintenanceWindowRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should create the maintenance window', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
createMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.create.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ body: createParams }
|
||||
);
|
||||
|
||||
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:write-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.create).toHaveBeenLastCalledWith(rewriteQueryReq(createParams));
|
||||
expect(res.ok).toHaveBeenLastCalledWith({
|
||||
body: rewritePartialMaintenanceBodyRes(mockMaintenanceWindow),
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures the license allows for creating maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
createMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.create.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ body: createParams }
|
||||
);
|
||||
await handler(context, req, res);
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
test('ensures the license check prevents for creating maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
createMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Failure');
|
||||
});
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ body: createParams }
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../../lib';
|
||||
import {
|
||||
verifyAccessAndContext,
|
||||
rRuleSchema,
|
||||
RewriteRequestCase,
|
||||
rewriteMaintenanceWindowRes,
|
||||
} from '../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
|
||||
import { MaintenanceWindowSOProperties, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
|
||||
|
||||
const bodySchema = schema.object({
|
||||
title: schema.string(),
|
||||
duration: schema.number(),
|
||||
r_rule: rRuleSchema,
|
||||
});
|
||||
|
||||
type MaintenanceWindowCreateBody = Omit<
|
||||
MaintenanceWindowSOProperties,
|
||||
'events' | 'expirationDate' | 'enabled' | 'archived'
|
||||
>;
|
||||
|
||||
export const rewriteQueryReq: RewriteRequestCase<MaintenanceWindowCreateBody> = ({
|
||||
r_rule: rRule,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
});
|
||||
|
||||
export const createMaintenanceWindowRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window`,
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
options: {
|
||||
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
|
||||
const maintenanceWindow = await maintenanceWindowClient.create(rewriteQueryReq(req.body));
|
||||
return res.ok({
|
||||
body: rewriteMaintenanceWindowRes(maintenanceWindow),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { deleteMaintenanceWindowRoute } from './delete_maintenance_window';
|
||||
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
|
||||
import { MaintenanceWindowStatus } from '../../../common';
|
||||
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMaintenanceWindow = {
|
||||
...getMockMaintenanceWindow(),
|
||||
startDate: new Date().toISOString(),
|
||||
endDate: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
describe('deleteMaintenanceWindowRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should delete the maintenance window', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
deleteMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.delete.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [config, handler] = router.delete.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
|
||||
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/{id}');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:write-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.delete).toHaveBeenLastCalledWith({ id: 'test-id' });
|
||||
expect(res.noContent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('ensures the license allows for deleting maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
deleteMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.delete.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [, handler] = router.delete.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
await handler(context, req, res);
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
test('ensures the license check prevents for deleting maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
deleteMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Failure');
|
||||
});
|
||||
const [, handler] = router.delete.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../../lib';
|
||||
import { verifyAccessAndContext } from '../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
|
||||
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
export const deleteMaintenanceWindowRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.delete(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`,
|
||||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
|
||||
const { id } = req.params;
|
||||
await maintenanceWindowClient.delete({ id });
|
||||
return res.noContent();
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { findMaintenanceWindowsRoute } from './find_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 = {
|
||||
data: [
|
||||
{
|
||||
...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('findMaintenanceWindowsRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should find the maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findMaintenanceWindowsRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.find.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/_find');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:read-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.find).toHaveBeenCalled();
|
||||
expect(res.ok).toHaveBeenLastCalledWith({
|
||||
body: {
|
||||
data: mockMaintenanceWindows.data.map((data) => rewriteMaintenanceWindowRes(data)),
|
||||
total: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures the license allows for finding maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findMaintenanceWindowsRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.find.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 finding maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findMaintenanceWindowsRoute(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]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 findMaintenanceWindowsRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_find`,
|
||||
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.find();
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
data: result.data.map((maintenanceWindow) =>
|
||||
rewriteMaintenanceWindowRes(maintenanceWindow)
|
||||
),
|
||||
total: result.data.length,
|
||||
},
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { finishMaintenanceWindowRoute } from './finish_maintenance_window';
|
||||
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
|
||||
import { MaintenanceWindowStatus } from '../../../common';
|
||||
import { rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMaintenanceWindow = {
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
describe('finishMaintenanceWindowRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should finish the maintenance window', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
finishMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.finish.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
|
||||
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/{id}/_finish');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:write-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.finish).toHaveBeenLastCalledWith({ id: 'test-id' });
|
||||
expect(res.ok).toHaveBeenLastCalledWith({
|
||||
body: rewritePartialMaintenanceBodyRes(mockMaintenanceWindow),
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures the license allows for finishing maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
finishMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.finish.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
await handler(context, req, res);
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
test('ensures the license check prevents for finishing maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
finishMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Failure');
|
||||
});
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../../lib';
|
||||
import { verifyAccessAndContext, rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
|
||||
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
export const finishMaintenanceWindowRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_finish`,
|
||||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
|
||||
const maintenanceWindow = await maintenanceWindowClient.finish(req.params);
|
||||
return res.ok({
|
||||
body: rewritePartialMaintenanceBodyRes(maintenanceWindow),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { getMaintenanceWindowRoute } from './get_maintenance_window';
|
||||
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
|
||||
import { MaintenanceWindowStatus } from '../../../common';
|
||||
import { rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMaintenanceWindow = {
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
describe('getMaintenanceWindowRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should get the maintenance window', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.get.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
|
||||
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/{id}');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:read-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.get).toHaveBeenLastCalledWith({ id: 'test-id' });
|
||||
expect(res.ok).toHaveBeenLastCalledWith({
|
||||
body: rewritePartialMaintenanceBodyRes(mockMaintenanceWindow),
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures the license allows for getting maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.get.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
await handler(context, req, res);
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
test('ensures the license check prevents for getting maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Failure');
|
||||
});
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{ params: { id: 'test-id' } }
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
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';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
export const getMaintenanceWindowRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`,
|
||||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
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 { id } = req.params;
|
||||
const maintenanceWindow = await maintenanceWindowClient.get({ id });
|
||||
return res.ok({
|
||||
body: rewriteMaintenanceWindowRes(maintenanceWindow),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { RRule } from 'rrule';
|
||||
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 { updateMaintenanceWindowRoute, rewriteQueryReq } from './update_maintenance_window';
|
||||
import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers';
|
||||
import { MaintenanceWindowStatus } from '../../../common';
|
||||
import { rewritePartialMaintenanceBodyRes } from '../lib';
|
||||
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMaintenanceWindow = {
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
const updateParams = {
|
||||
title: 'new-title',
|
||||
duration: 5000,
|
||||
enabled: false,
|
||||
r_rule: {
|
||||
tzid: 'CET',
|
||||
dtstart: '2023-03-26T00:00:00.000Z',
|
||||
freq: RRule.WEEKLY,
|
||||
count: 10,
|
||||
},
|
||||
};
|
||||
|
||||
describe('updateMaintenanceWindowRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should update the maintenance window', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
updateMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.update.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{
|
||||
params: { id: 'test-id' },
|
||||
body: updateParams,
|
||||
}
|
||||
);
|
||||
|
||||
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/{id}');
|
||||
expect(config.options?.tags?.[0]).toEqual('access:write-maintenance-window');
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(maintenanceWindowClient.update).toHaveBeenLastCalledWith({
|
||||
id: 'test-id',
|
||||
...rewriteQueryReq(updateParams),
|
||||
});
|
||||
|
||||
expect(res.ok).toHaveBeenLastCalledWith({
|
||||
body: rewritePartialMaintenanceBodyRes(mockMaintenanceWindow),
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures the license allows for updating maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
updateMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
maintenanceWindowClient.update.mockResolvedValueOnce(mockMaintenanceWindow);
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{
|
||||
params: { id: 'test-id' },
|
||||
body: updateParams,
|
||||
}
|
||||
);
|
||||
await handler(context, req, res);
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
test('ensures the license check prevents for updating maintenance windows', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
updateMaintenanceWindowRoute(router, licenseState);
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Failure');
|
||||
});
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ maintenanceWindowClient },
|
||||
{
|
||||
params: { id: 'test-id' },
|
||||
body: updateParams,
|
||||
}
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../../lib';
|
||||
import {
|
||||
verifyAccessAndContext,
|
||||
rRuleSchema,
|
||||
RewriteRequestCase,
|
||||
rewritePartialMaintenanceBodyRes,
|
||||
} from '../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
|
||||
import { MaintenanceWindowSOProperties, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
const bodySchema = schema.object({
|
||||
title: schema.maybe(schema.string()),
|
||||
enabled: schema.maybe(schema.boolean()),
|
||||
duration: schema.maybe(schema.number()),
|
||||
r_rule: schema.maybe(rRuleSchema),
|
||||
});
|
||||
|
||||
interface MaintenanceWindowUpdateBody {
|
||||
title?: MaintenanceWindowSOProperties['title'];
|
||||
enabled?: MaintenanceWindowSOProperties['enabled'];
|
||||
duration?: MaintenanceWindowSOProperties['duration'];
|
||||
rRule?: MaintenanceWindowSOProperties['rRule'];
|
||||
}
|
||||
|
||||
export const rewriteQueryReq: RewriteRequestCase<MaintenanceWindowUpdateBody> = ({
|
||||
r_rule: rRule,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
...(rRule ? { rRule } : {}),
|
||||
});
|
||||
|
||||
export const updateMaintenanceWindowRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`,
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW}`],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
|
||||
const maintenanceWindow = await maintenanceWindowClient.update({
|
||||
id: req.params.id,
|
||||
...rewriteQueryReq(req.body),
|
||||
});
|
||||
return res.ok({
|
||||
body: rewritePartialMaintenanceBodyRes(maintenanceWindow),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -8,11 +8,9 @@
|
|||
import { IRouter } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState, RuleMutedError } from '../lib';
|
||||
import { verifyAccessAndContext } from './lib';
|
||||
import { verifyAccessAndContext, rRuleSchema } from './lib';
|
||||
import { SnoozeOptions } from '../rules_client';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
|
||||
import { validateSnoozeStartDate, validateSnoozeEndDate } from '../lib/validate_snooze_date';
|
||||
import { createValidateRruleBy } from '../lib/validate_rrule_by';
|
||||
import { validateSnoozeSchedule } from '../lib/validate_snooze_schedule';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
|
@ -23,43 +21,7 @@ export const snoozeScheduleSchema = schema.object(
|
|||
{
|
||||
id: schema.maybe(schema.string()),
|
||||
duration: schema.number(),
|
||||
rRule: schema.object({
|
||||
dtstart: schema.string({ validate: validateSnoozeStartDate }),
|
||||
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: validateSnoozeEndDate })),
|
||||
count: schema.maybe(
|
||||
schema.number({
|
||||
validate: (count: number) => {
|
||||
if (count < 1) return 'rRule count must be > 0';
|
||||
},
|
||||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRruleBy('byweekday'),
|
||||
})
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRruleBy('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRruleBy('bymonth'),
|
||||
})
|
||||
),
|
||||
}),
|
||||
rRule: rRuleSchema,
|
||||
},
|
||||
{ validate: validateSnoozeSchedule }
|
||||
);
|
||||
|
|
|
@ -93,7 +93,7 @@ export class RulesSettingsClient {
|
|||
|
||||
/**
|
||||
* Helper function to ensure that a rules-settings saved object always exists.
|
||||
* Enabled the creation of the saved object is done lazily during retrieval.
|
||||
* Ensures the creation of the saved object is done lazily during retrieval.
|
||||
*/
|
||||
private async getOrCreate(): Promise<SavedObject<RulesSettings>> {
|
||||
try {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-p
|
|||
import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
|
||||
import { alertMappings } from './mappings';
|
||||
import { rulesSettingsMappings } from './rules_settings_mappings';
|
||||
import { maintenanceWindowMappings } from './maintenance_window_mapping';
|
||||
import { getMigrations } from './migrations';
|
||||
import { transformRulesForExport } from './transform_rule_for_export';
|
||||
import { RawRule } from '../types';
|
||||
|
@ -22,7 +23,10 @@ import { getImportWarnings } from './get_import_warnings';
|
|||
import { isRuleExportable } from './is_rule_exportable';
|
||||
import { RuleTypeRegistry } from '../rule_type_registry';
|
||||
export { partiallyUpdateAlert } from './partially_update_alert';
|
||||
import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../../common';
|
||||
import {
|
||||
RULES_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
} from '../../common';
|
||||
|
||||
// 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
|
||||
|
@ -125,6 +129,13 @@ export function setupSavedObjects(
|
|||
mappings: rulesSettingsMappings,
|
||||
});
|
||||
|
||||
savedObjects.registerType({
|
||||
name: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: maintenanceWindowMappings,
|
||||
});
|
||||
|
||||
// Encrypted attributes
|
||||
encryptedSavedObjects.registerType({
|
||||
type: 'alert',
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { SavedObjectsTypeMappingDefinition } from '@kbn/core/server';
|
||||
|
||||
export const maintenanceWindowMappings: SavedObjectsTypeMappingDefinition = {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
},
|
||||
events: {
|
||||
type: 'date_range',
|
||||
format: 'epoch_millis||strict_date_optional_time',
|
||||
},
|
||||
// NO NEED TO BE INDEXED
|
||||
// title: {
|
||||
// type: 'text',
|
||||
// fields: {
|
||||
// keyword: {
|
||||
// type: 'keyword',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// duration: {
|
||||
// type: 'long',
|
||||
// },
|
||||
// expirationDate: {
|
||||
// type: 'date',
|
||||
// },
|
||||
// rRule: rRuleMappingsField,
|
||||
// createdBy: {
|
||||
// index: false,
|
||||
// type: 'keyword',
|
||||
// },
|
||||
// updatedBy: {
|
||||
// index: false,
|
||||
// type: 'keyword',
|
||||
// },
|
||||
// createdAt: {
|
||||
// index: false,
|
||||
// type: 'date',
|
||||
// },
|
||||
// updatedAt: {
|
||||
// index: false,
|
||||
// type: 'date',
|
||||
// },
|
||||
},
|
||||
};
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsTypeMappingDefinition } from '@kbn/core/server';
|
||||
import { rRuleMappingsField } from './rrule_mappings_field';
|
||||
|
||||
export const alertMappings: SavedObjectsTypeMappingDefinition = {
|
||||
dynamic: false,
|
||||
|
@ -216,61 +217,7 @@ export const alertMappings: SavedObjectsTypeMappingDefinition = {
|
|||
type: 'date',
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
rRule: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
freq: {
|
||||
type: 'keyword',
|
||||
},
|
||||
dtstart: {
|
||||
type: 'date',
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
tzid: {
|
||||
type: 'keyword',
|
||||
},
|
||||
until: {
|
||||
type: 'date',
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
count: {
|
||||
type: 'long',
|
||||
},
|
||||
interval: {
|
||||
type: 'long',
|
||||
},
|
||||
wkst: {
|
||||
type: 'keyword',
|
||||
},
|
||||
byweekday: {
|
||||
type: 'keyword',
|
||||
},
|
||||
bymonth: {
|
||||
type: 'short',
|
||||
},
|
||||
bysetpos: {
|
||||
type: 'long',
|
||||
},
|
||||
bymonthday: {
|
||||
type: 'short',
|
||||
},
|
||||
byyearday: {
|
||||
type: 'short',
|
||||
},
|
||||
byweekno: {
|
||||
type: 'short',
|
||||
},
|
||||
byhour: {
|
||||
type: 'long',
|
||||
},
|
||||
byminute: {
|
||||
type: 'long',
|
||||
},
|
||||
bysecond: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
rRule: rRuleMappingsField,
|
||||
},
|
||||
},
|
||||
nextRun: {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { SavedObjectsFieldMapping } from '@kbn/core/server';
|
||||
|
||||
export const rRuleMappingsField: SavedObjectsFieldMapping = {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
freq: {
|
||||
type: 'keyword',
|
||||
},
|
||||
dtstart: {
|
||||
type: 'date',
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
tzid: {
|
||||
type: 'keyword',
|
||||
},
|
||||
until: {
|
||||
type: 'date',
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
count: {
|
||||
type: 'long',
|
||||
},
|
||||
interval: {
|
||||
type: 'long',
|
||||
},
|
||||
wkst: {
|
||||
type: 'keyword',
|
||||
},
|
||||
byweekday: {
|
||||
type: 'keyword',
|
||||
},
|
||||
bymonth: {
|
||||
type: 'short',
|
||||
},
|
||||
bysetpos: {
|
||||
type: 'long',
|
||||
},
|
||||
bymonthday: {
|
||||
type: 'short',
|
||||
},
|
||||
byyearday: {
|
||||
type: 'short',
|
||||
},
|
||||
byweekno: {
|
||||
type: 'short',
|
||||
},
|
||||
byhour: {
|
||||
type: 'long',
|
||||
},
|
||||
byminute: {
|
||||
type: 'long',
|
||||
},
|
||||
bysecond: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -27,6 +27,7 @@ import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry';
|
|||
import { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
import { RulesClient } from './rules_client';
|
||||
import { RulesSettingsClient, RulesSettingsFlappingClient } from './rules_settings_client';
|
||||
import { MaintenanceWindowClient } from './maintenance_window_client';
|
||||
export * from '../common';
|
||||
import {
|
||||
Rule,
|
||||
|
@ -64,6 +65,7 @@ export type { RuleTypeParams };
|
|||
export interface AlertingApiRequestHandlerContext {
|
||||
getRulesClient: () => RulesClient;
|
||||
getRulesSettingsClient: () => RulesSettingsClient;
|
||||
getMaintenanceWindowClient: () => MaintenanceWindowClient;
|
||||
listTypes: RuleTypeRegistry['list'];
|
||||
getFrameworkHealth: () => Promise<AlertsHealth>;
|
||||
areApiKeysEnabled: () => Promise<boolean>;
|
||||
|
@ -403,6 +405,8 @@ export type RulesClientApi = PublicMethodsOf<RulesClient>;
|
|||
export type RulesSettingsClientApi = PublicMethodsOf<RulesSettingsClient>;
|
||||
export type RulesSettingsFlappingClientApi = PublicMethodsOf<RulesSettingsFlappingClient>;
|
||||
|
||||
export type MaintenanceWindowClientApi = PublicMethodsOf<MaintenanceWindowClient>;
|
||||
|
||||
export interface PublicMetricsSetters {
|
||||
setLastRunMetricsTotalSearchDurationMs: (totalSearchDurationMs: number) => void;
|
||||
setLastRunMetricsTotalIndexingDurationMs: (totalIndexingDurationMs: number) => void;
|
||||
|
|
|
@ -12,6 +12,7 @@ interface ObjectToRemove {
|
|||
id: string;
|
||||
type: string;
|
||||
plugin: string;
|
||||
isInternal?: boolean;
|
||||
}
|
||||
|
||||
export class ObjectRemover {
|
||||
|
@ -26,16 +27,19 @@ export class ObjectRemover {
|
|||
spaceId: ObjectToRemove['spaceId'],
|
||||
id: ObjectToRemove['id'],
|
||||
type: ObjectToRemove['type'],
|
||||
plugin: ObjectToRemove['plugin']
|
||||
plugin: ObjectToRemove['plugin'],
|
||||
isInternal?: ObjectToRemove['isInternal']
|
||||
) {
|
||||
this.objectsToRemove.push({ spaceId, id, type, plugin });
|
||||
this.objectsToRemove.push({ spaceId, id, type, plugin, isInternal });
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
await Promise.all(
|
||||
this.objectsToRemove.map(({ spaceId, id, type, plugin }) => {
|
||||
this.objectsToRemove.map(({ spaceId, id, type, plugin, isInternal }) => {
|
||||
return this.supertest
|
||||
.delete(`${getUrlPrefix(spaceId)}/api/${plugin}/${type}/${id}`)
|
||||
.delete(
|
||||
`${getUrlPrefix(spaceId)}/${isInternal ? 'internal' : 'api'}/${plugin}/${type}/${id}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
})
|
||||
|
|
|
@ -11,5 +11,6 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
|||
export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) {
|
||||
describe('alerting api integration security and spaces enabled - Group 3', function () {
|
||||
loadTestFile(require.resolve('./alerting'));
|
||||
loadTestFile(require.resolve('./maintenance_window'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 updateMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('archiveMaintenanceWindow', () => {
|
||||
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, () => {
|
||||
it('should handle archive maintenance window request appropriately', async () => {
|
||||
const { body: createdMaintenanceWindow } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(createParams);
|
||||
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
createdMaintenanceWindow.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(
|
||||
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}/_archive`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({ archive: true });
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read 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 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(
|
||||
moment
|
||||
.utc(createdMaintenanceWindow.expirationDate)
|
||||
.isAfter(response.body.expirationDate)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 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 createMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('createMaintenanceWindow', () => {
|
||||
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, () => {
|
||||
it('should handle create maintenance window request appropriately', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send(createParams);
|
||||
|
||||
if (response.body.id) {
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
response.body.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read 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 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body.title).to.eql('test-maintenance-window');
|
||||
expect(response.body.duration).to.eql(3600000);
|
||||
expect(response.body.r_rule.dtstart).to.eql(createParams.r_rule.dtstart);
|
||||
expect(response.body.events.length).to.be.greaterThan(0);
|
||||
expect(response.body.status).to.eql('running');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 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 deleteMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('deleteMaintenanceWindow', () => {
|
||||
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, () => {
|
||||
it('should handle delete maintenance window request appropriately', async () => {
|
||||
const { body: maintenanceWindowBody } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(createParams);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.delete(
|
||||
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/${
|
||||
maintenanceWindowBody.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read 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,
|
||||
});
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
maintenanceWindowBody.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(204);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(
|
||||
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/${
|
||||
maintenanceWindowBody.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
|
||||
expect(getResponse.body.statusCode).to.eql(404);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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 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 findMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('findMaintenanceWindow', () => {
|
||||
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 () => {
|
||||
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);
|
||||
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
createdMaintenanceWindow1.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
createdMaintenanceWindow2.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/_find`)
|
||||
.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.total).to.eql(2);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body.data[0].title).to.eql('test-maintenance-window');
|
||||
expect(response.body.data[0].duration).to.eql(3600000);
|
||||
expect(response.body.data[0].r_rule.dtstart).to.eql(createParams.r_rule.dtstart);
|
||||
expect(response.body.data[0].events.length).to.be.greaterThan(0);
|
||||
expect(response.body.data[0].status).to.eql('running');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 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 findMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('finishMaintenanceWindow', () => {
|
||||
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
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle finish maintenance window request appropriately', async () => {
|
||||
const { body: createdMaintenanceWindow } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(createParams);
|
||||
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
createdMaintenanceWindow.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
|
||||
expect(createdMaintenanceWindow.status).to.eql('running');
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(
|
||||
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}/_finish`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send();
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read 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 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.body.status).to.eql('upcoming');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 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 getMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('getMaintenanceWindow', () => {
|
||||
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, () => {
|
||||
it('should get maintenance window correctly', async () => {
|
||||
const { body: createdMaintenanceWindow } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(createParams);
|
||||
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
createdMaintenanceWindow.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.get(
|
||||
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}`
|
||||
)
|
||||
.auth(user.username, user.password);
|
||||
|
||||
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.statusCode).to.eql(200);
|
||||
expect(response.body.title).to.eql('test-maintenance-window');
|
||||
expect(response.body.duration).to.eql(3600000);
|
||||
expect(response.body.r_rule.dtstart).to.eql(createParams.r_rule.dtstart);
|
||||
expect(response.body.events.length).to.be.greaterThan(0);
|
||||
expect(response.body.status).to.eql('running');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { setupSpacesAndUsers, tearDown } from '../../../setup';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function maintenanceWindowTests({ loadTestFile, getService }: FtrProviderContext) {
|
||||
describe('Maintenance Window - Group 3', () => {
|
||||
describe('maintenance window', () => {
|
||||
before(async () => {
|
||||
await setupSpacesAndUsers(getService);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await tearDown(getService);
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./get_maintenance_window'));
|
||||
loadTestFile(require.resolve('./create_maintenance_window'));
|
||||
loadTestFile(require.resolve('./update_maintenance_window'));
|
||||
loadTestFile(require.resolve('./delete_maintenance_window'));
|
||||
loadTestFile(require.resolve('./archive_maintenance_window'));
|
||||
loadTestFile(require.resolve('./finish_maintenance_window'));
|
||||
loadTestFile(require.resolve('./find_maintenance_windows'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 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 updateMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('updateMaintenanceWindow', () => {
|
||||
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, () => {
|
||||
it('should handle update maintenance window request appropriately', async () => {
|
||||
const { body: createdMaintenanceWindow } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(createParams);
|
||||
|
||||
objectRemover.add(
|
||||
space.id,
|
||||
createdMaintenanceWindow.id,
|
||||
'rules/maintenance_window',
|
||||
'alerting',
|
||||
true
|
||||
);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(
|
||||
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
...createParams,
|
||||
enabled: true,
|
||||
title: 'updated-title',
|
||||
duration: 2 * 60 * 60 * 1000, // 2 hrs
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read 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 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body.title).to.eql('updated-title');
|
||||
expect(response.body.duration).to.eql(7200000);
|
||||
expect(response.body.r_rule.dtstart).to.eql(createParams.r_rule.dtstart);
|
||||
expect(response.body.events.length).to.be.greaterThan(0);
|
||||
expect(response.body.status).to.eql('running');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -56,6 +56,7 @@ const GlobalRead: User = {
|
|||
alertsRestrictedFixture: ['read'],
|
||||
actionsSimulators: ['read'],
|
||||
rulesSettings: ['read', READ_FLAPPING_SETTINGS_SUB_FEATURE_ID],
|
||||
maintenanceWindow: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
|
@ -84,6 +85,7 @@ const Space1All: User = {
|
|||
alertsFixture: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
rulesSettings: ['all', ALL_FLAPPING_SETTINGS_SUB_FEATURE_ID],
|
||||
maintenanceWindow: ['all'],
|
||||
},
|
||||
spaces: ['space1'],
|
||||
},
|
||||
|
|
|
@ -116,6 +116,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'generalCases',
|
||||
'infrastructure',
|
||||
'logs',
|
||||
'maintenanceWindow',
|
||||
'maps',
|
||||
'osquery',
|
||||
'rulesSettings',
|
||||
|
|
|
@ -102,6 +102,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'allFlappingSettings',
|
||||
'readFlappingSettings',
|
||||
],
|
||||
maintenanceWindow: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
},
|
||||
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
filesManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
filesSharedImage: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
rulesSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
maintenanceWindow: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
},
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
|
@ -173,6 +174,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'allFlappingSettings',
|
||||
'readFlappingSettings',
|
||||
],
|
||||
maintenanceWindow: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
},
|
||||
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue