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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-sonarqube-backend': patch
|
||||
---
|
||||
|
||||
Added optional `externalBaseUrl` config for setting a different frontend URL
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user