Add the fossa plugin

This commit is contained in:
Dominik Henneke
2020-12-10 16:06:23 +01:00
parent ebbc133eaa
commit ab88f2a255
18 changed files with 897 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
/*
* 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 } from '@backstage/core';
export interface FindingSummary {
timestamp: string;
issueCount: number;
dependencyCount: number;
projectDefaultBranch: string;
projectUrl: string;
}
export const fossaApiRef = createApiRef<FossaApi>({
id: 'plugin.fossa.service',
description: 'Used by the Fossa plugin to make requests',
});
export type FossaApi = {
getFindingSummary(projectTitle?: string): Promise<FindingSummary | undefined>;
};
+138
View File
@@ -0,0 +1,138 @@
/*
* 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 { UrlPatternDiscovery } from '@backstage/core';
import { msw } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { FindingSummary, FossaApi, FossaClient } from './index';
const server = setupServer();
describe('FossaClient', () => {
msw.setupDefaultHandlers(server);
const mockBaseUrl = 'http://backstage:9191/api/proxy';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
let client: FossaApi;
beforeEach(() => {
client = new FossaClient({ discoveryApi, organizationId: '8736' });
});
it('should report finding summary', async () => {
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service&organizationId=8736',
);
return res(
ctx.json([
{
locator: 'custom+8736/our-service',
default_branch: 'develop',
revisions: [
{
updatedAt: '2020-01-01T00:00:00Z',
dependency_count: 160,
unresolved_licensing_issue_count: 5,
unresolved_issue_count: 100,
},
],
},
]),
);
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual({
timestamp: '2020-01-01T00:00:00Z',
issueCount: 5,
dependencyCount: 160,
projectDefaultBranch: 'develop',
projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service',
} as FindingSummary);
});
it('should report finding summary without licenseing_issue_count', async () => {
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service&organizationId=8736',
);
return res(
ctx.json([
{
locator: 'custom+8736/our-service',
default_branch: 'refs/master',
revisions: [
{
updatedAt: '2020-01-01T00:00:00Z',
dependency_count: 160,
unresolved_issue_count: 100,
},
],
},
]),
);
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual({
timestamp: '2020-01-01T00:00:00Z',
issueCount: 100,
dependencyCount: 160,
projectDefaultBranch: 'refs/master',
projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service',
} as FindingSummary);
});
it('should skip organizationId', async () => {
client = new FossaClient({ discoveryApi });
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service',
);
return res(ctx.status(404));
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toBeUndefined();
});
it('should handle 404 status', async () => {
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service&organizationId=8736',
);
return res(ctx.status(404));
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toBeUndefined();
});
});
+74
View File
@@ -0,0 +1,74 @@
/*
* 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 { DiscoveryApi } from '@backstage/core';
import fetch from 'cross-fetch';
import { FindingSummary, FossaApi } from './FossaApi';
export class FossaClient implements FossaApi {
discoveryApi: DiscoveryApi;
organizationId?: string;
constructor({
discoveryApi,
organizationId,
}: {
discoveryApi: DiscoveryApi;
organizationId?: string;
}) {
this.discoveryApi = discoveryApi;
this.organizationId = organizationId;
}
private async callApi(path: string): Promise<any> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`;
const response = await fetch(`${apiUrl}/${path}`);
if (response.status === 200) {
return await response.json();
}
return undefined;
}
async getFindingSummary(
projectTitle?: string,
): Promise<FindingSummary | undefined> {
if (!projectTitle) {
return undefined;
}
const project = await this.callApi(
`projects?count=1&title=${projectTitle}${
this.organizationId ? `&organizationId=${this.organizationId}` : ''
}`,
);
if (!project) {
return undefined;
}
const revision = project[0].revisions[0];
return {
timestamp: revision.updatedAt,
issueCount:
revision.unresolved_licensing_issue_count ||
revision.unresolved_issue_count,
dependencyCount: revision.dependency_count,
projectDefaultBranch: project[0].default_branch,
projectUrl: `https://app.fossa.com/projects/${encodeURIComponent(
project[0].locator,
)}`,
};
}
}
+19
View File
@@ -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.
*/
export type { FossaApi, FindingSummary } from './FossaApi';
export { fossaApiRef } from './FossaApi';
export { FossaClient } from './FossaClient';
@@ -0,0 +1,177 @@
/*
* 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 React from 'react';
import {
EmptyState,
InfoCard,
MissingAnnotationEmptyState,
Progress,
useApi,
} from '@backstage/core';
import { useAsync } from 'react-use';
import { Entity } from '@backstage/catalog-model';
import { fossaApiRef } from '../../api';
import { makeStyles } from '@material-ui/core/styles';
import {
FOSSA_PROJECT_NAME_ANNOTATION,
useProjectName,
} from '../useProjectName';
import { Grid, Tooltip } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
numberError: {
fontSize: '5rem',
textAlign: 'center',
fontWeight: theme.typography.fontWeightMedium,
margin: theme.spacing(2, 0),
color: theme.palette.error.main,
},
numberSuccess: {
fontSize: '5rem',
textAlign: 'center',
fontWeight: theme.typography.fontWeightMedium,
margin: theme.spacing(2, 0),
color: theme.palette.success.main,
},
description: {
fontSize: '1rem',
textAlign: 'center',
fontWeight: theme.typography.fontWeightMedium,
color: theme.palette.text.secondary,
},
disabled: {
backgroundColor: theme.palette.background.default,
},
lastAnalyzed: {
color: theme.palette.text.secondary,
textAlign: 'center',
},
branch: {
textDecoration: 'underline dotted',
},
}));
export const FossaCard = ({
entity,
variant = 'gridItem',
}: {
entity: Entity;
variant?: string;
}) => {
const fossaApi = useApi(fossaApiRef);
const projectTitle = useProjectName(entity);
const { value, loading } = useAsync(
async () => fossaApi.getFindingSummary(projectTitle),
[fossaApi, projectTitle],
);
const deepLink = value
? {
title: 'View more',
link: value.projectUrl,
}
: undefined;
const classes = useStyles();
return (
<>
<InfoCard
title="License Findings"
deepLink={deepLink}
variant={variant}
className={
!loading && (!projectTitle || !value) ? classes.disabled : undefined
}
>
{loading && <Progress />}
{!loading && !projectTitle && (
<MissingAnnotationEmptyState
annotation={FOSSA_PROJECT_NAME_ANNOTATION}
/>
)}
{!loading && projectTitle && !value && (
<EmptyState
missing="info"
title="No information to display"
description={`There is no Fossa project with title '${projectTitle}'.`}
/>
)}
{value && (
<Grid
item
container
direction="column"
justify="space-between"
alignItems="center"
style={{ height: '100%' }}
spacing={0}
>
<Grid item>
<p
className={
value.issueCount > 0 || value.dependencyCount === 0
? classes.numberError
: classes.numberSuccess
}
>
{value.issueCount}
</p>
{value.dependencyCount > 0 && (
<p className={classes.description}>Number of issues</p>
)}
{value.dependencyCount === 0 && (
<p className={classes.description}>
No Dependencies.
<br />
Please check your FOSSA project settings.
</p>
)}
</Grid>
<Grid item className={classes.lastAnalyzed}>
Last analyzed on{' '}
{new Date(value.timestamp).toLocaleString('en-US', {
timeZone: 'UTC',
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})}
</Grid>
<Grid item className={classes.lastAnalyzed}>
Based on {value.dependencyCount} Dependencies on branch{' '}
<Tooltip title="The default branch can be changed by a FOSSA admin.">
<span className={classes.branch}>
{value.projectDefaultBranch}
</span>
</Tooltip>
.
</Grid>
</Grid>
)}
</InfoCard>
</>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { FossaCard } from './FossaCard';
+17
View File
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './FossaCard';
@@ -0,0 +1,23 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
export const FOSSA_PROJECT_NAME_ANNOTATION = 'fossa.io/project-name';
export const useProjectName = (entity: Entity) => {
return entity?.metadata.annotations?.[FOSSA_PROJECT_NAME_ANNOTATION] ?? '';
};
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
export { plugin } from './plugin';
export * from './components';
+23
View File
@@ -0,0 +1,23 @@
/*
* 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 { plugin } from './plugin';
describe('fossa', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+38
View File
@@ -0,0 +1,38 @@
/*
* 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 {
configApiRef,
createApiFactory,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { fossaApiRef, FossaClient } from './api';
export const plugin = createPlugin({
id: 'fossa',
apis: [
createApiFactory({
api: fossaApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
new FossaClient({
discoveryApi,
organizationId: configApi.getOptionalString('fossa.organizationId'),
}),
}),
],
});
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 '@testing-library/jest-dom';