Skip to content

Commit

Permalink
Merge pull request backstage#3317 from backstage/samiram/pagerduty-pl…
Browse files Browse the repository at this point in the history
…ugin

Pagerduty plugin
  • Loading branch information
samiramkr authored Dec 4, 2020
2 parents 2d193da + 7c856e3 commit f3b24a9
Show file tree
Hide file tree
Showing 41 changed files with 2,063 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/great-spiders-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog': patch
'@backstage/plugin-pagerduty': patch
---

Added pagerduty plugin to example app
8 changes: 8 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ proxy:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY

'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization:
$env: PAGERDUTY_TOKEN

'/buildkite/api':
target: https://api.buildkite.com/v2/
headers:
Expand Down Expand Up @@ -310,3 +316,5 @@ homepage:
timezone: 'Europe/Stockholm'
- label: TYO
timezone: 'Asia/Tokyo'
pagerduty:
eventsBaseUrl: 'https://events.pagerduty.com/v2'
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@backstage/plugin-kubernetes": "^0.3.1",
"@backstage/plugin-lighthouse": "^0.2.4",
"@backstage/plugin-newrelic": "^0.2.1",
"@backstage/plugin-pagerduty": "0.2.1",
"@backstage/plugin-register-component": "^0.2.3",
"@backstage/plugin-rollbar": "^0.2.5",
"@backstage/plugin-scaffolder": "^0.3.2",
Expand All @@ -36,10 +37,10 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"@roadiehq/backstage-plugin-github-insights": "^0.2.16",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.3",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.8",
"@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/components/catalog/EntityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ import {
PullRequestsStatsCard,
Router as PullRequestsRouter,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
Expand Down Expand Up @@ -142,6 +146,11 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
{isPagerDutyAvailable(entity) && (
<Grid item md={6}>
<PagerDutyCard entity={entity} />
</Grid>
)}
<RecentCICDRunsSwitcher entity={entity} />
{isGitHubAvailable(entity) && (
<>
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
1 change: 1 addition & 0 deletions plugins/catalog/src/components/AboutCard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
*/

export { AboutCard } from './AboutCard';
export { IconLinkVertical } from './IconLinkVertical';
2 changes: 1 addition & 1 deletion plugins/catalog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

export * from '@backstage/catalog-client';
export { AboutCard } from './components/AboutCard';
export { AboutCard, IconLinkVertical } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
export { Router } from './components/Router';
export { useEntityCompoundName } from './components/useEntityCompoundName';
Expand Down
3 changes: 3 additions & 0 deletions plugins/pagerduty/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
82 changes: 82 additions & 0 deletions plugins/pagerduty/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# PagerDuty

## Overview

This plugin displays PagerDuty information about an entity such as if there are any active incidents and what the escalation policy is.

There is also an easy way to trigger an alarm directly to the person who is currently on-call.

This plugin requires that entities are annotated with an [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service). See more further down in this document.

This plugin provides:

- A list of incidents
- A way to trigger an alarm to the person on-call
- Information details about the person on-call

## Setup instructions

Install the plugin:

```bash
yarn add @backstage/plugin-pagerduty
```

Add it to the app in `plugins.ts`:

```ts
export { plugin as Pagerduty } from '@backstage/plugin-pagerduty';
```

Add it to the `EntityPage.ts`:

```ts
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
// add to code
{
isPagerDutyAvailable(entity) && (
<Grid item md={6}>
<PagerDutyCard entity={entity} />
</Grid>
);
}
```

## Client configuration

If you want to override the default URL for events, you can add it to `app-config.yaml`.

In `app-config.yaml`:

```yaml
pagerduty:
eventsBaseUrl: 'https://events.pagerduty.com/v2'
```
## Providing the API Token
In order for the client to make requests to the [PagerDuty API](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/) it needs an [API Token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-general-access-rest-api-key).
Then start the backend passing the token as an environment variable:
```bash
$ PAGERDUTY_TOKEN='Token token=<TOKEN>' yarn start
```

This will proxy the request by adding `Authorization` header with the provided token.

## Integration Key

The information displayed for each entity is based on the [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service).

### Adding the integration key to the entity annotation

If you want to use this plugin for an entity, you need to label it with the below annotation:

```yml
annotations:
pagerduty.com/integration-key: [INTEGRATION_KEY]
```
19 changes: 19 additions & 0 deletions plugins/pagerduty/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';

createDevApp().registerPlugin(plugin).render();
52 changes: 52 additions & 0 deletions plugins/pagerduty/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@backstage/plugin-pagerduty",
"version": "0.2.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.4.0",
"@backstage/core": "^0.3.2",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"classnames": "^2.2.6",
"date-fns": "^2.15.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.0",
"@backstage/dev-utils": "^0.1.5",
"@backstage/test-utils": "^0.1.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"msw": "^0.21.2",
"node-fetch": "^2.6.1",
"cross-fetch": "^3.0.6"
},
"files": [
"dist"
]
}
143 changes: 143 additions & 0 deletions plugins/pagerduty/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { createApiRef, DiscoveryApi, ConfigApi } from '@backstage/core';
import { Service, Incident, OnCall } from '../components/types';
import {
PagerDutyApi,
TriggerAlarmRequest,
ServicesResponse,
IncidentsResponse,
OnCallsResponse,
ClientApiConfig,
RequestOptions,
} from './types';

export class UnauthorizedError extends Error {}

export const pagerDutyApiRef = createApiRef<PagerDutyApi>({
id: 'plugin.pagerduty.api',
description: 'Used to fetch data from PagerDuty API',
});

export class PagerDutyClient implements PagerDutyApi {
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
const eventsBaseUrl: string =
configApi.getOptionalString('pagerDuty.eventsBaseUrl') ??
'https://events.pagerduty.com/v2';
return new PagerDutyClient({
eventsBaseUrl,
discoveryApi,
});
}
constructor(private readonly config: ClientApiConfig) {}

async getServiceByIntegrationKey(integrationKey: string): Promise<Service[]> {
const params = `include[]=integrations&include[]=escalation_policies&query=${integrationKey}`;
const url = `${await this.config.discoveryApi.getBaseUrl(
'proxy',
)}/pagerduty/services?${params}`;
const { services } = await this.getByUrl<ServicesResponse>(url);

return services;
}

async getIncidentsByServiceId(serviceId: string): Promise<Incident[]> {
const params = `statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`;
const url = `${await this.config.discoveryApi.getBaseUrl(
'proxy',
)}/pagerduty/incidents?${params}`;
const { incidents } = await this.getByUrl<IncidentsResponse>(url);

return incidents;
}

async getOnCallByPolicyId(policyId: string): Promise<OnCall[]> {
const params = `include[]=users&escalation_policy_ids[]=${policyId}`;
const url = `${await this.config.discoveryApi.getBaseUrl(
'proxy',
)}/pagerduty/oncalls?${params}`;
const { oncalls } = await this.getByUrl<OnCallsResponse>(url);

return oncalls;
}

triggerAlarm({
integrationKey,
source,
description,
userName,
}: TriggerAlarmRequest): Promise<Response> {
const body = JSON.stringify({
event_action: 'trigger',
routing_key: integrationKey,
client: 'Backstage',
client_url: source,
payload: {
summary: description,
source: source,
severity: 'error',
class: 'manual trigger',
custom_details: {
user: userName,
},
},
});

const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
Accept: 'application/json, text/plain, */*',
},
body,
};

const url = this.config.eventsBaseUrl ?? 'https://events.pagerduty.com/v2';

return this.request(`${url}/enqueue`, options);
}

private async getByUrl<T>(url: string): Promise<T> {
const options = {
method: 'GET',
headers: {
Accept: 'application/vnd.pagerduty+json;version=2',
'Content-Type': 'application/json',
},
};
const response = await this.request(url, options);

return response.json();
}

private async request(
url: string,
options: RequestOptions,
): Promise<Response> {
const response = await fetch(url, options);
if (response.status === 401) {
throw new UnauthorizedError();
}
if (!response.ok) {
const payload = await response.json();
const errors = payload.errors.map((error: string) => error).join(' ');
const message = `Request failed with ${response.status}, ${errors}`;
throw new Error(message);
}
return response;
}
}
Loading

0 comments on commit f3b24a9

Please sign in to comment.