diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 2e2f5ba91f..3229dc940f 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -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", diff --git a/plugins/code-coverage-backend/migrations/20210302_init.js b/plugins/code-coverage-backend/migrations/20210302_init.js index 0edd455e7d..b156a9d08d 100644 --- a/plugins/code-coverage-backend/migrations/20210302_init.js +++ b/plugins/code-coverage-backend/migrations/20210302_init.js @@ -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() diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts index af734dd358..108146287d 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts @@ -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); diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts index 44109e8e2e..cb83c2f606 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts @@ -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; - getHistory(entity: EntityName, limit: number): Promise; + getCodeCoverage(entity: string): Promise; + getHistory(entity: string, limit: number): Promise; } 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('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 { + async getCodeCoverage(entity: string): Promise { const [result] = await this.db('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 { const res = await this.db('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, }; } diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.ts index 0ed1561003..031e72434a 100644 --- a/plugins/code-coverage-backend/src/service/CoverageUtils.ts +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.ts @@ -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, diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index 3f3dcf8a1e..576bc26a31 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -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}`, }, ], }); diff --git a/plugins/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts index da18bde267..000ff132e9 100644 --- a/plugins/code-coverage/src/api.ts +++ b/plugins/code-coverage/src/api.ts @@ -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 { - return await this.fetch( - `/api/code-coverage/${kind}/${namespace}/${name}`, - ); + async getCoverageForEntity(entityName: EntityName): Promise { + const entity = stringifyEntityRef(entityName); + return await this.fetch(`/api/code-coverage/report?entity=${entity}`); } async getFileContentFromEntity( - { kind, namespace, name }: EntityName, + entityName: EntityName, filePath: string, ): Promise { + const entity = stringifyEntityRef(entityName); return await this.fetch( - `/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 { + const entity = stringifyEntityRef(entityName); const hasValidLimit = limit && limit > 0; return await this.fetch( - `/api/code-coverage/${kind}/${namespace}/${name}/history${ - hasValidLimit ? `?limit=${limit}` : '' + `/api/code-coverage/history?entity=${entity}${ + hasValidLimit ? `&limit=${limit}` : '' }`, ); }