Adds a built-in PagerDuty action (#43395)

The PagerDuty action can be used to post events via the PagerDuty
Events API v2:

https://v2.developer.pagerduty.com/docs/events-api-v2
This commit is contained in:
Patrick Mueller 2019-08-26 12:55:33 -04:00 committed by GitHub
parent 70546628de
commit dedfd62717
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 949 additions and 48 deletions

View file

@ -10,10 +10,12 @@ import { actionType as serverLogActionType } from './server_log';
import { actionType as slackActionType } from './slack';
import { actionType as emailActionType } from './email';
import { actionType as indexActionType } from './es_index';
import { actionType as pagerDutyActionType } from './pagerduty';
export function registerBuiltInActionTypes(actionTypeRegistry: ActionTypeRegistry) {
actionTypeRegistry.register(serverLogActionType);
actionTypeRegistry.register(slackActionType);
actionTypeRegistry.register(emailActionType);
actionTypeRegistry.register(indexActionType);
actionTypeRegistry.register(pagerDutyActionType);
}

View file

@ -0,0 +1,26 @@
/*
* 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 axios, { AxiosResponse } from 'axios';
import { Services } from '../../types';
interface PostPagerdutyOptions {
apiUrl: string;
data: any;
headers: Record<string, string>;
services: Services;
}
// post an event to pagerduty
export async function postPagerduty(options: PostPagerdutyOptions): Promise<AxiosResponse> {
const { apiUrl, data, headers } = options;
const axiosOptions = {
headers,
validateStatus: () => true,
};
return axios.post(apiUrl, data, axiosOptions);
}

View file

@ -0,0 +1,419 @@
/*
* 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/post_pagerduty', () => ({
postPagerduty: jest.fn(),
}));
import { ActionType, Services, ActionTypeExecutorOptions } from '../types';
import { ActionTypeRegistry } from '../action_type_registry';
import { taskManagerMock } from '../../../task_manager/task_manager.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { postPagerduty } from './lib/post_pagerduty';
import { registerBuiltInActionTypes } from './index';
const postPagerdutyMock = postPagerduty as jest.Mock;
const ACTION_TYPE_ID = '.pagerduty';
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 actionType: ActionType;
let actionTypeRegistry: ActionTypeRegistry;
const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
beforeAll(() => {
actionTypeRegistry = new ActionTypeRegistry({
getServices,
isSecurityEnabled: true,
taskManager: taskManagerMock.create(),
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
getBasePath: jest.fn().mockReturnValue(undefined),
});
registerBuiltInActionTypes(actionTypeRegistry);
actionType = actionTypeRegistry.get(ACTION_TYPE_ID);
});
beforeEach(() => {
services.log = NO_OP_FN;
});
describe('action registation', () => {
test('should be successful', () => {
expect(actionTypeRegistry.has(ACTION_TYPE_ID)).toEqual(true);
});
});
describe('get()', () => {
test('should return correct action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
expect(actionType.name).toEqual('pagerduty');
});
});
describe('validateConfig()', () => {
test('should validate and pass when config is valid', () => {
expect(validateConfig(actionType, {})).toEqual({ apiUrl: null });
expect(validateConfig(actionType, { apiUrl: 'bar' })).toEqual({ apiUrl: 'bar' });
});
test('should validate and throw error when config is invalid', () => {
expect(() => {
validateConfig(actionType, { shouldNotBeHere: true });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [shouldNotBeHere]: definition for this key is missing"`
);
});
});
describe('validateSecrets()', () => {
test('should validate and pass when secrets is valid', () => {
const routingKey = 'super-secret';
expect(validateSecrets(actionType, { routingKey })).toEqual({
routingKey,
});
});
test('should validate and throw error when secrets is invalid', () => {
expect(() => {
validateSecrets(actionType, { routingKey: false });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [routingKey]: expected value of type [string] but got [boolean]"`
);
expect(() => {
validateSecrets(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]"`
);
});
});
describe('validateParams()', () => {
test('should validate and pass when params is valid', () => {
expect(validateParams(actionType, {})).toEqual({});
const params = {
eventAction: 'trigger',
dedupKey: 'a dedupKey',
summary: 'a summary',
source: 'a source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'a component',
group: 'a group',
class: 'a class',
};
expect(validateParams(actionType, params)).toEqual(params);
});
test('should validate and throw error when params is invalid', () => {
expect(() => {
validateParams(actionType, { eventAction: 'ackynollage' });
}).toThrowErrorMatchingInlineSnapshot(`
"error validating action params: [eventAction]: types that failed validation:
- [eventAction.0]: expected value to equal [trigger] but got [ackynollage]
- [eventAction.1]: expected value to equal [resolve] but got [ackynollage]
- [eventAction.2]: expected value to equal [acknowledge] but got [ackynollage]"
`);
});
});
describe('execute()', () => {
beforeEach(() => {
postPagerdutyMock.mockReset();
});
test('should succeed with minimal valid params', async () => {
const secrets = { routingKey: 'super-secret' };
const config = {};
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "https://events.pagerduty.com/v2/enqueue",
"data": Object {
"dedup_key": "action:some-action-id",
"event_action": "trigger",
"payload": Object {
"severity": "info",
"source": "Kibana Action some-action-id",
"summary": "No summary provided.",
},
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"data": "data-here",
"status": "ok",
}
`);
});
test('should succeed with maximal valid params for trigger', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params = {
eventAction: 'trigger',
dedupKey: 'a-dedup-key',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
component: 'the-component',
group: 'the-group',
class: 'the-class',
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "trigger",
"payload": Object {
"class": "the-class",
"component": "the-component",
"group": "the-group",
"severity": "critical",
"source": "the-source",
"summary": "the summary",
"timestamp": "1963-09-23T01:23:45.000Z",
},
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"data": "data-here",
"status": "ok",
}
`);
});
test('should succeed with maximal valid params for acknowledge', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params = {
eventAction: 'acknowledge',
dedupKey: 'a-dedup-key',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
component: 'the-component',
group: 'the-group',
class: 'the-class',
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "acknowledge",
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"data": "data-here",
"status": "ok",
}
`);
});
test('should succeed with maximal valid params for resolve', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params = {
eventAction: 'resolve',
dedupKey: 'a-dedup-key',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
component: 'the-component',
group: 'the-group',
class: 'the-class',
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "resolve",
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"data": "data-here",
"status": "ok",
}
`);
});
test('should fail when sendPagerdury throws', async () => {
const secrets = { routingKey: 'super-secret' };
const config = {};
const params = {};
postPagerdutyMock.mockImplementation(() => {
throw new Error('doing some testing');
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"message": "error in pagerduty action \\"some-action-id\\" posting event: doing some testing",
"status": "error",
}
`);
});
test('should fail when sendPagerdury returns 429', async () => {
const secrets = { routingKey: 'super-secret' };
const config = {};
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 429, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"message": "error in pagerduty action \\"some-action-id\\" posting event: status 429, retry later",
"retry": true,
"status": "error",
}
`);
});
test('should fail when sendPagerdury returns 501', async () => {
const secrets = { routingKey: 'super-secret' };
const config = {};
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 501, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"message": "error in pagerduty action \\"some-action-id\\" posting event: status 501, retry later",
"retry": true,
"status": "error",
}
`);
});
test('should fail when sendPagerdury returns 418', async () => {
const secrets = { routingKey: 'super-secret' };
const config = {};
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 418, data: 'data-here' };
});
const id = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services };
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"message": "error in pagerduty action \\"some-action-id\\" posting event: unexpected status 418",
"status": "error",
}
`);
});
});

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { postPagerduty } from './lib/post_pagerduty';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
// uses the PagerDuty Events API v2
// https://v2.developer.pagerduty.com/docs/events-api-v2
const PAGER_DUTY_API_URL = 'https://events.pagerduty.com/v2/enqueue';
// config definition
export type ActionTypeConfigType = TypeOf<typeof ConfigSchema>;
const ConfigSchema = schema.object({
apiUrl: schema.nullable(schema.string()),
});
// secrets definition
export type ActionTypeSecretsType = TypeOf<typeof SecretsSchema>;
const SecretsSchema = schema.object({
routingKey: schema.string(),
});
// params definition
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
const EVENT_ACTION_TRIGGER = 'trigger';
const EVENT_ACTION_RESOLVE = 'resolve';
const EVENT_ACTION_ACKNOWLEDGE = 'acknowledge';
const EventActionSchema = schema.oneOf([
schema.literal(EVENT_ACTION_TRIGGER),
schema.literal(EVENT_ACTION_RESOLVE),
schema.literal(EVENT_ACTION_ACKNOWLEDGE),
]);
const PayloadSeveritySchema = schema.oneOf([
schema.literal('critical'),
schema.literal('error'),
schema.literal('warning'),
schema.literal('info'),
]);
const ParamsSchema = schema.object(
{
eventAction: schema.maybe(EventActionSchema),
dedupKey: schema.maybe(schema.string({ maxLength: 255 })),
summary: schema.maybe(schema.string({ maxLength: 1024 })),
source: schema.maybe(schema.string()),
severity: schema.maybe(PayloadSeveritySchema),
timestamp: schema.maybe(schema.string()),
component: schema.maybe(schema.string()),
group: schema.maybe(schema.string()),
class: schema.maybe(schema.string()),
},
{ validate: validateParams }
);
function validateParams(paramsObject: any): string | void {
const params: ActionParamsType = paramsObject;
const { timestamp } = params;
if (timestamp != null) {
let date;
try {
date = Date.parse(timestamp);
} catch (err) {
return 'error parsing timestamp: ${err.message}';
}
if (isNaN(date)) {
return 'error parsing timestamp';
}
}
}
// action type definition
export const actionType: ActionType = {
id: '.pagerduty',
name: 'pagerduty',
validate: {
config: ConfigSchema,
secrets: SecretsSchema,
params: ParamsSchema,
},
executor,
};
// action executor
async function executor(execOptions: ActionTypeExecutorOptions): Promise<ActionTypeExecutorResult> {
const id = execOptions.id;
const config = execOptions.config as ActionTypeConfigType;
const secrets = execOptions.secrets as ActionTypeSecretsType;
const params = execOptions.params as ActionParamsType;
const services = execOptions.services;
const apiUrl = config.apiUrl || PAGER_DUTY_API_URL;
const headers = {
'Content-Type': 'application/json',
'X-Routing-Key': secrets.routingKey,
};
const data = getBodyForEventAction(id, params);
let response;
try {
response = await postPagerduty({ apiUrl, data, headers, services });
} catch (err) {
const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', {
defaultMessage: 'error in pagerduty action "{id}" posting event: {errorMessage}',
values: {
id,
errorMessage: err.message,
},
});
services.log(
['warn', 'actions', 'pagerduty'],
`error thrown posting pagerduty event: ${err.message}`
);
return {
status: 'error',
message,
};
}
services.log(
['debug', 'actions', 'pagerduty'],
`response posting pagerduty event: ${response.status}`
);
if (response.status === 202) {
return {
status: 'ok',
data: response.data,
};
}
if (response.status === 429 || response.status >= 500) {
const message = i18n.translate('xpack.actions.builtin.pagerduty.postingRetryErrorMessage', {
defaultMessage:
'error in pagerduty action "{id}" posting event: status {status}, retry later',
values: {
id,
status: response.status,
},
});
return {
status: 'error',
message,
retry: true,
};
}
const message = i18n.translate('xpack.actions.builtin.pagerduty.postingUnexpectedErrorMessage', {
defaultMessage: 'error in pagerduty action "{id}" posting event: unexpected status {status}',
values: {
id,
status: response.status,
},
});
return {
status: 'error',
message,
};
}
// utilities
const AcknowledgeOrResolve = new Set([EVENT_ACTION_ACKNOWLEDGE, EVENT_ACTION_RESOLVE]);
function getBodyForEventAction(actionId: string, params: ActionParamsType): any {
const eventAction = params.eventAction || EVENT_ACTION_TRIGGER;
const dedupKey = params.dedupKey || `action:${actionId}`;
const data: any = {
event_action: eventAction,
dedup_key: dedupKey,
};
// for acknowledge / resolve, just send the dedup key
if (AcknowledgeOrResolve.has(eventAction)) {
return data;
}
data.payload = {
summary: params.summary || 'No summary provided.',
source: params.source || `Kibana Action ${actionId}`,
severity: params.severity || 'info',
};
if (params.timestamp != null) data.payload.timestamp = params.timestamp;
if (params.component != null) data.payload.component = params.component;
if (params.group != null) data.payload.group = params.group;
if (params.class != null) data.payload.class = params.class;
return data;
}

View file

@ -1,56 +1,46 @@
functional test server slack simulator
================================================================================
The code in this directory will run a Slack HTTP simulator; it will return
different responses based on the content of the text message sent to the
endpoint.
The code in this directory includes external service simulators for testing
Kibana actions. The simulators are available when running the function test
server.
This will be used during functional testing runner tests for actions; an
action will be created pointing to the simulator, and then messages posted
to test handling different error conditions.
They are used during function testing for actions; an action will be created
pointing to the simulator, and then messages posted to test handling different
error conditions.
Generally, the simulator will generate specialized http responses based on
some string property passed as input. Consult the simulators for more details.
what a Slack server returns
simulator usage
--------------------------------------------------------------------------------
Here's some examples of `curl`'ing a Slack webhook to see the different
responses it will return:
This may get out of date, consult the code for exact urls and inputs. Each
simulator's last path segment should be the name of the service (eg, slack,
pagerduty, etc).
```console
$ curl -v $SLACK_WEBHOOK_URL -d '{"text":"Hello, World!"}'
< HTTP/2 200
< content-type: text/html
$ export SLACK_URL=http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/slack
$ curl -v $SLACK_URL -H 'content-type: application/json' -d '{"text":"success"}'
< HTTP/1.1 200 OK
...
ok
$ curl -v $SLACK_WEBHOOK_URL -d '{"txt":"Hello, World!"}'
< HTTP/2 400
< content-type: text/html
no_text
$ curl -v $SLACK_WEBHOOK_URL -d '[]'
< HTTP/2 400
< content-type: text/html
invalid_payload
$ curl -v $SLACK_WEBHOOK_URL_LESS_ONE_CHAR -d '{"text":"Hello, World!"}'
< HTTP/2 403
< content-type: text/html
invalid_token
$ curl -v $SLACK_WEBHOOK_URL -d '{"text":"rate limited yet?"}'
< HTTP/2 429
< content-type: application/json; charset=utf-8
$ curl -v $SLACK_URL -H 'content-type: application/json' -d '{"text":"rate_limit"}'
< HTTP/1.1 429 Too Many Requests
...
< retry-after: 1
<
{"retry_after":1,"ok":false,"error":"rate_limited"}
```
abuse a server
bonus points: abuse a slack server
--------------------------------------------------------------------------------
To get a rate limiting response, run this in one terminal window, and while
that is running, run a normal curl command to post a message. You may need to
try a few times.
To get a rate limiting slack response, from a real slack server, to see what it
looks like, run this in one terminal window, and while that is running, run a
normal curl command to post a message. You may need to try a few times.
You should probably do this with a personal slack instance, not a company one :-)
@ -58,16 +48,3 @@ You should probably do this with a personal slack instance, not a company one :-
$ autocannon --amount 10000 --method POST --body '{"text":"Hello, World!"}' $SLACK_WEBHOOK_URL
```
simulator usage
--------------------------------------------------------------------------------
These may get out of date, consult the code for exact urls and inputs:
```console
$ export SLACK_URL=http://localhost:5620/api/_actions-FTS-external-service-simulators/slack
$ curl -v $SLACK_URL -H 'content-type: application/json' -d '{"text":"slack-success"}'
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
ok
```

View file

@ -6,12 +6,14 @@
import Hapi from 'hapi';
import { initPlugin as initSlack } from './slack_simulation';
import { initPlugin as initWebhook } from './webhook_simulation';
import { initPlugin as initPagerduty } from './pagerduty_simulation';
const NAME = 'actions-FTS-external-service-simulators';
export enum ExternalServiceSimulator {
SLACK = 'slack',
WEBHOOK = 'webhook',
PAGERDUTY = 'pagerduty',
}
export function getExternalServiceSimulatorPath(service: ExternalServiceSimulator): string {
@ -32,6 +34,7 @@ export default function(kibana: any) {
init: (server: Hapi.Server) => {
initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK));
initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK));
initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY));
},
});
}

View file

@ -0,0 +1,79 @@
/*
* 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 Hapi from 'hapi';
interface PagerdutyRequest extends Hapi.Request {
payload: {
dedup_key: string;
payload: {
summary: string;
};
};
}
export function initPlugin(server: Hapi.Server, path: string) {
server.route({
method: 'POST',
path,
options: {
auth: false,
validate: {
options: { abortEarly: false },
payload: Joi.object()
.unknown(true)
.keys({
dedup_key: Joi.string(),
payload: Joi.object()
.unknown(true)
.keys({
summary: Joi.string(),
}),
}),
},
},
handler: pagerdutyHandler,
});
}
// Pagerduty simulator: create an action pointing here, and you can get
// different responses based on the message posted. See the README.md for
// more info.
function pagerdutyHandler(request: PagerdutyRequest, h: any) {
const body = request.payload;
let dedupKey = body && body.dedup_key;
const summary = body && body.payload && body.payload.summary;
if (dedupKey == null) {
dedupKey = `kibana-ft-simulator-dedup-key-${new Date().toISOString()}`;
}
switch (summary) {
case 'respond-with-429':
return jsonResponse(h, 429);
case 'respond-with-502':
return jsonResponse(h, 502);
case 'respond-with-418':
return jsonResponse(h, 418);
}
return jsonResponse(h, 202, {
status: 'success',
message: 'Event processed',
dedup_key: dedupKey,
});
}
function jsonResponse(h: any, code: number, object?: any) {
if (object == null) {
return h.response('').code(code);
}
return h
.response(JSON.stringify(object))
.type('application/json')
.code(code);
}

View file

@ -0,0 +1,185 @@
/*
* 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 { FtrProviderContext } from '../../../../common/ftr_provider_context';
import {
getExternalServiceSimulatorPath,
ExternalServiceSimulator,
} from '../../../../common/fixtures/plugins/actions';
// eslint-disable-next-line import/no-default-export
export default function pagerdutyTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('pagerduty action', () => {
let simulatedActionId = '';
let pagerdutySimulatorURL: string = '<could not determine kibana url>';
// need to wait for kibanaServer to settle ...
before(() => {
const kibanaServer = getService('kibanaServer');
const kibanaUrl = kibanaServer.status && kibanaServer.status.kibanaServerUrl;
pagerdutySimulatorURL = `${kibanaUrl}${getExternalServiceSimulatorPath(
ExternalServiceSimulator.PAGERDUTY
)}`;
});
after(() => esArchiver.unload('empty_kibana'));
it('should return successfully when passed valid create parameters', async () => {
const { body: createdAction } = await supertest
.post('/api/action')
.set('kbn-xsrf', 'foo')
.send({
description: 'A pagerduty action',
actionTypeId: '.pagerduty',
secrets: {
routingKey: 'pager-duty-routing-key',
},
})
.expect(200);
expect(createdAction).to.eql({
id: createdAction.id,
description: 'A pagerduty action',
actionTypeId: '.pagerduty',
config: {
apiUrl: null,
},
});
expect(typeof createdAction.id).to.be('string');
const { body: fetchedAction } = await supertest
.get(`/api/action/${createdAction.id}`)
.expect(200);
expect(fetchedAction).to.eql({
id: fetchedAction.id,
description: 'A pagerduty action',
actionTypeId: '.pagerduty',
config: {
apiUrl: null,
},
});
});
it('should return unsuccessfully when passed invalid create parameters', async () => {
await supertest
.post('/api/action')
.set('kbn-xsrf', 'foo')
.send({
description: 'A pagerduty action',
actionTypeId: '.pagerduty',
secrets: {},
})
.expect(400)
.then((resp: any) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]',
});
});
});
it('should create pagerduty simulator action successfully', async () => {
const { body: createdSimulatedAction } = await supertest
.post('/api/action')
.set('kbn-xsrf', 'foo')
.send({
description: 'A pagerduty simulator',
actionTypeId: '.pagerduty',
config: {
apiUrl: pagerdutySimulatorURL,
},
secrets: {
routingKey: 'pager-duty-routing-key',
},
})
.expect(200);
simulatedActionId = createdSimulatedAction.id;
});
it('should handle executing with a simulated success', async () => {
const { body: result } = await supertest
.post(`/api/action/${simulatedActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
summary: 'just a test',
},
})
.expect(200);
expect(result).to.eql({
status: 'ok',
data: {
dedup_key: `action:${simulatedActionId}`,
message: 'Event processed',
status: 'success',
},
});
});
it('should handle a 40x pagerduty error', async () => {
const { body: result } = await supertest
.post(`/api/action/${simulatedActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
summary: 'respond-with-418',
},
})
.expect(200);
expect(result.status).to.equal('error');
expect(result.message).to.match(
/error in pagerduty action .+ posting event: unexpected status 418/
);
});
it('should handle a 429 pagerduty error', async () => {
const { body: result } = await supertest
.post(`/api/action/${simulatedActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
summary: 'respond-with-429',
},
})
.expect(200);
expect(result.status).to.equal('error');
expect(result.message).to.match(
/error in pagerduty action .+ posting event: status 429, retry later/
);
expect(result.retry).to.equal(true);
});
it('should handle a 500 pagerduty error', async () => {
const { body: result } = await supertest
.post(`/api/action/${simulatedActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
summary: 'respond-with-502',
},
})
.expect(200);
expect(result.status).to.equal('error');
expect(result.message).to.match(
/error in pagerduty action .+ posting event: status 502, retry later/
);
expect(result.retry).to.equal(true);
});
});
}

View file

@ -20,5 +20,6 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./builtin_action_types/slack'));
loadTestFile(require.resolve('./builtin_action_types/email'));
loadTestFile(require.resolve('./builtin_action_types/es_index'));
loadTestFile(require.resolve('./builtin_action_types/pagerduty'));
});
}