From 619b5151721ad879cb608efef861f65f76644d3f Mon Sep 17 00:00:00 2001 From: Neemys <36508659+Neemys@users.noreply.github.com> Date: Thu, 16 Jun 2022 17:50:24 +0200 Subject: [PATCH] Modify sonarqube frontend plugin to call the new sonarqube backend backend only have mock API for now Signed-off-by: Neemys <36508659+Neemys@users.noreply.github.com> --- .changeset/forty-lobsters-guess.md | 9 ++ .../src/service/router.test.ts | 12 +- .../sonarqube-backend/src/service/router.ts | 11 +- .../sonarqube/src/api/SonarQubeClient.test.ts | 138 +++--------------- plugins/sonarqube/src/api/SonarQubeClient.ts | 55 ++----- plugins/sonarqube/src/api/types.ts | 10 +- 6 files changed, 57 insertions(+), 178 deletions(-) create mode 100644 .changeset/forty-lobsters-guess.md diff --git a/.changeset/forty-lobsters-guess.md b/.changeset/forty-lobsters-guess.md new file mode 100644 index 0000000000..50dd22627f --- /dev/null +++ b/.changeset/forty-lobsters-guess.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-sonarqube': minor +--- + +**BREAKING** This plugin now call the sonarqube-backend plugin instead of relying on the proxy plugin + +The whole proxy's `'/sonarqube':` key can be removed from your configuration files. + +Then head to the sonarqube-backend plugin page to learn how to set-up the link to your sonarqube instances. diff --git a/plugins/sonarqube-backend/src/service/router.test.ts b/plugins/sonarqube-backend/src/service/router.test.ts index 8b77a04348..24e482896b 100644 --- a/plugins/sonarqube-backend/src/service/router.test.ts +++ b/plugins/sonarqube-backend/src/service/router.test.ts @@ -34,12 +34,18 @@ describe('createRouter', () => { jest.resetAllMocks(); }); - describe('GET /health', () => { + describe('GET /findings', () => { it('returns ok', async () => { - const response = await request(app).get('/health'); + const response = await request(app) + .get('/findings') + .set('componentKey', 'my:app') + .send(); expect(response.status).toEqual(200); - expect(response.body).toEqual({ status: 'ok' }); + expect(response.body).toEqual({ + analysisDate: '2022-10-22T04:55:23Z', + measures: [{ metric: 'coverage', value: '50' }], + }); }); }); }); diff --git a/plugins/sonarqube-backend/src/service/router.ts b/plugins/sonarqube-backend/src/service/router.ts index 9ceaa47627..9d13104d82 100644 --- a/plugins/sonarqube-backend/src/service/router.ts +++ b/plugins/sonarqube-backend/src/service/router.ts @@ -30,10 +30,13 @@ export async function createRouter( const router = Router(); router.use(express.json()); - - router.get('/health', (_, response) => { - logger.info('PONG!'); - response.send({ status: 'ok' }); + // mock api for now + router.get('/findings', (request, response) => { + logger.info(request.params); + response.send({ + analysisDate: '2022-10-22T04:55:23Z', + measures: [{ metric: 'coverage', value: '50' }], + }); }); router.use(errorHandler()); return router; diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index b592953434..1727e73e93 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -18,7 +18,7 @@ import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FindingSummary, SonarQubeClient } from './index'; -import { ComponentWrapper, MeasuresWrapper } from './types'; +import { FindingsWrapper } from './types'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; @@ -40,133 +40,66 @@ const identityApiGuest: IdentityApi = { describe('SonarQubeClient', () => { setupRequestMockHandlers(server); - const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const mockBaseUrl = 'http://backstage:9191/api/sonarqube'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - const setupHandlers = ( - metricKeys = [ - 'alert_status', - 'bugs', - 'reliability_rating', - 'vulnerabilities', - 'security_rating', - 'security_hotspots_reviewed', - 'security_review_rating', - 'code_smells', - 'sqale_rating', - 'coverage', - 'duplicated_lines_density', - ], - ) => { + const setupHandlers = () => { server.use( - rest.get(`${mockBaseUrl}/sonarqube/metrics/search`, (req, res, ctx) => { - expect(req.url.searchParams.get('ps')).toBe('500'); - - // emulate paging to check if everything is requested - if (req.url.searchParams.get('p') === '1') { - return res( - ctx.json({ - metrics: metricKeys.slice(0, 5).map(k => ({ key: k })), - total: metricKeys.length, - }), - ); - } - - // make sure this is only called twice - expect(req.url.searchParams.get('p')).toBe('2'); - return res( - ctx.json({ - metrics: metricKeys.slice(5).map(k => ({ key: k })), - total: metricKeys.length, - }), - ); - }), - ); - - server.use( - rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe('component=our%3Aservice'); - return res( - ctx.json({ - component: { - analysisDate: '2020-01-01T00:00:00Z', - }, - } as ComponentWrapper), - ); - }), - ); - - server.use( - rest.get(`${mockBaseUrl}/sonarqube/measures/search`, (req, res, ctx) => { + rest.get(`${mockBaseUrl}/findings`, (req, res, ctx) => { expect(req.url.searchParams.toString()).toBe( - `projectKeys=our%3Aservice&metricKeys=${metricKeys.join('%2C')}`, + 'componentKey=our%3Aservice', ); return res( ctx.json({ + analysisDate: '2020-01-01T00:00:00Z', measures: [ { metric: 'alert_status', value: 'OK', - component: 'our:service', - }, - { - metric: 'alert_status', - value: 'ERROR', - component: 'other-service', }, { metric: 'bugs', value: '2', - component: 'our:service', }, { metric: 'reliability_rating', value: '3.0', - component: 'our:service', }, { metric: 'vulnerabilities', value: '4', - component: 'our:service', }, { metric: 'security_rating', value: '1.0', - component: 'our:service', }, { metric: 'security_hotspots_reviewed', value: '100', - component: 'our:service', }, { metric: 'security_review_rating', value: '1.0', - component: 'our:service', }, { metric: 'code_smells', value: '100', - component: 'our:service', }, { metric: 'sqale_rating', value: '2.0', - component: 'our:service', }, { metric: 'coverage', value: '55.5', - component: 'our:service', }, { metric: 'duplicated_lines_density', value: '1.0', - component: 'our:service', }, - ].filter(m => metricKeys.includes(m.metric)), - } as MeasuresWrapper), + ], + } as FindingsWrapper), ); }), ); @@ -246,47 +179,19 @@ describe('SonarQubeClient', () => { ); }); - it('should only request selected metrics', async () => { - setupHandlers(['alert_status', 'bugs']); - - const client = new SonarQubeClient({ - discoveryApi, - baseUrl: 'http://a.instance.local', - identityApi: identityApiAuthenticated, - }); - - const summary = await client.getFindingSummary('our:service'); - - expect(summary).toEqual( - expect.objectContaining({ - lastAnalysis: '2020-01-01T00:00:00Z', - metrics: { - alert_status: 'OK', - bugs: '2', - }, - projectUrl: 'http://a.instance.local/dashboard?id=our%3Aservice', - }) as FindingSummary, - ); - expect(summary?.getIssuesUrl('CODE_SMELL')).toEqual( - 'http://a.instance.local/project/issues?id=our%3Aservice&types=CODE_SMELL&resolved=false', - ); - expect(summary?.getComponentMeasuresUrl('COVERAGE')).toEqual( - 'http://a.instance.local/component_measures?id=our%3Aservice&metric=coverage&resolved=false&view=list', - ); - }); - it('should add identity token for logged in users', async () => { setupHandlers(); server.use( - rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe('component=our%3Aservice'); + rest.get(`${mockBaseUrl}/findings`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'componentKey=our%3Aservice', + ); expect(req.headers.get('Authorization')).toBe('Bearer fake-id-token'); return res( ctx.json({ - component: { - analysisDate: '2020-01-01T00:00:00Z', - }, - } as ComponentWrapper), + analysisDate: '2020-01-01T00:00:00Z', + measures: [], + } as FindingsWrapper), ); }), ); @@ -304,15 +209,16 @@ describe('SonarQubeClient', () => { it('should omit identity token for guest users', async () => { setupHandlers(); server.use( - rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe('component=our%3Aservice'); + rest.get(`${mockBaseUrl}/findings`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'componentKey=our%3Aservice', + ); expect(req.headers.has('Authorization')).toBeFalsy(); return res( ctx.json({ - component: { - analysisDate: '2020-01-01T00:00:00Z', - }, - } as ComponentWrapper), + analysisDate: '2020-01-01T00:00:00Z', + measures: [], + } as FindingsWrapper), ); }), ); diff --git a/plugins/sonarqube/src/api/SonarQubeClient.ts b/plugins/sonarqube/src/api/SonarQubeClient.ts index 39d0f11369..ed0b7dc3a7 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.ts @@ -16,7 +16,7 @@ import fetch from 'cross-fetch'; import { FindingSummary, Metrics, SonarQubeApi } from './SonarQubeApi'; -import { ComponentWrapper, MeasuresWrapper } from './types'; +import { FindingsWrapper } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; export class SonarQubeClient implements SonarQubeApi { @@ -44,7 +44,7 @@ export class SonarQubeClient implements SonarQubeApi { ): Promise { const { token: idToken } = await this.identityApi.getCredentials(); - const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`; + const apiUrl = `${await this.discoveryApi.getBaseUrl('sonarqube')}`; const response = await fetch( `${apiUrl}/${path}?${new URLSearchParams(query).toString()}`, { @@ -60,27 +60,6 @@ export class SonarQubeClient implements SonarQubeApi { return undefined; } - private async getSupportedMetrics(): Promise { - const metrics: string[] = []; - let nextPage: number = 1; - - for (;;) { - const result = await this.callApi<{ - metrics: Array<{ key: string }>; - total: number; - }>('metrics/search', { ps: 500, p: nextPage }); - - metrics.push(...(result?.metrics?.map(m => m.key) ?? [])); - - if (result && metrics.length < result.total) { - nextPage++; - continue; - } - - return metrics; - } - } - async getFindingSummary( componentKey?: string, ): Promise { @@ -88,13 +67,6 @@ export class SonarQubeClient implements SonarQubeApi { return undefined; } - const component = await this.callApi('components/show', { - component: componentKey, - }); - if (!component) { - return undefined; - } - const metrics: Metrics = { alert_status: undefined, bugs: undefined, @@ -109,28 +81,19 @@ export class SonarQubeClient implements SonarQubeApi { duplicated_lines_density: undefined, }; - // select the metrics that are supported by the SonarQube instance - const supportedMetrics = await this.getSupportedMetrics(); - const metricKeys = Object.keys(metrics).filter(m => - supportedMetrics.includes(m), - ); - - const measures = await this.callApi('measures/search', { - projectKeys: componentKey, - metricKeys: metricKeys.join(','), + const findings = await this.callApi('findings', { + componentKey: componentKey, }); - if (!measures) { + if (!findings) { return undefined; } - measures.measures - .filter(m => m.component === componentKey) - .forEach(m => { - metrics[m.metric] = m.value; - }); + findings.measures.forEach(m => { + metrics[m.metric] = m.value; + }); return { - lastAnalysis: component.component.analysisDate, + lastAnalysis: findings.analysisDate, metrics, projectUrl: `${this.baseUrl}dashboard?id=${encodeURIComponent( componentKey, diff --git a/plugins/sonarqube/src/api/types.ts b/plugins/sonarqube/src/api/types.ts index 6e951f9d8b..27f065d36a 100644 --- a/plugins/sonarqube/src/api/types.ts +++ b/plugins/sonarqube/src/api/types.ts @@ -14,15 +14,8 @@ * limitations under the License. */ -export interface ComponentWrapper { - component: Component; -} - -export interface Component { +export interface FindingsWrapper { analysisDate: string; -} - -export interface MeasuresWrapper { measures: Measure[]; } @@ -55,7 +48,6 @@ export type MetricKey = export interface Measure { metric: MetricKey; value: string; - component: string; } export type SonarUrlProcessorFunc = (identifier: string) => string;