Merge pull request #18825 from ConnorDY/feature/sonarqube-external-url

[sonarqube-backend plugin] added optional `externalBaseUrl` config for overriding the frontend URL
This commit is contained in:
Patrik Oldsberg
2023-08-12 15:10:38 +02:00
committed by GitHub
8 changed files with 133 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-sonarqube-backend': patch
---
Added optional `externalBaseUrl` config for setting a different frontend URL
+28
View File
@@ -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)
+3
View File
@@ -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;
}
+14
View File
@@ -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
@@ -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,
});
});
});
});
@@ -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,
});
});
@@ -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);
@@ -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,
};
}
/**