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:
@@ -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}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { Config } from '@backstage/config';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { EntityName, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
export class FetchError extends Error {
|
||||
get name(): string {
|
||||
@@ -69,33 +69,31 @@ export class CodeCoverageRestApi implements CodeCoverageApi {
|
||||
return await resp.text();
|
||||
}
|
||||
|
||||
async getCoverageForEntity({
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
}: EntityName): Promise<any> {
|
||||
return await this.fetch<any>(
|
||||
`/api/code-coverage/${kind}/${namespace}/${name}`,
|
||||
);
|
||||
async getCoverageForEntity(entityName: EntityName): Promise<any> {
|
||||
const entity = stringifyEntityRef(entityName);
|
||||
return await this.fetch<any>(`/api/code-coverage/report?entity=${entity}`);
|
||||
}
|
||||
|
||||
async getFileContentFromEntity(
|
||||
{ kind, namespace, name }: EntityName,
|
||||
entityName: EntityName,
|
||||
filePath: string,
|
||||
): Promise<any> {
|
||||
const entity = stringifyEntityRef(entityName);
|
||||
return await this.fetch<any>(
|
||||
`/api/code-coverage/${kind}/${namespace}/${name}/file-content?path=${filePath}`,
|
||||
/// file-content?entity=component:default/mycomponent&path=src/some-file.go
|
||||
`/api/code-coverage/file-content?entity=${entity}&path=${filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getCoverageHistoryForEntity(
|
||||
{ kind, namespace, name }: EntityName,
|
||||
entityName: EntityName,
|
||||
limit?: number,
|
||||
): Promise<any> {
|
||||
const entity = stringifyEntityRef(entityName);
|
||||
const hasValidLimit = limit && limit > 0;
|
||||
return await this.fetch<any>(
|
||||
`/api/code-coverage/${kind}/${namespace}/${name}/history${
|
||||
hasValidLimit ? `?limit=${limit}` : ''
|
||||
`/api/code-coverage/history?entity=${entity}${
|
||||
hasValidLimit ? `&limit=${limit}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user