Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce basic alerting and actions plugin #37042

Merged
merged 21 commits into from
Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0241553
Create actions plugin (#35679)
mikecote May 23, 2019
1671d5d
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote May 28, 2019
ac7b3e2
Pass services to action executors (#37194)
mikecote May 28, 2019
9ea8594
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote May 30, 2019
ee0917e
Cleanup actions plugin (#37250)
mikecote May 30, 2019
4be76d3
add server log action for alerting (#37530)
pmuellr May 31, 2019
3c46d13
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote Jun 4, 2019
1c501dd
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote Jun 6, 2019
b1a6583
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote Jun 10, 2019
8a09767
Create alerting plugin (#37043)
mikecote Jun 18, 2019
3074ef2
Merge with master
mikecote Jun 18, 2019
91a4219
Fix failing jest tests
mikecote Jun 18, 2019
f8b39a5
Accept core api changes
mikecote Jun 18, 2019
5839a9d
Fix saved objects client mock
mikecote Jun 18, 2019
a762bff
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote Jun 18, 2019
76ec667
Merge branch 'master' of github.com:elastic/kibana into feature/alerting
mikecote Jun 19, 2019
68445b1
PR feedback pt1
mikecote Jun 19, 2019
49ac129
Fix eslint issues
mikecote Jun 19, 2019
8478d21
Throw error when alert instance already fired (#39251)
mikecote Jun 20, 2019
d771163
Merge with master
mikecote Jun 20, 2019
c3ceb4d
Actions & alerting getting started user guides (#39093)
mikecote Jun 20, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"tagCloud": "src/legacy/core_plugins/tagcloud",
"tsvb": "src/legacy/core_plugins/metrics",
"kbnESQuery": "packages/kbn-es-query",
"xpack.actions": "x-pack/plugins/actions",
"xpack.alerting": "x-pack/plugins/alerting",
"xpack.apm": "x-pack/plugins/apm",
"xpack.beatsManagement": "x-pack/plugins/beats_management",
"xpack.canvas": "x-pack/plugins/canvas",
Expand Down
1 change: 1 addition & 0 deletions src/core/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export { configServiceMock } from './config/config_service.mock';
export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
export { httpServiceMock } from './http/http_service.mock';
export { loggingServiceMock } from './logging/logging_service.mock';
export { SavedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock';
15 changes: 14 additions & 1 deletion src/core/server/saved_objects/service/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,25 @@ export interface SavedObjectsMigrationVersion {
[pluginName: string]: string;
}

/**
*
* @public
*/
export type SavedObjectAttribute =
| string
| number
| boolean
| null
| undefined
| SavedObjectAttributes
| SavedObjectAttributes[];

/**
*
* @public
*/
export interface SavedObjectAttributes {
[key: string]: SavedObjectAttributes | string | number | boolean | null;
[key: string]: SavedObjectAttribute | SavedObjectAttribute[];
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,10 @@ export interface SavedObject<T extends SavedObjectAttributes = any> {

// @public (undocumented)
export interface SavedObjectAttributes {
// Warning: (ae-forgotten-export) The symbol "SavedObjectAttribute" needs to be exported by the entry point index.d.ts
//
// (undocumented)
[key: string]: SavedObjectAttributes | string | number | boolean | null;
[key: string]: SavedObjectAttribute | SavedObjectAttribute[];
}

// @public
Expand Down
4 changes: 4 additions & 0 deletions x-pack/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { fileUpload } from './plugins/file_upload';
import { telemetry } from './plugins/telemetry';
import { encryptedSavedObjects } from './plugins/encrypted_saved_objects';
import { snapshotRestore } from './plugins/snapshot_restore';
import { actions } from './plugins/actions';
import { alerting } from './plugins/alerting';

module.exports = function (kibana) {
return [
Expand Down Expand Up @@ -85,5 +87,7 @@ module.exports = function (kibana) {
fileUpload(kibana),
encryptedSavedObjects(kibana),
snapshotRestore(kibana),
actions(kibana),
alerting(kibana),
];
};
38 changes: 38 additions & 0 deletions x-pack/plugins/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Legacy } from 'kibana';
import { Root } from 'joi';
import mappings from './mappings.json';
import { init } from './server';

export { ActionsPlugin, ActionsClient, ActionType, ActionTypeExecutorOptions } from './server';

export function actions(kibana: any) {
return new kibana.Plugin({
id: 'actions',
configPrefix: 'xpack.actions',
require: ['kibana', 'elasticsearch', 'task_manager', 'encrypted_saved_objects'],
isEnabled(config: Legacy.KibanaConfig) {
return (
config.get('xpack.encrypted_saved_objects.enabled') === true &&
config.get('xpack.actions.enabled') === true &&
config.get('xpack.task_manager.enabled') === true
);
},
config(Joi: Root) {
return Joi.object()
.keys({
enabled: Joi.boolean().default(true),
})
.default();
},
init,
uiExports: {
mappings,
},
});
}
19 changes: 19 additions & 0 deletions x-pack/plugins/actions/mappings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"action": {
"properties": {
"description": {
"type": "text"
},
"actionTypeId": {
"type": "keyword"
},
"actionTypeConfig": {
"enabled": false,
"type": "object"
},
"actionTypeConfigSecrets": {
"type": "binary"
}
}
}
}
23 changes: 23 additions & 0 deletions x-pack/plugins/actions/server/action_type_registry.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ActionTypeRegistry } from './action_type_registry';

type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;

const createActionTypeRegistryMock = () => {
const mocked: jest.Mocked<ActionTypeRegistryContract> = {
has: jest.fn(),
register: jest.fn(),
get: jest.fn(),
list: jest.fn(),
};
return mocked;
};

export const actionTypeRegistryMock = {
create: createActionTypeRegistryMock,
};
151 changes: 151 additions & 0 deletions x-pack/plugins/actions/server/action_type_registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

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

import { taskManagerMock } from '../../task_manager/task_manager.mock';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/plugin.mock';
import { ActionTypeRegistry } from './action_type_registry';
import { SavedObjectsClientMock } from '../../../../src/core/server/mocks';

const mockTaskManager = taskManagerMock.create();

function getServices() {
return {
log: jest.fn(),
callCluster: jest.fn(),
savedObjectsClient: SavedObjectsClientMock.create(),
};
}
const actionTypeRegistryParams = {
getServices,
taskManager: mockTaskManager,
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(),
};

beforeEach(() => jest.resetAllMocks());

describe('register()', () => {
test('able to register action types', () => {
const executor = jest.fn();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getCreateTaskRunnerFunction } = require('./lib/get_create_task_runner_function');
getCreateTaskRunnerFunction.mockReturnValueOnce(jest.fn());
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
expect(actionTypeRegistry.has('my-action-type')).toEqual(true);
expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
expect(mockTaskManager.registerTaskDefinitions.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"actions:my-action-type": Object {
"createTaskRunner": [MockFunction],
"title": "My action type",
"type": "actions:my-action-type",
},
},
]
`);
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
const call = getCreateTaskRunnerFunction.mock.calls[0][0];
expect(call.actionType).toMatchInlineSnapshot(`
Object {
"executor": [MockFunction],
"id": "my-action-type",
"name": "My action type",
}
`);
expect(call.encryptedSavedObjectsPlugin).toBeTruthy();
expect(call.getServices).toBeTruthy();
});

test('throws error if action type already registered', () => {
const executor = jest.fn();
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
expect(() =>
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
})
).toThrowErrorMatchingInlineSnapshot(
`"Action type \\"my-action-type\\" is already registered."`
);
});
});

describe('get()', () => {
test('returns action type', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
async executor() {},
});
const actionType = actionTypeRegistry.get('my-action-type');
expect(actionType).toMatchInlineSnapshot(`
Object {
"executor": [Function],
"id": "my-action-type",
"name": "My action type",
}
`);
});

test(`throws an error when action type doesn't exist`, () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
expect(() => actionTypeRegistry.get('my-action-type')).toThrowErrorMatchingInlineSnapshot(
`"Action type \\"my-action-type\\" is not registered."`
);
});
});

describe('list()', () => {
test('returns list of action types', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
async executor() {},
});
const actionTypes = actionTypeRegistry.list();
expect(actionTypes).toEqual([
{
id: 'my-action-type',
name: 'My action type',
},
]);
});
});

describe('has()', () => {
test('returns false for unregistered action types', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
expect(actionTypeRegistry.has('my-action-type')).toEqual(false);
});

test('returns true after registering an action type', () => {
const executor = jest.fn();
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
expect(actionTypeRegistry.has('my-action-type'));
});
});
93 changes: 93 additions & 0 deletions x-pack/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { ActionType, Services } from './types';
import { TaskManager } from '../../task_manager';
import { getCreateTaskRunnerFunction } from './lib';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';

interface ConstructorOptions {
getServices: (basePath: string) => Services;
taskManager: TaskManager;
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
}

export class ActionTypeRegistry {
private readonly getServices: (basePath: string) => Services;
private readonly taskManager: TaskManager;
private readonly actionTypes: Map<string, ActionType> = new Map();
private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;

constructor({ getServices, taskManager, encryptedSavedObjectsPlugin }: ConstructorOptions) {
this.getServices = getServices;
this.taskManager = taskManager;
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
}

/**
* Returns if the action type registry has the given action type registered
*/
public has(id: string) {
return this.actionTypes.has(id);
}

/**
* Registers an action type to the action type registry
*/
public register(actionType: ActionType) {
if (this.has(actionType.id)) {
throw new Error(
i18n.translate('xpack.actions.actionTypeRegistry.register.duplicateActionTypeError', {
defaultMessage: 'Action type "{id}" is already registered.',
values: {
id: actionType.id,
},
})
);
}
this.actionTypes.set(actionType.id, actionType);
this.taskManager.registerTaskDefinitions({
[`actions:${actionType.id}`]: {
title: actionType.name,
type: `actions:${actionType.id}`,
createTaskRunner: getCreateTaskRunnerFunction({
actionType,
getServices: this.getServices,
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
}),
},
});
}

/**
* Returns an action type, throws if not registered
*/
public get(id: string): ActionType {
if (!this.has(id)) {
throw Boom.badRequest(
i18n.translate('xpack.actions.actionTypeRegistry.get.missingActionTypeError', {
defaultMessage: 'Action type "{id}" is not registered.',
values: {
id,
},
})
);
}
return this.actionTypes.get(id)!;
}

/**
* Returns a list of registered action types [{ id, name }]
*/
public list() {
return Array.from(this.actionTypes).map(([actionTypeId, actionType]) => ({
id: actionTypeId,
name: actionType.name,
}));
}
}
Loading