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

[ResponseOps][Stack Connectors] Opsgenie backend #142164

Merged
merged 21 commits into from
Oct 11, 2022
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
33bcdcc
Starting opsgenie backend
jonathan-buttner Sep 28, 2022
7413f2c
Merge branch 'main' of github.com:elastic/kibana into opsgenie-connector
jonathan-buttner Sep 29, 2022
dcb1fd9
Adding more integration tests
jonathan-buttner Sep 29, 2022
b6950fc
Updating readme
jonathan-buttner Sep 30, 2022
0ba5a6c
Merge branch 'main' of github.com:elastic/kibana into opsgenie-connector
jonathan-buttner Oct 4, 2022
0111502
Adding hash and alias
jonathan-buttner Oct 4, 2022
af4a94a
Fixing tests
jonathan-buttner Oct 5, 2022
3eb263a
Switch to platinum for now
jonathan-buttner Oct 5, 2022
854492b
Adding server side translations
jonathan-buttner Oct 5, 2022
a08975b
Merge branch 'main' of github.com:elastic/kibana into opsgenie-connector
jonathan-buttner Oct 5, 2022
9892a2d
Fixing merge issues
jonathan-buttner Oct 5, 2022
40bbcc4
Merge branch 'main' of github.com:elastic/kibana into opsgenie-connector
jonathan-buttner Oct 6, 2022
c79afdc
Fixing file location error
jonathan-buttner Oct 6, 2022
b2b7e53
Fixing test
jonathan-buttner Oct 6, 2022
d3012f9
Addressing feedback
jonathan-buttner Oct 10, 2022
cf442ad
Merge branch 'main' of github.com:elastic/kibana into opsgenie-connector
jonathan-buttner Oct 10, 2022
22d32ad
Removing details validation
jonathan-buttner Oct 11, 2022
13431f0
Merge branch 'main' into opsgenie-connector
kibanamachine Oct 11, 2022
b233bbb
Merge branch 'main' into opsgenie-connector
kibanamachine Oct 11, 2022
fef41ba
Fixing close alert alias bug
jonathan-buttner Oct 11, 2022
5483dbb
Merge branch 'opsgenie-connector' of github.com:jonathan-buttner/kiba…
jonathan-buttner Oct 11, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ export abstract class SubActionConnector<Config, Secrets> {
`Request to external service failed. Connector Id: ${this.connector.id}. Connector type: ${this.connector.type}. Method: ${error.config.method}. URL: ${error.config.url}`
);

const errorMessage = this.getResponseErrorMessage(error);
const errorMessage = `Status code: ${error.status}. Message: ${this.getResponseErrorMessage(
error
)}`;
throw new Error(errorMessage);
}

Expand Down
28 changes: 27 additions & 1 deletion x-pack/plugins/stack_connectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,32 @@ The following table describes the properties of the `incident` object.
| severity | The severity of the incident. | string _(optional)_ |
---

## Ospgenie

Refer to the [Run connector API documentation](https://www.elastic.co/guide/en/kibana/master/execute-connector-api.html#execute-connector-api-request-body)
for the full list of properties.

### `params`

| Property | Description | Type |
| --------------- | ------------------------------------------------------------------ | ------ |
| subAction | The subaction to perform. It can be `createAlert` or `closeAlert`. | string |
| subActionParams | The parameters of the subaction. | object |

`subActionParams (createAlert)`

| Property | Description | Type |
| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
| message | The alert message. | string |

The optional parameters `alias`, `description`, `responders`, `visibleTo`, `actions`, `tags`, `details`, `entity`, `source`, `priority`, `user`, and `note` are supported. See the [Opsgenie API documentation](https://docs.opsgenie.com/docs/alert-api#create-alert) for more information on their types.

`subActionParams (closeAlert)`

No parameters are required. For the definition of the optional parameters see the [Opsgenie API documentation](https://docs.opsgenie.com/docs/alert-api#close-alert).

---

# Developing New Connector Types

When creating a new connector type, your plugin will eventually call `server.plugins.actions.setup.registerType()` to register the type with the `actions` plugin, but there are some additional things to think about about and implement.
Expand Down Expand Up @@ -370,4 +396,4 @@ Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `sche

## User interface

To make this connector usable in the Kibana UI, you will need to provide all the UI editing aspects of the connector. The existing connector type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui).
To make this connector usable in the Kibana UI, you will need to provide all the UI editing aspects of the connector. The existing connector type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this was a whitespace change 🤔

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getSlackConnectorType,
getTeamsConnectorType,
getWebhookConnectorType,
getOpsgenieConnectorType,
getXmattersConnectorType,
} from './stack';
import {
Expand All @@ -26,7 +27,6 @@ import {
getServiceNowSIRConnectorType,
getSwimlaneConnectorType,
} from './cases';

export type {
EmailActionParams,
IndexActionParams,
Expand All @@ -46,6 +46,7 @@ export {
SlackConnectorTypeId,
TeamsConnectorTypeId,
WebhookConnectorTypeId,
OpsgenieConnectorTypeId,
XmattersConnectorTypeId,
} from './stack';
export type {
Expand Down Expand Up @@ -86,4 +87,5 @@ export function registerConnectorTypes({
actions.registerType(getJiraConnectorType({ logger }));
actions.registerType(getResilientConnectorType({ logger }));
actions.registerType(getTeamsConnectorType({ logger }));
actions.registerSubActionConnectorType(getOpsgenieConnectorType());
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export {
} from './webhook';
export type { ActionParamsType as WebhookActionParams } from './webhook';

export { getOpsgenieConnectorType, OpsgenieConnectorTypeId } from './opsgenie';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add the OpsgenieActionParams in the next UI PR when it's actually needed externally


export {
getConnectorType as getXmattersConnectorType,
ConnectorTypeId as XmattersConnectorTypeId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import axios, { AxiosInstance } from 'axios';
import crypto from 'crypto';
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { MockedLogger } from '@kbn/logging-mocks';
import { OpsgenieConnectorTypeId } from '.';
import { OpsgenieConnector } from './connector';
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';

jest.mock('axios');

jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
return {
...originalUtils,
request: jest.fn(),
};
});

const axiosMock = axios as jest.Mocked<typeof axios>;
const requestMock = utils.request as jest.Mock;

describe('OpsgenieConnector', () => {
const axiosInstanceMock = jest.fn();

let connector: OpsgenieConnector;
let mockedActionsConfig: jest.Mocked<ActionsConfigurationUtilities>;
let logger: MockedLogger;
let services: ReturnType<typeof actionsMock.createServices>;

const defaultCreateAlertExpect = {
method: 'post',
url: 'https://example.com/v2/alerts',
headers: { Authorization: 'GenieKey 123', 'Content-Type': 'application/json' },
};

const createCloseAlertExpect = (alias: string) => ({
method: 'post',
url: `https://example.com/v2/alerts/${alias}/close?identifierType=alias`,
headers: { Authorization: 'GenieKey 123', 'Content-Type': 'application/json' },
});

const ignoredRequestFields = {
axios: expect.anything(),
configurationUtilities: expect.anything(),
logger: expect.anything(),
};

beforeEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
requestMock.mockReturnValue({ data: { took: 5, requestId: '123', result: 'ok' } });
axiosMock.create.mockImplementation(() => {
return axiosInstanceMock as unknown as AxiosInstance;
});

logger = loggingSystemMock.createLogger();
services = actionsMock.createServices();
mockedActionsConfig = actionsConfigMock.create();

connector = new OpsgenieConnector({
configurationUtilities: mockedActionsConfig,
config: { apiUrl: 'https://example.com' },
connector: { id: '1', type: OpsgenieConnectorTypeId },
secrets: { apiKey: '123' },
logger,
services,
});
});

it('calls request with the correct arguments for creating an alert', async () => {
await connector.createAlert({ message: 'hello' });

expect(requestMock.mock.calls[0][0]).toEqual({
data: { message: 'hello' },
...ignoredRequestFields,
...defaultCreateAlertExpect,
});
});

it('calls request without modifying the alias when it is less than 512 characters when creating an alert', async () => {
await connector.createAlert({ message: 'hello', alias: '111' });

expect(requestMock.mock.calls[0][0]).toEqual({
...ignoredRequestFields,
...defaultCreateAlertExpect,
data: { message: 'hello', alias: '111' },
});
});

it('calls request without modifying the alias when it is equal to 512 characters when creating an alert', async () => {
const alias = 'a'.repeat(512);
await connector.createAlert({ message: 'hello', alias });

expect(requestMock.mock.calls[0][0]).toEqual({
...ignoredRequestFields,
...defaultCreateAlertExpect,
data: { message: 'hello', alias },
});
});

it('calls request with the sha256 hash of the alias when it is greater than 512 characters when creating an alert', async () => {
const alias = 'a'.repeat(513);

const hasher = crypto.createHash('sha256');
const sha256Hash = hasher.update(alias);

await connector.createAlert({ message: 'hello', alias });

expect(requestMock.mock.calls[0][0]).toEqual({
...ignoredRequestFields,
...defaultCreateAlertExpect,
data: { message: 'hello', alias: sha256Hash.digest('hex') },
});
});

it('calls request with the correct arguments for closing an alert', async () => {
await connector.closeAlert({ user: 'sam', alias: '111' });

expect(requestMock.mock.calls[0][0]).toEqual({
...ignoredRequestFields,
...createCloseAlertExpect('111'),
data: { user: 'sam' },
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import crypto from 'crypto';
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
import { AxiosError } from 'axios';
import { CloseAlertParamsSchema, CreateAlertParamsSchema, Response } from './schema';
import { CloseAlertParams, Config, CreateAlertParams, Secrets } from './types';
import * as i18n from './translations';

interface ErrorSchema {
message?: string;
errors?: {
message?: string;
};
}

export class OpsgenieConnector extends SubActionConnector<Config, Secrets> {
constructor(params: ServiceParams<Config, Secrets>) {
super(params);

this.registerSubAction({
method: this.createAlert.name,
name: 'createAlert',
schema: CreateAlertParamsSchema,
});

this.registerSubAction({
method: this.closeAlert.name,
name: 'closeAlert',
schema: CloseAlertParamsSchema,
});
}

public getResponseErrorMessage(error: AxiosError<ErrorSchema>) {
return `Message: ${
error.response?.data.errors?.message ??
error.response?.data.message ??
error.message ??
i18n.UNKNOWN_ERROR
}`;
}

public async createAlert(params: CreateAlertParams) {
const res = await this.request({
method: 'post',
url: this.concatPathToURL('v2/alerts').toString(),
data: { ...params, ...OpsgenieConnector.createAliasObj(params.alias) },
headers: this.createHeaders(),
responseSchema: Response,
});

return res.data;
}

private static createAliasObj(alias?: string) {
if (!alias) {
return {};
}

// opsgenie v2 requires that the alias length be no more than 512 characters
// see their docs for more details https://docs.opsgenie.com/docs/alert-api#create-alert
if (alias.length <= 512) {
return { alias };
}

// To give preference to avoiding collisions we're using sha256 over of md5 but we are compromising on speed a bit here
const hasher = crypto.createHash('sha256');
const sha256Hash = hasher.update(alias);

return { alias: sha256Hash.digest('hex') };
}

private createHeaders() {
return { Authorization: `GenieKey ${this.secrets.apiKey}` };
}

public async closeAlert(params: CloseAlertParams) {
const fullURL = this.concatPathToURL(`v2/alerts/${params.alias}/close`);
fullURL.searchParams.set('identifierType', 'alias');

const { alias, ...paramsWithoutAlias } = params;

const res = await this.request({
method: 'post',
url: fullURL.toString(),
data: paramsWithoutAlias,
headers: this.createHeaders(),
responseSchema: Response,
});

return res.data;
}

private concatPathToURL(path: string) {
const fullURL = new URL(path, this.config.apiUrl);

return fullURL;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
AlertingConnectorFeatureId,
SecurityConnectorFeatureId,
UptimeConnectorFeatureId,
} from '@kbn/actions-plugin/common';
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
import {
SubActionConnectorType,
ValidatorType,
} from '@kbn/actions-plugin/server/sub_action_framework/types';
import { OpsgenieConnector } from './connector';
import { ConfigSchema, SecretsSchema } from './schema';
import { Config, Secrets } from './types';
import * as i18n from './translations';

export const OpsgenieConnectorTypeId = '.opsgenie';

export const getOpsgenieConnectorType = (): SubActionConnectorType<Config, Secrets> => {
return {
Service: OpsgenieConnector,
minimumLicenseRequired: 'platinum',
name: i18n.OPSGENIE_NAME,
id: OpsgenieConnectorTypeId,
schema: { config: ConfigSchema, secrets: SecretsSchema },
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('apiUrl') }],
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
};
};
Loading