Skip to content

Commit

Permalink
release: 0.14.0 (#120)
Browse files Browse the repository at this point in the history
### Description

**Release 0.14.0** introduces support for multi-account configuration.
With this release, admins will be able to specify multiple PagerDuty
accounts in the same Backstage instance. Support was added across all
the components of the plugin:

- **Backend:** All API routes exposed take account as an optional
parameter so you can create and query information from different
PagerDuty Accounts. If admins leverage the Entity Mapping feature, the
account is now persisted into the plugin database.
- **Scaffolder Actions:** When the scaffolder action is used on a
template it now lists Escalation Policies from all accounts configured
so the user can easily select the one they want. The Scaffolder action
also outputs the account so it can be replaced in the `app-config.yaml`
annotations.
- **Entity Processor:** If an entity mapping stored in the plugin
database contains the `account` property, the processor will add an
annotation to the entity with its information.

This release solves an existing problem for many large organizations
that have several PagerDuty accounts for segregation purposes or that
result from company acquisitions.

## Acknowledgement

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

**Disclaimer:** We value your time and bandwidth. As such, any pull
requests created on non-triaged issues might not be successful.
  • Loading branch information
t1agob authored Jul 12, 2024
2 parents 224b966 + 03ce02b commit daec2a0
Show file tree
Hide file tree
Showing 16 changed files with 138 additions and 40 deletions.
23 changes: 19 additions & 4 deletions config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { PagerDutyOAuthConfig } from "@pagerduty/backstage-plugin-common";
import { PagerDutyAccountConfig, PagerDutyOAuthConfig } from '@pagerduty/backstage-plugin-common';

export interface Config {
/**
Expand All @@ -28,9 +28,24 @@ export interface Config {
*/
eventsBaseUrl?: string;
/**
* Optional API Base URL to override the default.
* @visibility frontend
*/
* Optional API Base URL to override the default.
* @visibility frontend
*/
apiBaseUrl?: string;
/**
* Optional PagerDuty API Token used in API calls from the backend component.
* @visibility secret
*/
apiToken?: string;
/**
* Optional PagerDuty Scoped OAuth Token used in API calls from the backend component.
* @deepVisibility secret
*/
oauth?: PagerDutyOAuthConfig;
/**
* Optional PagerDuty multi-account configuration
* @deepVisibility secret
*/
accounts?: PagerDutyAccountConfig[];
};
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@mui/icons-material": "^5.15.19",
"@mui/material": "^5.15.19",
"@mui/x-date-pickers": "^7.6.1",
"@pagerduty/backstage-plugin-common": "0.1.5",
"@pagerduty/backstage-plugin-common": "0.2.0",
"@tanstack/react-query": "^5.40.1",
"classnames": "^2.2.6",
"luxon": "^3.4.1",
Expand Down
56 changes: 47 additions & 9 deletions src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class PagerDutyClient implements PagerDutyApi {
async getServiceByPagerDutyEntity(
pagerDutyEntity: PagerDutyEntity,
): Promise<PagerDutyServiceResponse> {
const { integrationKey, serviceId } = pagerDutyEntity;
const { integrationKey, serviceId, account } = pagerDutyEntity;

let response: PagerDutyServiceResponse;
let url: string;
Expand All @@ -80,6 +80,10 @@ export class PagerDutyClient implements PagerDutyApi {
url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/services?integration_key=${integrationKey}`;

if(account) {
url = `${url}&account=${account}`;
}
const serviceResponse = await this.findByUrl<PagerDutyServiceResponse>(url);

if (serviceResponse.service === undefined) throw new NotFoundError();
Expand All @@ -90,6 +94,10 @@ export class PagerDutyClient implements PagerDutyApi {
'pagerduty',
)}/services/${serviceId}`;

if (account) {
url = `${url}?account=${account}`;
}

response = await this.findByUrl<PagerDutyServiceResponse>(url);
} else {
throw new NotFoundError();
Expand All @@ -106,12 +114,13 @@ export class PagerDutyClient implements PagerDutyApi {
return await this.findByUrl<PagerDutyEntityMappingsResponse>(url);
}

async storeServiceMapping(serviceId: string, integrationKey: string, backstageEntityRef: string): Promise<Response> {
async storeServiceMapping(serviceId: string, integrationKey: string, backstageEntityRef: string, account: string): Promise<Response> {

const body = JSON.stringify({
entityRef: backstageEntityRef,
serviceId: serviceId,
integrationKey: integrationKey,
account: account,
});

const options = {
Expand All @@ -134,62 +143,91 @@ export class PagerDutyClient implements PagerDutyApi {
return await this.getServiceByPagerDutyEntity(getPagerDutyEntity(entity));
}

async getServiceById(serviceId: string): Promise<PagerDutyServiceResponse> {
const url = `${await this.config.discoveryApi.getBaseUrl(
async getServiceById(serviceId: string, account?: string): Promise<PagerDutyServiceResponse> {
let url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/services/${serviceId}`;

if(account) {
url = url.concat(`?account=${account}`);
}

return await this.findByUrl<PagerDutyServiceResponse>(url);
}

async getIncidentsByServiceId(
serviceId: string,
account?: string,
): Promise<PagerDutyIncidentsResponse> {
const url = `${await this.config.discoveryApi.getBaseUrl(
let url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/services/${serviceId}/incidents`;

if(account) {
url = url.concat(`?account=${account}`);
}

return await this.findByUrl<PagerDutyIncidentsResponse>(url);
}

async getChangeEventsByServiceId(
serviceId: string,
account?: string,
): Promise<PagerDutyChangeEventsResponse> {
const url = `${await this.config.discoveryApi.getBaseUrl(
let url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/services/${serviceId}/change-events`;

if(account) {
url = url.concat(`?account=${account}`);
}

return await this.findByUrl<PagerDutyChangeEventsResponse>(url);
}

async getServiceStandardsByServiceId(
serviceId: string,
account?: string
): Promise<PagerDutyServiceStandardsResponse> {
const url = `${await this.config.discoveryApi.getBaseUrl(
let url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/services/${serviceId}/standards`;

if(account) {
url = url.concat(`?account=${account}`);
}

return await this.findByUrl<PagerDutyServiceStandardsResponse>(url);
}

async getServiceMetricsByServiceId(
serviceId: string,
account?: string
): Promise<PagerDutyServiceMetricsResponse> {
const url = `${await this.config.discoveryApi.getBaseUrl(
let url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/services/${serviceId}/metrics`;

if (account){
url = url.concat(`?account=${account}`);
}

return await this.findByUrl<PagerDutyServiceMetricsResponse>(url);
}

async getOnCallByPolicyId(
policyId: string,
account?: string,
): Promise<PagerDutyUser[]> {
const params = `escalation_policy_ids[]=${policyId}`;
const url = `${await this.config.discoveryApi.getBaseUrl(
let url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/oncall-users?${params}`;

if (account) {
url = url.concat(`&account=${account}`);
}

const response: PagerDutyOnCallUsersResponse = await this.findByUrl<PagerDutyOnCallUsersResponse>(url);
return response.users;
}
Expand Down
10 changes: 8 additions & 2 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type PagerDutyCardServiceResponse = {
policyLink: string;
policyName: string;
status?: string;
account?: string;
standards?: PagerDutyServiceStandards;
metrics?: PagerDutyServiceMetrics[];
}
Expand All @@ -61,7 +62,7 @@ export interface PagerDutyApi {
* Stores the service mapping in the database.
*
*/
storeServiceMapping(serviceId: string, integrationKey: string, entityRef: string): Promise<Response>;
storeServiceMapping(serviceId: string, integrationKey: string, entityRef: string, account: string): Promise<Response>;

/**
* Fetches the service for the provided pager duty Entity.
Expand All @@ -83,6 +84,7 @@ export interface PagerDutyApi {
*/
getServiceById(
serviceId: string,
account?: string,
): Promise<PagerDutyServiceResponse>;

/**
Expand All @@ -91,6 +93,7 @@ export interface PagerDutyApi {
*/
getIncidentsByServiceId(
serviceId: string,
account?: string,
): Promise<PagerDutyIncidentsResponse>;

/**
Expand All @@ -99,6 +102,7 @@ export interface PagerDutyApi {
*/
getChangeEventsByServiceId(
serviceId: string,
account?: string,
): Promise<PagerDutyChangeEventsResponse>;

/**
Expand All @@ -107,6 +111,7 @@ export interface PagerDutyApi {
*/
getServiceStandardsByServiceId(
serviceId: string,
account?: string,
): Promise<PagerDutyServiceStandardsResponse>;

/**
Expand All @@ -115,13 +120,14 @@ export interface PagerDutyApi {
*/
getServiceMetricsByServiceId(
serviceId: string,
account?: string,
): Promise<PagerDutyServiceMetricsResponse>;

/**
* Fetches the list of users in an escalation policy.
*
*/
getOnCallByPolicyId(policyId: string): Promise<PagerDutyUser[]>;
getOnCallByPolicyId(policyId: string, account?: string): Promise<PagerDutyUser[]>;

/**
* Triggers an incident to whoever is on-call.
Expand Down
5 changes: 3 additions & 2 deletions src/components/ChangeEvents/ChangeEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ import { Alert } from '@material-ui/lab';

type Props = {
serviceId: string;
account?: string;
refreshEvents: boolean;
};

export const ChangeEvents = ({ serviceId, refreshEvents }: Props) => {
export const ChangeEvents = ({ serviceId, account, refreshEvents }: Props) => {
const api = useApi(pagerDutyApiRef);

const [{ value: changeEvents, loading, error }, getChangeEvents] = useAsyncFn(
async () => {
const { change_events } = await api.getChangeEventsByServiceId(serviceId);
const { change_events } = await api.getChangeEventsByServiceId(serviceId, account);
return change_events;
},
);
Expand Down
15 changes: 12 additions & 3 deletions src/components/Escalation/Escalation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ describe("Escalation", () => {
expect(
getByText("No one is on-call. Update the escalation policy.")
).toBeInTheDocument();
expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith("456");
expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith(
"456",
undefined
); // undefined is the default value for the optional account parameter
});

it("Renders a forbidden state when change events is undefined", async () => {
Expand Down Expand Up @@ -109,7 +112,10 @@ describe("Escalation", () => {

expect(getByText("person1")).toBeInTheDocument();
expect(getByText("person1@example.com")).toBeInTheDocument();
expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith("abc");
expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith(
"abc",
undefined
); // undefined is the default value for the optional account parameter
});

it("Renders a user with profile picture", async () => {
Expand Down Expand Up @@ -147,7 +153,10 @@ describe("Escalation", () => {
"src",
"https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y"
);
expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith("abc");
expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith(
"abc",
undefined
); // undefined is the default value for the optional account parameter
});

it("Handles errors", async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Escalation/EscalationPolicy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Props = {
policyId: string;
policyUrl: string;
policyName: string;
account?: string;
};
const useStyles = makeStyles<BackstageTheme>(() =>
createStyles({
Expand All @@ -44,6 +45,7 @@ export const EscalationPolicy = ({
policyId,
policyUrl,
policyName,
account,
}: Props) => {
const api = useApi(pagerDutyApiRef);
const classes = useStyles();
Expand All @@ -53,7 +55,7 @@ export const EscalationPolicy = ({
loading,
error,
} = useAsync(async () => {
return await api.getOnCallByPolicyId(policyId);
return await api.getOnCallByPolicyId(policyId, account);
});

if (error) {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Incident/Incidents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ import { IncidentForbiddenState } from './IncidentForbiddenState';

type Props = {
serviceId: string;
account?: string;
refreshIncidents: boolean;
};

export const Incidents = ({ serviceId, refreshIncidents }: Props) => {
export const Incidents = ({ serviceId, account, refreshIncidents }: Props) => {
const api = useApi(pagerDutyApiRef);

const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
async () => {
const { incidents: foundIncidents } = await api.getIncidentsByServiceId(
serviceId,
account
);
return foundIncidents;
},
Expand Down
Loading

0 comments on commit daec2a0

Please sign in to comment.