Update URLs to be more consistent

GET  /report?entity=<ref>

POST /report?entity=<ref>&coverageType=<cobertura|jacoco>

GET  /history?entity=<ref>

GET  /file-content?entity=<ref>&path=<filepath>

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
alde
2021-04-19 11:36:19 -04:00
committed by Fredrik Adelöw
parent e7d2fb93f0
commit 0e57ec3f91
7 changed files with 105 additions and 101 deletions
+8 -8
View File
@@ -22,12 +22,12 @@ Note: This requires the [`backstage.io/source-location` annotation](https://back
### Adding cobertura report
POST a cobertura xml to `/:kind/:namespace/:name?coverageType=cobertura`
POST a cobertura xml to `/report`
Example:
```json
// curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/Component/default/entity-name?coverageType=cobertura"
// curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/report?entity=component:default/entity-name&coverageType=cobertura"
{
"links": [
{
@@ -40,12 +40,12 @@ Example:
### Adding jacoco report
POST a jacoco xml to `/:kind/:namespace/:name?coverageType=jacoco`
POST a jacoco xml to `/report`
Example:
```json
// curl -X POST -H "Content-Type:text/xml" -d @jacoco.xml "localhost:7000/api/code-coverage/Component/default/entity-name?coverageType=jacoco"
// curl -X POST -H "Content-Type:text/xml" -d @jacoco.xml "localhost:7000/api/code-coverage/report?entity=component:default/entity-name&coverageType=jacoco"
{
"links": [
{
@@ -58,12 +58,12 @@ Example:
### Reading json coverage
GET `/:kind/:namespace/:name`
GET `/report`
Example:
```json
// curl localhost:7000/api/code-coverage/Component/default/entity-name
// curl localhost:7000/api/code-coverage/report?entity=component:default/entity-name
{
"aggregate": {
"branch": {
@@ -102,12 +102,12 @@ Example:
### Coverage history
GET `/:kind/:namespace/:name/history`
GET `/history`
Example
```json
// curl localhost:7000/api/code-coverage/Component/default/entity-name/history
// curl localhost:7000/api/code-coverage/history?entity=component:default/entity-name
{
"entity": {
"kind": "Component",
@@ -27,9 +27,7 @@ exports.up = async function up(knex) {
.primary()
.notNullable()
.comment('The ID of the code coverage');
table.text('entity_name').notNullable().comment('entity name');
table.text('entity_kind').notNullable().comment('entity kind');
table.text('entity_namespace').notNullable().comment('entity namespace');
table.text('entity').notNullable().comment('entity string reference');
table
.text('coverage')
.notNullable()
@@ -17,6 +17,7 @@ import {
getVoidLogger,
SingleConnectionDatabaseManager,
} from '@backstage/backend-common';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
CodeCoverageDatabase,
@@ -142,11 +143,13 @@ describe('CodeCoverageDatabase', () => {
describe('getCodeCoverage', () => {
it("can get coverage that's in the database", async () => {
const cov = await database.getCodeCoverage({
name: 'test-entity',
kind: 'Component',
namespace: 'default',
});
const cov = await database.getCodeCoverage(
stringifyEntityRef({
name: 'test-entity',
kind: 'Component',
namespace: 'default',
}),
);
expect(cov).toEqual(coverage[1]);
});
});
@@ -154,11 +157,11 @@ describe('CodeCoverageDatabase', () => {
describe('getHistory', () => {
it("can get history that's in the database", async () => {
const cov = await database.getHistory(
{
stringifyEntityRef({
name: 'test-entity',
kind: 'Component',
namespace: 'default',
},
}),
5,
);
expect(cov.history.length).toEqual(2);
@@ -15,7 +15,7 @@
*/
import { resolvePackagePath } from '@backstage/backend-common';
import { NotFoundError } from '@backstage/errors';
import { EntityName } from '@backstage/catalog-model';
import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model';
import { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import { Logger } from 'winston';
@@ -24,17 +24,15 @@ import { JsonCodeCoverage, JsonCoverageHistory } from './jsoncoverage-types';
export type RawDbCoverageRow = {
id: string;
entity_name: string;
entity_namespace: string;
entity_kind: string;
entity: string;
coverage: string;
};
export interface CodeCoverageStore {
insertCodeCoverage(
coverage: JsonCodeCoverage,
): Promise<{ codeCoverageId: string }>;
getCodeCoverage(entity: EntityName): Promise<JsonCodeCoverage>;
getHistory(entity: EntityName, limit: number): Promise<JsonCoverageHistory>;
getCodeCoverage(entity: string): Promise<JsonCodeCoverage>;
getHistory(entity: string, limit: number): Promise<JsonCoverageHistory>;
}
const migrationsDir = resolvePackagePath(
@@ -55,22 +53,22 @@ export class CodeCoverageDatabase implements CodeCoverageStore {
coverage: JsonCodeCoverage,
): Promise<{ codeCoverageId: string }> {
const codeCoverageId = uuid();
this.logger.error(JSON.stringify(coverage.entity));
const entity = stringifyEntityRef({
kind: coverage.entity.kind,
namespace: coverage.entity.namespace,
name: coverage.entity.name,
});
await this.db<RawDbCoverageRow>('code_coverage').insert({
id: codeCoverageId,
entity_name: coverage.entity.name,
entity_kind: coverage.entity.kind,
entity_namespace: coverage.entity.namespace,
entity: entity,
coverage: JSON.stringify(coverage),
});
return { codeCoverageId };
}
async getCodeCoverage(entity: EntityName): Promise<JsonCodeCoverage> {
async getCodeCoverage(entity: string): Promise<JsonCodeCoverage> {
const [result] = await this.db<RawDbCoverageRow>('code_coverage')
.where({
entity_name: entity.name,
entity_kind: entity.kind,
entity_namespace: entity.namespace,
})
.where({ entity: entity })
.orderBy('created_at', 'desc')
.limit(1)
.select();
@@ -87,15 +85,11 @@ export class CodeCoverageDatabase implements CodeCoverageStore {
}
async getHistory(
entity: EntityName,
entity: string,
limit: number,
): Promise<JsonCoverageHistory> {
const res = await this.db<RawDbCoverageRow>('code_coverage')
.where({
entity_name: entity.name,
entity_kind: entity.kind,
entity_namespace: entity.namespace,
})
.where({ entity: entity })
.orderBy('created_at', 'desc')
.limit(limit)
.select();
@@ -104,8 +98,13 @@ export class CodeCoverageDatabase implements CodeCoverageStore {
.map(r => JSON.parse(r.coverage))
.map(c => aggregateCoverage(c));
const entityName = parseEntityName(entity);
return {
entity,
entity: {
name: entityName.name,
kind: entityName.kind,
namespace: entityName.namespace,
},
history: history,
};
}
@@ -16,7 +16,7 @@
import { Request } from 'express';
import { UrlReader } from '@backstage/backend-common';
import { InputError, NotFoundError } from '@backstage/errors';
import { Entity } from '@backstage/catalog-model';
import { Entity, EntityName } from '@backstage/catalog-model';
import { ScmIntegration, ScmIntegrations } from '@backstage/integration';
import {
AggregateCoverage,
@@ -33,6 +33,11 @@ import { aggregateCoverage, CoverageUtils } from './CoverageUtils';
import { Cobertura } from './converter/cobertura';
import { Jacoco } from './converter/jacoco';
import { Converter } from './converter';
import {
EntityRef,
parseEntityName,
parseEntityRef,
} from '@backstage/catalog-model';
export interface RouterOptions {
config: Config;
@@ -69,19 +74,17 @@ export const makeRouter = async (
res.status(200).json({ status: 'ok' });
});
router.get('/:kind/:namespace/:name', async (req, res) => {
const { kind, namespace, name } = req.params;
const entity = await catalogApi.getEntityByName({ kind, namespace, name });
if (!entity) {
throw new NotFoundError(
`No entity found matching ${kind}/${namespace}/${name}`,
);
/**
* /report?entity=component:default/mycomponent
*/
router.get('/report', async (req, res) => {
const { entity } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
const stored = await codeCoverageDatabase.getCodeCoverage({
kind,
namespace,
name,
});
const stored = await codeCoverageDatabase.getCodeCoverage(entity as string);
const aggregate = aggregateCoverage(stored);
@@ -94,45 +97,45 @@ export const makeRouter = async (
});
});
router.get('/:kind/:namespace/:name/history', async (req, res) => {
const { kind, namespace, name } = req.params;
const entity = await catalogApi.getEntityByName({ kind, namespace, name });
if (!entity) {
throw new NotFoundError(
`No entity found matching ${kind}/${namespace}/${name}`,
);
/**
* /history?entity=component:default/mycomponent
*/
router.get('/history', async (req, res) => {
const { entity } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
const { limit } = req.query;
const history = await codeCoverageDatabase.getHistory(
{
kind,
namespace,
name,
},
entity as string,
parseInt(limit?.toString() || '10', 10),
);
res.status(200).json(history);
});
router.get('/:kind/:namespace/:name/file-content', async (req, res) => {
const { kind, namespace, name } = req.params;
const entity = await catalogApi.getEntityByName({ kind, namespace, name });
if (!entity) {
throw new NotFoundError(
`No entity found matching ${kind}/${namespace}/${name}`,
);
/**
* /file-content?entity=component:default/mycomponent&path=src/some-file.go
*/
router.get('/file-content', async (req, res) => {
const { entity, path } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
const { path } = req.query;
if (!path) {
throw new InputError('Need path query parameter');
}
const sourceLocation =
entity.metadata.annotations?.['backstage.io/source-location'];
entityLookup.metadata.annotations?.['backstage.io/source-location'];
if (!sourceLocation) {
throw new InputError(
`No "backstage.io/source-location" annotation on entity ${entity.kind}/${entity.metadata.namespace}/${entity.metadata.name}`,
`No "backstage.io/source-location" annotation on entity ${entity}`,
);
}
@@ -165,9 +168,17 @@ export const makeRouter = async (
res.status(200).contentType('text/plain').send(data);
});
router.post('/:kind/:namespace/:name/', async (req, res) => {
const { kind, namespace, name } = req.params;
const { coverageType } = req.query;
/**
* /report?entity=component:default/mycomponent&coverageType=cobertura
*/
router.post('/report', async (req, res) => {
const { entity, coverageType } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
let converter: Converter;
if (!coverageType) {
throw new InputError('Need coverageType query parameter');
@@ -178,18 +189,13 @@ export const makeRouter = async (
} else {
throw new NotFoundError(`unsupported coverage type '${coverageType}`);
}
const entity = await catalogApi.getEntityByName({ kind, namespace, name });
if (!entity) {
throw new NotFoundError(
`No entity found matching ${kind}/${namespace}/${name}`,
);
}
const {
sourceLocation,
vcs,
scmFiles,
body,
} = await utils.processCoveragePayload(entity, req);
} = await utils.processCoveragePayload(entityLookup, req);
const files = converter.convert(body, scmFiles);
if (!files || files.length === 0) {
@@ -197,7 +203,7 @@ export const makeRouter = async (
}
const coverage = await utils.buildCoverage(
entity,
entityLookup,
sourceLocation,
vcs,
files,
@@ -208,7 +214,7 @@ export const makeRouter = async (
links: [
{
rel: 'coverage',
href: `${codecovUrl}/${kind}/${namespace}/${name}`,
href: `${codecovUrl}/report?entity=${entity}`,
},
],
});