Merge remote-tracking branch 'upstream/master' into airbrake-backend

# Conflicts:
#	ADOPTERS.md
This commit is contained in:
Karan Shah
2022-02-09 14:36:08 +00:00
12 changed files with 308 additions and 65 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-splunk-on-call': patch
---
Add Splunk On-Call plugin support for a `splunk.com/on-call-routing-key` annotation. If the `splunk.com/on-call-routing-key` is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key.
+2 -2
View File
@@ -62,7 +62,7 @@ jobs:
workflow_id: 'release.yml',
ref: 'master',
inputs: {
version: require('./package.json').version,
version: require('./backstage/package.json').version,
},
});
@@ -73,6 +73,6 @@ jobs:
workflow_id: 'release.yml',
ref: 'master',
inputs: {
version: require('./packages/create-app/package.json').version,
version: require('./backstage/packages/create-app/package.json').version,
},
});
+1 -1
View File
@@ -71,7 +71,7 @@
| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! |
| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. |
| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 |
| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. |
| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. |
| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates<br/> Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc |
| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. |
| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 |
+18 -8
View File
@@ -2,16 +2,16 @@
## Overview
This plugin displays Splunk On-Call, formerly VictorOps, information about an entity.
This plugin displays Splunk On-Call (formerly VictorOps) information associated with an entity.
There is a way to trigger an new incident directly to specific users or/and specific teams.
It also provides the ability to trigger new incidents to specific users and/or specific teams from within Backstage.
This plugin requires that entities are annotated with a team name. See more further down in this document.
This plugin requires that entities feature either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. See below for further details.
This plugin provides:
- A list of incidents
- A way to trigger a new incident to specific users or/and teams
- A way to trigger a new incident to specific users and/or teams
- A way to acknowledge/resolve an incident
- Information details about the persons on-call
@@ -47,7 +47,7 @@ const overviewContent = (
## Client configuration
In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
In order to be able to perform certain actions (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST Generic.
You can now copy the URL to notify: `<SPLUNK_ON_CALL_REST_ENDPOINT>/$routing_key`
@@ -76,12 +76,22 @@ In addition, to make certain API calls (trigger-resolve-acknowledge an incident)
### Adding your team name to the entity annotation
The information displayed for each entity is based on the team name.
If you want to use this plugin for an entity, you need to label it with the below annotation:
The information displayed for each entity is based on either an associated team name or an associated routing key.
To use this plugin for an entity, the entity must be labeled with either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation.
For example, by specifying a `splunk.com/on-call-team`, the plugin displays Splunk On-Call data associated with the specified team:
```yaml
annotations:
splunk.com/on-call-team': <SPLUNK_ON_CALL_TEAM_NAME>
splunk.com/on-call-team: <SPLUNK_ON_CALL_TEAM_NAME>
```
Alternatively, by specifying a `splunk.com/on-call-routing-key`, the plugin displays Splunk On-Call data associated with _each_ of the teams associated with the specified routing key:
```yaml
annotations:
splunk.com/on-call-routing-key: <SPLUNK_ON_CALL_ROUTING_KEY>
```
### Create the Routing Key
+4
View File
@@ -51,6 +51,10 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
//
// (undocumented)
getOnCallUsers(): Promise<OnCall[]>;
// Warning: (ae-forgotten-export) The symbol "RoutingKey" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getRoutingKeys(): Promise<RoutingKey[]>;
// Warning: (ae-forgotten-export) The symbol "Team" needs to be exported by the entry point index.d.ts
//
// (undocumented)
+11
View File
@@ -19,6 +19,7 @@ import {
OnCall,
User,
EscalationPolicyInfo,
RoutingKey,
Team,
} from '../components/types';
import {
@@ -30,6 +31,7 @@ import {
RequestOptions,
ListUserResponse,
EscalationPolicyResponse,
ListRoutingKeyResponse,
} from './types';
import {
createApiRef,
@@ -82,6 +84,15 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
return teams;
}
async getRoutingKeys(): Promise<RoutingKey[]> {
const url = `${await this.config.discoveryApi.getBaseUrl(
'proxy',
)}/splunk-on-call/v1/org/routing-keys`;
const { routingKeys } = await this.getByUrl<ListRoutingKeyResponse>(url);
return routingKeys;
}
async getUsers(): Promise<User[]> {
const url = `${await this.config.discoveryApi.getBaseUrl(
'proxy',
+13
View File
@@ -17,6 +17,7 @@
import {
EscalationPolicyInfo,
Incident,
RoutingKey,
Team,
User,
} from '../components/types';
@@ -88,6 +89,18 @@ export const MOCK_TEAM: Team = {
isDefaultTeam: false,
};
export const MOCK_ROUTING_KEY: RoutingKey = {
routingKey: 'test-routing-key',
targets: [
{
policyName: 'some policy',
policySlug: MOCK_TEAM.slug || '',
_teamUrl: `/api-public/v1/team/${MOCK_TEAM.slug}`,
},
],
isDefault: false,
};
export const MOCK_TEAM_NO_INCIDENTS: Team = {
...MOCK_TEAM,
name: 'test-noincidents',
+11
View File
@@ -18,6 +18,7 @@ import {
EscalationPolicyInfo,
Incident,
OnCall,
RoutingKey,
Team,
User,
} from '../components/types';
@@ -65,6 +66,11 @@ export interface SplunkOnCallApi {
*/
getTeams(): Promise<Team[]>;
/**
* Get a list of routing keys for your organization.
*/
getRoutingKeys(): Promise<RoutingKey[]>;
/**
* Get a list of escalation policies for your organization.
*/
@@ -80,6 +86,11 @@ export type ListUserResponse = {
_selfUrl?: string;
};
export type ListRoutingKeyResponse = {
routingKeys: RoutingKey[];
_selfUrl?: string;
};
export type IncidentsResponse = {
incidents: Incident[];
};
@@ -28,6 +28,7 @@ import {
MOCKED_ON_CALL,
MOCKED_USER,
MOCK_INCIDENT,
MOCK_ROUTING_KEY,
MOCK_TEAM,
MOCK_TEAM_NO_INCIDENTS,
} from '../api/mocks';
@@ -45,6 +46,7 @@ const mockSplunkOnCallApi: Partial<SplunkOnCallClient> = {
getIncidents: async () => [MOCK_INCIDENT],
getOnCallUsers: async () => MOCKED_ON_CALL,
getTeams: async () => [MOCK_TEAM],
getRoutingKeys: async () => [MOCK_ROUTING_KEY],
getEscalationPolicies: async () => ESCALATION_POLICIES,
};
@@ -71,6 +73,26 @@ const mockEntity = {
},
} as Entity;
const mockEntityWithRoutingKeyAnnotation = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'splunkoncall-test',
annotations: {
'splunk.com/on-call-routing-key': MOCK_ROUTING_KEY.routingKey,
},
},
} as Entity;
const mockEntityNoAnnotation = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'splunkoncall-test',
annotations: {},
},
} as Entity;
const mockEntityNoIncidents = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -109,6 +131,34 @@ describe('SplunkOnCallCard', () => {
expect(getByText('Empty escalation policy')).toBeInTheDocument();
});
it('handles a "splunk.com/on-call-routing-key" annotation', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
.mockImplementationOnce(async () => [MOCKED_USER]);
mockSplunkOnCallApi.getRoutingKeys = jest
.fn()
.mockImplementationOnce(async () => [MOCK_ROUTING_KEY]);
mockSplunkOnCallApi.getTeams = jest
.fn()
.mockImplementation(async () => [MOCK_TEAM]);
const { getByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntityWithRoutingKeyAnnotation}>
<EntitySplunkOnCallCard />
</EntityProvider>
</ApiProvider>,
),
);
await waitFor(() => !queryByTestId('progress'));
expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument();
await waitFor(
() => expect(getByText('test-incident')).toBeInTheDocument(),
{ timeout: 2000 },
);
});
it('Handles custom error for missing token', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
@@ -151,6 +201,20 @@ describe('SplunkOnCallCard', () => {
).toBeInTheDocument();
});
it('handles warning for missing required annotations', async () => {
const { getAllByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntityNoAnnotation}>
<EntitySplunkOnCallCard />
</EntityProvider>
</ApiProvider>,
),
);
await waitFor(() => !queryByTestId('progress'));
expect(getAllByText('Missing Annotation').length).toEqual(1);
});
it('handles warning for incorrect team annotation', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
@@ -170,7 +234,37 @@ describe('SplunkOnCallCard', () => {
);
await waitFor(() => !queryByTestId('progress'));
expect(
getByText('Could not find team named "test" in the Splunk On-Call API'),
getByText(
'Splunk On-Call API returned no record of teams associated with the "test" team name',
),
).toBeInTheDocument();
});
it('handles warning for incorrect routing key annotation', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
.mockImplementationOnce(async () => [MOCKED_USER]);
mockSplunkOnCallApi.getRoutingKeys = jest
.fn()
.mockImplementationOnce(async () => [MOCK_ROUTING_KEY]);
mockSplunkOnCallApi.getTeams = jest
.fn()
.mockImplementationOnce(async () => []);
const { getByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntityWithRoutingKeyAnnotation}>
<EntitySplunkOnCallCard />
</EntityProvider>
</ApiProvider>,
),
);
await waitFor(() => !queryByTestId('progress'));
expect(
getByText(
`Splunk On-Call API returned no record of teams associated with the "${MOCK_ROUTING_KEY.routingKey}" routing key`,
),
).toBeInTheDocument();
});
@@ -22,6 +22,7 @@ import {
CardContent,
CardHeader,
Divider,
makeStyles,
Typography,
} from '@material-ui/core';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
@@ -32,8 +33,7 @@ import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError';
import { EscalationPolicy } from './Escalation';
import { Incidents } from './Incident';
import { TriggerDialog } from './TriggerDialog';
import { User } from './types';
import { Team, User } from './types';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import {
@@ -45,20 +45,49 @@ import {
} from '@backstage/core-components';
export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team';
export const SPLUNK_ON_CALL_ROUTING_KEY = 'splunk.com/on-call-routing-key';
export const MissingTeamAnnotation = () => (
<MissingAnnotationEmptyState annotation={SPLUNK_ON_CALL_TEAM} />
export const MissingAnnotation = () => (
<div>
<Typography>
The Splunk On Call plugin requires setting either the{' '}
<code>{SPLUNK_ON_CALL_TEAM}</code> or the{' '}
<code>{SPLUNK_ON_CALL_ROUTING_KEY}</code> annotation.
</Typography>
<MissingAnnotationEmptyState annotation={SPLUNK_ON_CALL_TEAM} />
</div>
);
export const InvalidTeamAnnotation = ({ teamName }: { teamName: string }) => (
<CardContent>
<EmptyState
title={`Could not find team named "${teamName}" in the Splunk On-Call API`}
missing="info"
description={`Escalation Policy and incident information unavailable. Please verify that the team you added "${teamName}" is valid if you want to enable Splunk On-Call.`}
/>
</CardContent>
);
export const InvalidAnnotation = ({
teamName,
routingKey,
}: {
teamName: string | undefined;
routingKey: string | undefined;
}) => {
let titleSuffix = 'provided annotation';
if (routingKey) {
titleSuffix = `"${routingKey}" routing key`;
}
if (teamName) {
titleSuffix = `"${teamName}" team name`;
}
return (
<Card>
<CardHeader title="Splunk On-Call" />
<CardContent>
<EmptyState
title={`Splunk On-Call API returned no record of teams associated with the ${titleSuffix}`}
missing="info"
description="Escalation Policy and incident information unavailable. Splunk On-Call requires a valid team name or routing key."
/>
</CardContent>
</Card>
);
};
export const MissingEventsRestEndpoint = () => (
<CardContent>
@@ -71,15 +100,28 @@ export const MissingEventsRestEndpoint = () => (
);
export const isSplunkOnCallAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]);
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) ||
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_ROUTING_KEY]);
const useStyles = makeStyles({
onCallCard: {
marginBottom: '1em',
},
});
export const EntitySplunkOnCallCard = () => {
const classes = useStyles();
const config = useApi(configApiRef);
const api = useApi(splunkOnCallApiRef);
const { entity } = useEntity();
const [showDialog, setShowDialog] = useState<boolean>(false);
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM];
const teamAnnotation = entity
? entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]
: undefined;
const routingKeyAnnotation = entity
? entity.metadata.annotations![SPLUNK_ON_CALL_ROUTING_KEY]
: undefined;
const eventsRestEndpoint =
config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null;
@@ -93,7 +135,7 @@ export const EntitySplunkOnCallCard = () => {
}, []);
const {
value: usersAndTeam,
value: usersAndTeams,
loading,
error,
} = useAsync(async () => {
@@ -108,10 +150,38 @@ export const EntitySplunkOnCallCard = () => {
{},
);
const teams = await api.getTeams();
const foundTeam = teams.find(teamValue => teamValue.name === team);
return { usersHashMap, foundTeam };
let foundTeams = [
teams.find(teamValue => teamValue.name === teamAnnotation),
].filter(team => team !== undefined);
if (!foundTeams.length && routingKeyAnnotation) {
const routingKeys = await api.getRoutingKeys();
const foundRoutingKey = routingKeys.find(
key => key.routingKey === routingKeyAnnotation,
);
foundTeams = foundRoutingKey
? foundRoutingKey.targets
.map(target => {
const teamUrlParts = target._teamUrl.split('/');
const teamSlug = teamUrlParts[teamUrlParts.length - 1];
return teams.find(teamValue => teamValue.slug === teamSlug);
})
.filter(team => team !== undefined)
: [];
}
return { usersHashMap, foundTeams };
});
if (!teamAnnotation && !routingKeyAnnotation) {
return <MissingAnnotation />;
}
if (!eventsRestEndpoint) {
return <MissingEventsRestEndpoint />;
}
if (error instanceof UnauthorizedError) {
return <MissingApiKeyOrApiIdError />;
}
@@ -128,27 +198,32 @@ export const EntitySplunkOnCallCard = () => {
return <Progress />;
}
const Content = () => {
if (!team) {
return <MissingTeamAnnotation />;
}
if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) {
return (
<InvalidAnnotation
teamName={teamAnnotation}
routingKey={routingKeyAnnotation}
/>
);
}
if (!usersAndTeam?.foundTeam) {
return <InvalidTeamAnnotation teamName={team} />;
}
if (!eventsRestEndpoint) {
return <MissingEventsRestEndpoint />;
}
const Content = ({
team,
usersHashMap,
}: {
team: Team | undefined;
usersHashMap: any;
}) => {
const teamName = team?.name ?? '';
return (
<>
<Incidents team={team} refreshIncidents={refreshIncidents} />
{usersAndTeam?.usersHashMap && team && (
<EscalationPolicy team={team} users={usersAndTeam.usersHashMap} />
<Incidents team={teamName} refreshIncidents={refreshIncidents} />
{usersHashMap && team && (
<EscalationPolicy team={teamName} users={usersHashMap} />
)}
<TriggerDialog
team={team}
team={teamName}
showDialog={showDialog}
handleDialog={handleDialog}
onIncidentCreated={handleRefresh}
@@ -170,22 +245,30 @@ export const EntitySplunkOnCallCard = () => {
icon: <WebIcon />,
};
const teams = usersAndTeams?.foundTeams || [];
return (
<Card>
<CardHeader
title="Splunk On-Call"
subheader={[
<Typography key="team_name">Team: {team}</Typography>,
<HeaderIconLinkRow
key="incident_trigger"
links={[serviceLink, triggerLink]}
/>,
]}
/>
<Divider />
<CardContent>
<Content />
</CardContent>
</Card>
<>
{teams.map((team, i) => (
<Card key={i} className={classes.onCallCard}>
<CardHeader
title="Splunk On-Call"
subheader={[
<Typography key="team_name">
Team: {team && team.name ? team.name : ''}
</Typography>,
<HeaderIconLinkRow
key="incident_trigger"
links={[serviceLink, triggerLink]}
/>,
]}
/>
<Divider />
<CardContent>
<Content team={team} usersHashMap={usersAndTeams?.usersHashMap} />
</CardContent>
</Card>
))}
</>
);
};
@@ -117,3 +117,15 @@ export type EscalationPolicyTeam = {
name: string;
slug: string;
};
export type RoutingKey = {
routingKey: string;
targets: RoutingKeyTarget[];
isDefault: boolean;
};
export type RoutingKeyTarget = {
policyName: string;
policySlug: string;
_teamUrl: string;
};
+4 -4
View File
@@ -4653,13 +4653,13 @@
react-use "^17.2.4"
"@roadiehq/backstage-plugin-github-pull-requests@^1.3.2":
version "1.3.4"
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.3.4.tgz#3e6be41fe81909b28a3d517aadcc7ae6b70d7d7a"
integrity sha512-F1y3CmMZOiNiam3zt6TBaJ5+NqAhPwS3HkWbUsMvJq1PKAzYdSeDBPhoTBwhYOu3uV36EtKjfog4r4wVHOf7EA==
version "1.3.7"
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.3.7.tgz#d3c884c9b85f88c5f851f383596a616598c56076"
integrity sha512-a3PBLQ6SRtEDGNIekcGT8v4Aaac3BoEww/UU9uaQuE7bCEVolx2MeOVbSxb1uCWr6C6uGeDF5Kw4S4hPJJcnNg==
dependencies:
"@backstage/catalog-model" "^0.9.7"
"@backstage/core-components" "^0.8.0"
"@backstage/core-plugin-api" "^0.4.0"
"@backstage/core-plugin-api" "^0.6.0"
"@backstage/plugin-catalog-react" "^0.6.5"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"