diff --git a/.changeset/warm-peas-hang.md b/.changeset/warm-peas-hang.md new file mode 100644 index 0000000000..5359e2c547 --- /dev/null +++ b/.changeset/warm-peas-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sonarqube-backend': patch +--- + +Added optional `externalBaseUrl` config for setting a different frontend URL diff --git a/plugins/sonarqube-backend/README.md b/plugins/sonarqube-backend/README.md index 4d16dce025..b417fea7c4 100644 --- a/plugins/sonarqube-backend/README.md +++ b/plugins/sonarqube-backend/README.md @@ -138,6 +138,34 @@ sonarqube: apiKey: abcdef0123456789abcedf0123456789ab ``` +#### Example - Different frontend and backend URLs + +In some instances, you might want to use one URL for the backend and another for the frontend. +This can be achieved by using the optional `externalBaseUrl` property in the config. + +##### Single instance config + +```yaml +sonarqube: + baseUrl: https://sonarqube-internal.example.com + externalBaseUrl: https://sonarqube.example.com + apiKey: 123456789abcdef0123456789abcedf012 +``` + +##### Multiple instance config + +```yaml +sonarqube: + instances: + - name: default + baseUrl: https://default-sonarqube-internal.example.com + externalBaseUrl: https://default-sonarqube.example.com + apiKey: 123456789abcdef0123456789abcedf012 + - name: specialProject + baseUrl: https://special-project-sonarqube.example.com + apiKey: abcdef0123456789abcedf0123456789ab +``` + ## Links - [Sonarqube Frontend](../sonarqube/README.md) diff --git a/plugins/sonarqube-backend/api-report.md b/plugins/sonarqube-backend/api-report.md index 0fbd463cf3..535db37185 100644 --- a/plugins/sonarqube-backend/api-report.md +++ b/plugins/sonarqube-backend/api-report.md @@ -15,6 +15,7 @@ export class DefaultSonarqubeInfoProvider implements SonarqubeInfoProvider { static fromConfig(config: Config): DefaultSonarqubeInfoProvider; getBaseUrl(options?: { instanceName?: string }): { baseUrl: string; + externalBaseUrl?: string; }; getFindings(options: { componentKey: string; @@ -49,6 +50,7 @@ export interface SonarqubeFindings { export interface SonarqubeInfoProvider { getBaseUrl(options?: { instanceName?: string }): { baseUrl: string; + externalBaseUrl?: string; }; getFindings(options: { componentKey: string; @@ -60,6 +62,7 @@ export interface SonarqubeInfoProvider { export interface SonarqubeInstanceConfig { apiKey: string; baseUrl: string; + externalBaseUrl?: string; name: string; } diff --git a/plugins/sonarqube-backend/config.d.ts b/plugins/sonarqube-backend/config.d.ts index 7e93f61eb4..6e03a53f0c 100644 --- a/plugins/sonarqube-backend/config.d.ts +++ b/plugins/sonarqube-backend/config.d.ts @@ -23,6 +23,13 @@ export interface Config { */ baseUrl?: string; + /** + * The external url of the sonarqube installation. + * Use this if you want to use a different url for the frontend than the backend. + * @visibility frontend + */ + externalBaseUrl?: string; + /** * The api key to access the sonarqube instance under baseUrl. * @visibility secret @@ -46,6 +53,13 @@ export interface Config { */ baseUrl: string; + /** + * The external url of the sonarqube instance. + * Use this if you want to use a different url for the frontend than the backend. + * @visibility frontend + */ + externalBaseUrl?: string; + /** * The api key to access the sonarqube instance. * @visibility secret diff --git a/plugins/sonarqube-backend/src/service/router.test.ts b/plugins/sonarqube-backend/src/service/router.test.ts index 7045cdf0d2..b40e2d1528 100644 --- a/plugins/sonarqube-backend/src/service/router.test.ts +++ b/plugins/sonarqube-backend/src/service/router.test.ts @@ -24,7 +24,7 @@ import { SonarqubeFindings } from './sonarqubeInfoProvider'; describe('createRouter', () => { let app: express.Express; const getBaseUrlMock: jest.Mock< - { baseUrl: string }, + { baseUrl: string; externalBaseUrl?: string }, [{ instanceName: string }] > = jest.fn(); const getFindingsMock: jest.Mock< @@ -55,6 +55,7 @@ describe('createRouter', () => { describe('GET /findings', () => { const DUMMY_COMPONENT_KEY = 'my:component'; const DUMMY_INSTANCE_KEY = 'myInstance'; + it('returns ok', async () => { const measures = { analysisDate: '2022-01-01T00:00:00Z', @@ -77,6 +78,7 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toEqual(measures); }); + it('returns an error when component key is not defined', async () => { const response = await request(app) .get('/findings') @@ -112,9 +114,12 @@ describe('createRouter', () => { expect(response.body).toEqual(measures); }); }); + describe('GET /instanceUrl', () => { const DUMMY_INSTANCE_KEY = 'myInstance'; - const DUMMY_INSTANCE_URL = 'http://sonarqube.example.com'; + const DUMMY_INSTANCE_URL = 'http://sonarqube-internal.example.com'; + const DUMMY_INSTANCE_EXTERNAL_URL = 'http://sonarqube.example.com'; + it('returns ok', async () => { getBaseUrlMock.mockReturnValue({ baseUrl: DUMMY_INSTANCE_URL }); const response = await request(app) @@ -141,5 +146,17 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toEqual({ instanceUrl: DUMMY_INSTANCE_URL }); }); + + it('returns the external base url when provided', async () => { + getBaseUrlMock.mockReturnValue({ + baseUrl: DUMMY_INSTANCE_URL, + externalBaseUrl: DUMMY_INSTANCE_EXTERNAL_URL, + }); + const response = await request(app).get('/instanceUrl').send(); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + instanceUrl: DUMMY_INSTANCE_EXTERNAL_URL, + }); + }); }); }); diff --git a/plugins/sonarqube-backend/src/service/router.ts b/plugins/sonarqube-backend/src/service/router.ts index f244ae5ce3..5a993b5d59 100644 --- a/plugins/sonarqube-backend/src/service/router.ts +++ b/plugins/sonarqube-backend/src/service/router.ts @@ -81,11 +81,11 @@ export async function createRouter( ? `Retrieving sonarqube instance URL for key ${instanceKey}` : `Retrieving default sonarqube instance URL as instanceKey is not provided`, ); - const { baseUrl } = sonarqubeInfoProvider.getBaseUrl({ + const { baseUrl, externalBaseUrl } = sonarqubeInfoProvider.getBaseUrl({ instanceName: instanceKey, }); response.json({ - instanceUrl: baseUrl, + instanceUrl: externalBaseUrl || baseUrl, }); }); diff --git a/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.test.ts b/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.test.ts index 72ab2ecee2..468020c1b3 100644 --- a/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.test.ts +++ b/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.test.ts @@ -27,6 +27,7 @@ describe('SonarqubeConfig', () => { const SONARQUBE_DEFAULT_INSTANCE_NAME = 'default'; const DUMMY_SONAR_URL = 'https://sonarqube.example.com'; const DUMMY_SONAR_APIKEY = '123456789abcdef0123456789abcedf012'; + const DUMMY_SIMPLE_OBJECT_FOR_DEFAULT_SONARQUBE_CONFIG = { name: SONARQUBE_DEFAULT_INSTANCE_NAME, baseUrl: DUMMY_SONAR_URL, @@ -112,6 +113,7 @@ describe('SonarqubeConfig', () => { }, ]); }); + it('Throw an error if both a named default config and top level config', async () => { expect(() => SonarqubeConfig.fromConfig( @@ -299,6 +301,46 @@ describe('DefaultSonarqubeInfoProvider', () => { baseUrl: 'https://sonarqube-other.example.com', }); }); + + it('Provide external base url for simple config', async () => { + const provider = configureProvider({ + sonarqube: { + baseUrl: 'https://sonarqube-internal.example.com', + externalBaseUrl: 'https://sonarqube.example.com', + apiKey: '123456789abcdef0123456789abcedf012', + }, + }); + + expect(provider.getBaseUrl()).toEqual({ + baseUrl: 'https://sonarqube-internal.example.com', + externalBaseUrl: 'https://sonarqube.example.com', + }); + }); + + it('Provide external base url for named config', async () => { + const provider = configureProvider({ + sonarqube: { + instances: [ + { + name: 'default', + baseUrl: 'https://sonarqube.example.com', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://sonarqube-other-internal.example.com', + externalBaseUrl: 'https://sonarqube-other.example.com', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ], + }, + }); + + expect(provider.getBaseUrl({ instanceName: 'other' })).toEqual({ + baseUrl: 'https://sonarqube-other-internal.example.com', + externalBaseUrl: 'https://sonarqube-other.example.com', + }); + }); }); describe('getFindings', () => { @@ -385,6 +427,7 @@ describe('DefaultSonarqubeInfoProvider', () => { apiKey: DUMMY_API_KEY, }, }; + it('Provide findings when everything is ok', async () => { setupHandlers(); const provider = configureProvider(DUMMY_SIMPLE_CONFIG_FOR_PROVIDER); diff --git a/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.ts b/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.ts index 2b96e6764e..a5c0bf985a 100644 --- a/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.ts +++ b/plugins/sonarqube-backend/src/service/sonarqubeInfoProvider.ts @@ -30,7 +30,10 @@ export interface SonarqubeInfoProvider { * @param instanceName - Name of the sonarqube instance to get the info from * @returns the url of the instance */ - getBaseUrl(options?: { instanceName?: string }): { baseUrl: string }; + getBaseUrl(options?: { instanceName?: string }): { + baseUrl: string; + externalBaseUrl?: string; + }; /** * Query the sonarqube instance corresponding to the instanceName to get all @@ -96,6 +99,10 @@ export interface SonarqubeInstanceConfig { * Base url to access the instance */ baseUrl: string; + /** + * External url to access the instance from the frontend + */ + externalBaseUrl?: string; /** * Access token to access the sonarqube instance as generated in user profile. */ @@ -132,6 +139,7 @@ export class SonarqubeConfig { sonarqubeConfig.getOptionalConfigArray('instances')?.map(c => ({ name: c.getString('name'), baseUrl: c.getString('baseUrl'), + externalBaseUrl: c.getOptionalString('externalBaseUrl'), apiKey: c.getString('apiKey'), })) || []; @@ -142,9 +150,11 @@ export class SonarqubeConfig { // Get these as optional strings and check to give a better error message const baseUrl = sonarqubeConfig.getOptionalString('baseUrl'); + const externalBaseUrl = + sonarqubeConfig.getOptionalString('externalBaseUrl'); const apiKey = sonarqubeConfig.getOptionalString('apiKey'); - if (hasNamedDefault && (baseUrl || apiKey)) { + if (hasNamedDefault && (baseUrl || externalBaseUrl || apiKey)) { throw new Error( `Found both a named sonarqube instance with name ${DEFAULT_SONARQUBE_NAME} and top level baseUrl or apiKey config. Use only one style of config.`, ); @@ -160,10 +170,11 @@ export class SonarqubeConfig { if (unnamedAllPresent) { const unnamedInstanceConfig = [ - { name: DEFAULT_SONARQUBE_NAME, baseUrl, apiKey }, + { name: DEFAULT_SONARQUBE_NAME, baseUrl, externalBaseUrl, apiKey }, ] as { name: string; baseUrl: string; + externalBaseUrl?: string; apiKey: string; }[]; @@ -303,11 +314,15 @@ export class DefaultSonarqubeInfoProvider implements SonarqubeInfoProvider { */ getBaseUrl(options: { instanceName?: string } = {}): { baseUrl: string; + externalBaseUrl?: string; } { const instanceConfig = this.config.getInstanceConfig({ sonarqubeName: options.instanceName, }); - return { baseUrl: instanceConfig.baseUrl }; + return { + baseUrl: instanceConfig.baseUrl, + externalBaseUrl: instanceConfig.externalBaseUrl, + }; } /**