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

Create actions plugin #35679

Merged
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
2b3af4c
Basic alerting plugin with actions
mikecote Apr 26, 2019
bc0140b
Remove relative imports
mikecote Apr 26, 2019
682743e
Code cleanup
mikecote Apr 29, 2019
0311aff
Split service into 3 parts, change connector structure
mikecote Apr 30, 2019
d2d9250
Ability to disable plugin, ability to get actions
mikecote Apr 30, 2019
06669ab
Add slack connector
mikecote Apr 30, 2019
d84f9d6
Add email connector
mikecote Apr 30, 2019
dfd0f36
Ability to validate params and connector options
mikecote Apr 30, 2019
632a2d9
Remove connectorOptionsSecrets for now
mikecote Apr 30, 2019
174efea
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote Apr 30, 2019
c8580ef
Fix plugin config validation
mikecote Apr 30, 2019
d7fad22
Add tests for slack connector
mikecote Apr 30, 2019
8085c6b
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 1, 2019
e4c8897
Default connectors register on plugin init, console renamed to log, s…
mikecote May 2, 2019
661e731
Add remaining API endpoints for action CRUD
mikecote May 3, 2019
d393e46
Add list connectors API
mikecote May 3, 2019
2d5857a
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 3, 2019
488f7dd
Change actions CRUD APIs to be closer with saved objects structure
mikecote May 3, 2019
3e40856
Merge with master
mikecote May 3, 2019
c5f48ef
WIP
mikecote May 3, 2019
75c0145
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 6, 2019
0917339
Fix broken tests
mikecote May 6, 2019
b1faff5
Add encrypted attribute support
mikecote May 6, 2019
9ff7836
Add params and connectorOptions for email
mikecote May 6, 2019
40d59a2
WIP
mikecote May 6, 2019
44966a4
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 7, 2019
ff69d43
Remove action's ability to have custom ids
mikecote May 7, 2019
46e2d53
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 7, 2019
ef1fb55
Remove ts-ignore
mikecote May 7, 2019
25c1bfe
Fix broken test
mikecote May 7, 2019
3bd30f8
Remove default connectors from this branch
mikecote May 7, 2019
bebf717
Fix API integration tests to use fixture connector
mikecote May 7, 2019
3641255
Rename connector terminology to action type
mikecote May 8, 2019
9915fb4
Rename actionTypeOptions to actionTypeConfig
mikecote May 8, 2019
4e2b1df
Code cleanup
mikecote May 8, 2019
4d7d6f2
Fix broken tests
mikecote May 8, 2019
7eda24c
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 8, 2019
fcd9306
Rename alerting plugin to actions
mikecote May 8, 2019
d21d02a
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 8, 2019
0ccd660
Some code cleanup and add API unit tests
mikecote May 9, 2019
4c5661c
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 9, 2019
da985f8
Change signature of action type service execute function
mikecote May 9, 2019
e2bec4f
Add some plugin api integration tests
mikecote May 10, 2019
33243f4
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 10, 2019
9573053
Fix type check failure
mikecote May 10, 2019
1aa660c
Code cleanup
mikecote May 10, 2019
e57191a
Create an actions client instead of an action service
mikecote May 10, 2019
c9af1e0
Merge with master
mikecote May 16, 2019
05ef18a
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 19, 2019
6d9363d
Apply Bill's PR feedback
mikecote May 19, 2019
c26eaf6
Fix broken test
mikecote May 19, 2019
9225e44
Find function to have destructured params
mikecote May 21, 2019
78b1d25
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 21, 2019
7e4dc57
Add tests to ensure encrypted attributes are not returned
mikecote May 21, 2019
e1dd525
Fix broken test
mikecote May 21, 2019
c4ef04a
Add tests for validation
mikecote May 21, 2019
fe23aec
Ensure actions can be updated without re-passing the config
mikecote May 21, 2019
b1938ab
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 21, 2019
9fc8360
Remove dead code
mikecote May 22, 2019
eb06854
Merge branch 'master' of github.com:elastic/kibana into alerting/intr…
mikecote May 22, 2019
8af6507
Test cleanup
mikecote May 22, 2019
0fab9c8
Fix eslint issue
mikecote May 22, 2019
30bcd8a
Apply Peter's PR feedback
mikecote May 22, 2019
910fc7b
Code cleanup and fix broken tests
mikecote May 22, 2019
8254f73
Merge with upstream
mikecote May 23, 2019
e0df91d
Apply Brandon's PR feedback
mikecote May 23, 2019
42098d4
Add namespace support
mikecote May 23, 2019
94107eb
Fix broken test
mikecote May 23, 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 x-pack/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { uptime } from './plugins/uptime';
import { ossTelemetry } from './plugins/oss_telemetry';
import { encryptedSavedObjects } from './plugins/encrypted_saved_objects';
import { snapshotRestore } from './plugins/snapshot_restore';
import { actions } from './plugins/actions';

module.exports = function (kibana) {
return [
Expand Down Expand Up @@ -81,5 +82,6 @@ module.exports = function (kibana) {
ossTelemetry(kibana),
encryptedSavedObjects(kibana),
snapshotRestore(kibana),
actions(kibana),
];
};
30 changes: 30 additions & 0 deletions x-pack/plugins/actions/index.ts
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

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

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

export function actions(kibana: any) {
return new kibana.Plugin({
id: 'actions',
configPrefix: 'xpack.actions',
require: ['kibana', 'elasticsearch', 'encrypted_saved_objects'],
config(Joi: Root) {
return Joi.object()
.keys({
enabled: Joi.boolean().default(true),
})
.default();
},
init,
uiExports: {
mappings,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we anticipate all "actions" being "space specific"? How will this interact with Stack Monitoring's usage of alerting, which will likely be "space agnostic"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this time, we haven't decided how we're going to approach this. I will setup a design discussion for spaces and another one for feature controls. Until then I'm thinking of keeping it space specific until we have those discussions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a single "saved object type" for all actions means we won't be able to restrict users to only access certain "action types", similar to the discussion which I raised here: #36836

If we're going to want to restrict users to a subset of actions in the future, it might be advantageous to create different saved object types for the various alert types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to what I wrote above, I'm thinking we do a future phase post design discussion to properly implement feature controls / actions limiting.

},
});
}
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"
}
}
}
}
331 changes: 331 additions & 0 deletions x-pack/plugins/actions/server/__jest__/action_type_service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
/*
* 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 Joi from 'joi';
import { ActionTypeService } from '../action_type_service';

describe('register()', () => {
test('able to register action types', () => {
const executor = jest.fn();
const actionTypeService = new ActionTypeService();
mikecote marked this conversation as resolved.
Show resolved Hide resolved
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
expect(actionTypeService.has('my-action-type')).toEqual(true);
});

test('throws error if action type already registered', () => {
const executor = jest.fn();
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
expect(() =>
actionTypeService.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 actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
async executor() {},
});
const actionType = actionTypeService.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 actionTypeService = new ActionTypeService();
expect(() => actionTypeService.get('my-action-type')).toThrowErrorMatchingInlineSnapshot(
`"Action type \\"my-action-type\\" is not registered."`
);
});
});

describe('getUnencryptedAttributes()', () => {
test('returns empty array when unencryptedAttributes is undefined', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
async executor() {},
});
const result = actionTypeService.getUnencryptedAttributes('my-action-type');
expect(result).toEqual([]);
});

test('returns values inside unencryptedAttributes array when it exists', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
unencryptedAttributes: ['a', 'b', 'c'],
async executor() {},
});
const result = actionTypeService.getUnencryptedAttributes('my-action-type');
expect(result).toEqual(['a', 'b', 'c']);
});
});

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

describe('validateParams()', () => {
test('should pass when validation not defined', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
async executor() {},
});
actionTypeService.validateParams('my-action-type', {});
});

test('should validate and pass when params is valid', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
validate: {
params: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
async executor() {},
});
actionTypeService.validateParams('my-action-type', { param1: 'value' });
});

test('should validate and throw error when params is invalid', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
validate: {
params: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
async executor() {},
});
expect(() =>
actionTypeService.validateParams('my-action-type', {})
).toThrowErrorMatchingInlineSnapshot(
`"child \\"param1\\" fails because [\\"param1\\" is required]"`
);
});
});

describe('validateActionTypeConfig()', () => {
test('should pass when validation not defined', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
async executor() {},
});
actionTypeService.validateActionTypeConfig('my-action-type', {});
});

test('should validate and pass when actionTypeConfig is valid', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
validate: {
actionTypeConfig: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
async executor() {},
});
actionTypeService.validateActionTypeConfig('my-action-type', { param1: 'value' });
});

test('should validate and throw error when actionTypeConfig is invalid', () => {
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
validate: {
actionTypeConfig: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
async executor() {},
});
expect(() =>
actionTypeService.validateActionTypeConfig('my-action-type', {})
).toThrowErrorMatchingInlineSnapshot(
`"child \\"param1\\" fails because [\\"param1\\" is required]"`
);
});
});

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

test('returns true after registering an action type', () => {
const executor = jest.fn();
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
expect(actionTypeService.has('my-action-type'));
});
});

describe('execute()', () => {
test('calls the executor with proper params', async () => {
const executor = jest.fn().mockResolvedValueOnce({ success: true });
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
await actionTypeService.execute({
id: 'my-action-type',
actionTypeConfig: { foo: true },
params: { bar: false },
});
expect(executor).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"actionTypeConfig": Object {
"foo": true,
},
"params": Object {
"bar": false,
},
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});

test('validates params', async () => {
const executor = jest.fn().mockResolvedValueOnce({ success: true });
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
executor,
validate: {
params: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
});
await expect(
actionTypeService.execute({
id: 'my-action-type',
actionTypeConfig: {},
params: {},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"child \\"param1\\" fails because [\\"param1\\" is required]"`
);
});

test('validates actionTypeConfig', async () => {
const executor = jest.fn().mockResolvedValueOnce({ success: true });
const actionTypeService = new ActionTypeService();
actionTypeService.register({
id: 'my-action-type',
name: 'My action type',
executor,
validate: {
actionTypeConfig: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
});
await expect(
actionTypeService.execute({
id: 'my-action-type',
actionTypeConfig: {},
params: {},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"child \\"param1\\" fails because [\\"param1\\" is required]"`
);
});

test('throws error if action type not registered', async () => {
const actionTypeService = new ActionTypeService();
await expect(
actionTypeService.execute({
id: 'my-action-type',
actionTypeConfig: { foo: true },
params: { bar: false },
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Action type \\"my-action-type\\" is not registered."`
);
});
});
Loading