diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts new file mode 100644 index 000000000000000..1b140a6c50a3638 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts @@ -0,0 +1,20 @@ +/* + * 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'; + +interface SendPagerdutyOptions { + apiUrl: string; + data: any; + headers: Record; +} + +// post an event to pagerduty +export async function sendPagerduty(options: SendPagerdutyOptions): Promise { + const { apiUrl, data, headers } = options; + + return axios.post(apiUrl, data, { headers }); +} diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts index 16ec3f3737681bf..ff20828d83f04cb 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -4,15 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType, Services } from '../types'; +jest.mock('./lib/send_pagerduty', () => ({ + sendPagerduty: 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 { sendPagerduty } from './lib/send_pagerduty'; import { registerBuiltInActionTypes } from './index'; +const sendPagerdutyMock = sendPagerduty as jest.Mock; + const ACTION_TYPE_ID = '.pagerduty'; const NO_OP_FN = () => {}; @@ -26,6 +32,7 @@ function getServices(): Services { return services; } +let actionType: ActionType; let actionTypeRegistry: ActionTypeRegistry; const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); @@ -33,40 +40,34 @@ 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 is registered', () => { - test('gets registered with builtin actions', () => { +describe('action registation', () => { + test('should be successful', () => { expect(actionTypeRegistry.has(ACTION_TYPE_ID)).toEqual(true); }); }); describe('get()', () => { - test('returns action type', () => { - const actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + test('should return correct action type', () => { expect(actionType.id).toEqual(ACTION_TYPE_ID); expect(actionType.name).toEqual('pagerduty'); }); }); describe('validateConfig()', () => { - let actionType: ActionType; - - beforeAll(() => { - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); - expect(actionType).toBeTruthy(); - }); - test('should validate and pass when config is valid', () => { expect(validateConfig(actionType, {})).toEqual({ apiUrl: null }); expect(validateConfig(actionType, { apiUrl: 'bar' })).toEqual({ apiUrl: 'bar' }); @@ -89,13 +90,6 @@ describe('validateConfig()', () => { }); describe('validateSecrets()', () => { - let actionType: ActionType; - - beforeAll(() => { - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); - expect(actionType).toBeTruthy(); - }); - test('should validate and pass when secrets is valid', () => { const routingKey = '0123456789ABCDEF0123456789ABCDEF'; expect(validateSecrets(actionType, { routingKey })).toEqual({ @@ -126,17 +120,8 @@ describe('validateSecrets()', () => { }); describe('validateParams()', () => { - let actionType: ActionType; - - beforeAll(() => { - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); - expect(actionType).toBeTruthy(); - }); - test('should validate and pass when params is valid', () => { - expect(validateParams(actionType, {})).toEqual({ - eventAction: 'trigger', - }); + expect(validateParams(actionType, {})).toEqual({}); const params = { eventAction: 'trigger', @@ -165,7 +150,274 @@ describe('validateParams()', () => { }); describe('execute()', () => { - test('calls the executor with proper params', async () => { - expect('TBD').toEqual('TBD'); + beforeEach(() => { + sendPagerdutyMock.mockReset(); + }); + + test('should succeed with minimal valid params', async () => { + const secrets = { routingKey: 'super-secret' }; + const config = {}; + const params = {}; + + sendPagerdutyMock.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); + expect(sendPagerdutyMock.mock.calls[0][0]).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', + }; + + sendPagerdutyMock.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); + expect(sendPagerdutyMock.mock.calls[0][0]).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', + }; + + sendPagerdutyMock.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); + expect(sendPagerdutyMock.mock.calls[0][0]).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', + }; + + sendPagerdutyMock.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); + expect(sendPagerdutyMock.mock.calls[0][0]).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 = {}; + + sendPagerdutyMock.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 = {}; + + sendPagerdutyMock.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 = {}; + + sendPagerdutyMock.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 = {}; + + sendPagerdutyMock.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", + } + `); }); }); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts index 766ba55feed4cf8..c8e32aa2f938ad9 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; - +import { sendPagerduty } from './lib/send_pagerduty'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; // uses the PagerDuty Events API v2 @@ -38,16 +37,11 @@ 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), - ], - { - defaultValue: EVENT_ACTION_TRIGGER, - } -); +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'), @@ -58,7 +52,7 @@ const PayloadSeveritySchema = schema.oneOf([ const ParamsSchema = schema.object( { - eventAction: EventActionSchema, + eventAction: schema.maybe(EventActionSchema), dedupKey: schema.maybe(schema.string({ maxLength: 255 })), summary: schema.maybe(schema.string({ maxLength: 1024 })), source: schema.maybe(schema.string()), @@ -111,27 +105,28 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise= 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, - }; - } - - if (response.status === 429) { - return { - status: 'error', - message: `an error occurred in action ${id} sending a pagerduty event, retry later`, retry: true, }; } - if (response.status >= 500) { - return { - status: 'error', - message: `an http error ${response.status} occurred in action ${id} sending a pagerduty event, retry later`, - 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, + }, + }); - const message = 'unexpected error from pagerduty'; return { status: 'error', message, - data: response.data, }; } @@ -187,11 +180,8 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise