Adds a slack action to the x-pack actions plugin (#39221)
This commit is contained in:
parent
4afed1af4b
commit
9ac21391f3
|
@ -7,7 +7,9 @@
|
|||
import { ActionTypeRegistry } from '../action_type_registry';
|
||||
|
||||
import { actionType as serverLogActionType } from './server_log';
|
||||
import { actionType as slackActionType } from './slack';
|
||||
|
||||
export function registerBuiltInActionTypes(actionTypeRegistry: ActionTypeRegistry) {
|
||||
actionTypeRegistry.register(serverLogActionType);
|
||||
actionTypeRegistry.register(slackActionType);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionType } from '../types';
|
||||
import { ActionType, Services } from '../types';
|
||||
import { ActionTypeRegistry } from '../action_type_registry';
|
||||
import { taskManagerMock } from '../../../task_manager/task_manager.mock';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
|
||||
|
@ -16,13 +16,13 @@ import { registerBuiltInActionTypes } from './index';
|
|||
const ACTION_TYPE_ID = 'kibana.server-log';
|
||||
const NO_OP_FN = () => {};
|
||||
|
||||
const services = {
|
||||
const services: Services = {
|
||||
log: NO_OP_FN,
|
||||
callCluster: async (path: string, opts: any) => {},
|
||||
savedObjectsClient: SavedObjectsClientMock.create(),
|
||||
};
|
||||
|
||||
function getServices() {
|
||||
function getServices(): Services {
|
||||
return services;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 { ActionType, Services, ActionTypeExecutorOptions } from '../types';
|
||||
import { ActionTypeRegistry } from '../action_type_registry';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
|
||||
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
import { validateActionTypeParams } from '../lib';
|
||||
import { validateActionTypeConfig } from '../lib';
|
||||
import { getActionType } from './slack';
|
||||
import { taskManagerMock } from '../../../task_manager/task_manager.mock';
|
||||
|
||||
const ACTION_TYPE_ID = '.slack';
|
||||
|
||||
const NO_OP_FN = () => {};
|
||||
|
||||
const services: Services = {
|
||||
log: NO_OP_FN,
|
||||
callCluster: async (path: string, opts: any) => {},
|
||||
savedObjectsClient: SavedObjectsClientMock.create(),
|
||||
};
|
||||
|
||||
function getServices(): Services {
|
||||
return services;
|
||||
}
|
||||
|
||||
let actionTypeRegistry: ActionTypeRegistry;
|
||||
let actionType: ActionType;
|
||||
|
||||
const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
||||
|
||||
async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise<any> {
|
||||
const { params } = options;
|
||||
const { message } = params;
|
||||
if (message == null) throw new Error('message property required in parameter');
|
||||
|
||||
const failureMatch = message.match(/^failure: (.*)$/);
|
||||
if (failureMatch != null) {
|
||||
const failMessage = failureMatch[1];
|
||||
throw new Error(`slack mockExecutor failure: ${failMessage}`);
|
||||
}
|
||||
|
||||
return {
|
||||
text: `slack mockExecutor success: ${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
actionTypeRegistry = new ActionTypeRegistry({
|
||||
getServices,
|
||||
taskManager: taskManagerMock.create(),
|
||||
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
|
||||
});
|
||||
actionTypeRegistry.register(getActionType({ executor: mockSlackExecutor }));
|
||||
actionType = actionTypeRegistry.get(ACTION_TYPE_ID);
|
||||
|
||||
test('ensure action type is valid', () => {
|
||||
expect(actionType).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('action is registered', () => {
|
||||
test('gets registered with builtin actions', () => {
|
||||
expect(actionTypeRegistry.has(ACTION_TYPE_ID)).toEqual(true);
|
||||
});
|
||||
|
||||
test('returns action type', () => {
|
||||
const returnedActionType = actionTypeRegistry.get(ACTION_TYPE_ID);
|
||||
expect(returnedActionType.id).toEqual(ACTION_TYPE_ID);
|
||||
expect(returnedActionType.name).toEqual('slack');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateParams()', () => {
|
||||
test('should validate and pass when params is valid', () => {
|
||||
expect(validateActionTypeParams(actionType, { message: 'a message' })).toEqual({
|
||||
message: 'a message',
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate and throw error when params is invalid', () => {
|
||||
expect(() => {
|
||||
validateActionTypeParams(actionType, {});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"params invalid: child \\"message\\" fails because [\\"message\\" is required]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateActionTypeParams(actionType, { message: 1 });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"params invalid: child \\"message\\" fails because [\\"message\\" must be a string]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateActionTypeConfig()', () => {
|
||||
test('should validate and pass when config is valid', () => {
|
||||
validateActionTypeConfig(actionType, {
|
||||
webhookUrl: 'https://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate and throw error when config is invalid', () => {
|
||||
expect(() => {
|
||||
validateActionTypeConfig(actionType, {});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"The following actionTypeConfig attributes are invalid: webhookUrl [any.required]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateActionTypeConfig(actionType, { webhookUrl: 1 });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"The following actionTypeConfig attributes are invalid: webhookUrl [string.base]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
test('calls the mock executor with success', async () => {
|
||||
const response = await actionType.executor({
|
||||
services,
|
||||
config: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"text": "slack mockExecutor success: this invocation should succeed",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('calls the mock executor with failure', async () => {
|
||||
await expect(
|
||||
actionType.executor({
|
||||
services,
|
||||
config: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'failure: this invocation should fail' },
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"slack mockExecutor failure: this invocation should fail"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { IncomingWebhook } from '@slack/webhook';
|
||||
|
||||
import { ActionType, ActionTypeExecutorOptions, ExecutorType } from '../types';
|
||||
|
||||
const CONFIG_SCHEMA = Joi.object()
|
||||
.keys({
|
||||
webhookUrl: Joi.string().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const PARAMS_SCHEMA = Joi.object()
|
||||
.keys({
|
||||
message: Joi.string().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
// customizing executor is only used for tests
|
||||
export function getActionType({ executor }: { executor?: ExecutorType } = {}): ActionType {
|
||||
if (executor == null) executor = slackExecutor;
|
||||
|
||||
return {
|
||||
id: '.slack',
|
||||
name: 'slack',
|
||||
unencryptedAttributes: [],
|
||||
validate: {
|
||||
params: PARAMS_SCHEMA,
|
||||
config: CONFIG_SCHEMA,
|
||||
},
|
||||
executor,
|
||||
};
|
||||
}
|
||||
|
||||
// the production executor for this action
|
||||
export const actionType = getActionType();
|
||||
|
||||
async function slackExecutor({
|
||||
config,
|
||||
params,
|
||||
services,
|
||||
}: ActionTypeExecutorOptions): Promise<any> {
|
||||
const { webhookUrl } = config;
|
||||
const { message } = params;
|
||||
|
||||
const webhook = new IncomingWebhook(webhookUrl);
|
||||
|
||||
return await webhook.send(message);
|
||||
}
|
|
@ -33,6 +33,8 @@ export interface ActionTypeExecutorOptions {
|
|||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
export type ExecutorType = (options: ActionTypeExecutorOptions) => Promise<any>;
|
||||
|
||||
export interface ActionType {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -41,5 +43,5 @@ export interface ActionType {
|
|||
params?: any;
|
||||
config?: any;
|
||||
};
|
||||
executor({ services, config, params }: ActionTypeExecutorOptions): Promise<any>;
|
||||
executor: ExecutorType;
|
||||
}
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
"@samverschueren/stream-to-observable": "^0.3.0",
|
||||
"@scant/router": "^0.1.0",
|
||||
"@slack/client": "^4.8.0",
|
||||
"@slack/webhook": "^5.0.0",
|
||||
"@turf/boolean-contains": "6.0.1",
|
||||
"angular-resource": "1.4.9",
|
||||
"angular-sanitize": "1.6.5",
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function slackTest({ getService }: KibanaFunctionalTestDefaultProviders) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('create slack action', () => {
|
||||
it('should return 200 when creating a slack action successfully', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
attributes: {
|
||||
description: 'A slack action',
|
||||
actionTypeId: '.slack',
|
||||
actionTypeConfig: {
|
||||
webhookUrl: 'http://example.com',
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
type: 'action',
|
||||
id: resp.body.id,
|
||||
attributes: {
|
||||
description: 'A slack action',
|
||||
actionTypeId: '.slack',
|
||||
actionTypeConfig: {},
|
||||
},
|
||||
references: [],
|
||||
updated_at: resp.body.updated_at,
|
||||
version: resp.body.version,
|
||||
});
|
||||
expect(typeof resp.body.id).to.be('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with no webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
attributes: {
|
||||
description: 'A slack action',
|
||||
actionTypeId: '.slack',
|
||||
actionTypeConfig: {},
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'The following actionTypeConfig attributes are invalid: webhookUrl [any.required]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: once we have the HTTP API fire action, test that with a webhook url pointing
|
||||
// back to the Kibana server
|
||||
}
|
|
@ -16,5 +16,6 @@ export default function actionsTests({ loadTestFile }: KibanaFunctionalTestDefau
|
|||
loadTestFile(require.resolve('./list_action_types'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/server_log'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/slack'));
|
||||
});
|
||||
}
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -2692,6 +2692,20 @@
|
|||
retry "^0.12.0"
|
||||
ws "^5.2.0"
|
||||
|
||||
"@slack/types@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.0.0.tgz#1dc7a63b293c4911e474197585c3feda012df17a"
|
||||
integrity sha512-IktC4uD/CHfLQcSitKSmjmRu4a6+Nf/KzfS6dTgUlDzENhh26l8aESKAuIpvYD5VOOE6NxDDIAdPJOXBvUGxlg==
|
||||
|
||||
"@slack/webhook@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@slack/webhook/-/webhook-5.0.0.tgz#0044a3940afc16cbc607c71acdffddb9e9d4f161"
|
||||
integrity sha512-cDj3kz3x9z9271xPNzlwb90DpKTYybG2OWPJHigJL8FegR80rzQyD0v4bGuStGGkHbAYDKE2BMpJambR55hnSg==
|
||||
dependencies:
|
||||
"@slack/types" "^1.0.0"
|
||||
"@types/node" ">=8.9.0"
|
||||
axios "^0.18.0"
|
||||
|
||||
"@storybook/addon-actions@^5.0.5":
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.0.5.tgz#9179d08262c326c865021f5ecd173708c82edc87"
|
||||
|
@ -3890,7 +3904,7 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@10.12.27", "@types/node@8.5.8", "@types/node@>=6.0.0", "@types/node@^10.12.27", "@types/node@^12.0.2":
|
||||
"@types/node@*", "@types/node@10.12.27", "@types/node@8.5.8", "@types/node@>=6.0.0", "@types/node@>=8.9.0", "@types/node@^10.12.27", "@types/node@^12.0.2":
|
||||
version "10.12.27"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.27.tgz#eb3843f15d0ba0986cc7e4d734d2ee8b50709ef8"
|
||||
integrity sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg==
|
||||
|
|
Loading…
Reference in a new issue