Merge pull request #7192 from awanlin/awanlin/azure-devops-backend-plugin

Adding Azure DevOps backend plugin
This commit is contained in:
Patrik Oldsberg
2021-09-28 19:16:57 +02:00
committed by GitHub
19 changed files with 901 additions and 0 deletions
+5
View File
@@ -400,3 +400,8 @@ jenkins:
baseUrl: https://jenkins.example.com
username: backstage-bot
apiKey: 123456789abcdef0123456789abcedf012
azureDevOps:
host: dev.azure.com
token: ${AZURE_TOKEN}
organization: my-company
+1
View File
@@ -31,6 +31,7 @@
"@backstage/integration": "^0.6.5",
"@backstage/plugin-app-backend": "^0.3.16",
"@backstage/plugin-auth-backend": "^0.4.1",
"@backstage/plugin-azure-devops-backend": "^0.1.0",
"@backstage/plugin-badges-backend": "^0.1.10",
"@backstage/plugin-catalog-backend": "^0.14.0",
"@backstage/plugin-code-coverage-backend": "^0.1.11",
+3
View File
@@ -38,6 +38,7 @@ import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import { metricsInit, metricsHandler } from './metrics';
import auth from './plugins/auth';
import azureDevOps from './plugins/azuredevops';
import catalog from './plugins/catalog';
import codeCoverage from './plugins/codecoverage';
import kubernetes from './plugins/kubernetes';
@@ -94,6 +95,7 @@ async function main() {
);
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
@@ -112,6 +114,7 @@ async function main() {
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv));
apiRouter.use('/search', await search(searchEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/todo', await todo(todoEnv));
@@ -0,0 +1,26 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-azure-devops-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
return await createRouter({ logger, config });
}
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+24
View File
@@ -0,0 +1,24 @@
# Azure DevOps Backend
Simple plugin that proxies requests to the [Azure DevOps](https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-6.1) API.
## Setup
The following values are read from the configuration file:
```yaml
azureDevOps:
host: dev.azure.com
token: ${AZURE_TOKEN}
organization: my-company
```
Configuration Details:
- `host` and `token` can be the same as the ones used for the `integration` section
- `AZURE_TOKEN` environment variable must be set to a [Personal Access Token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) with read access to both Code and Build
- `organization` is your Azure DevOps Organization name or for Azure DevOps Server (on-premise) this will be your Collection name
## Links
- [The Backstage homepage](https://backstage.io)
@@ -0,0 +1,70 @@
## API Report File for "@backstage/plugin-azure-devops-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { BuildResult } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Config } from '@backstage/config';
import express from 'express';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { Logger as Logger_2 } from 'winston';
import { WebApi } from 'azure-devops-node-api';
// Warning: (ae-missing-release-tag) "AzureDevOpsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class AzureDevOpsApi {
constructor(logger: Logger_2, webApi: WebApi);
// (undocumented)
getBuildList(
projectName: string,
repoId: string,
top: number,
): Promise<Build[]>;
// (undocumented)
getGitRepository(
projectName: string,
repoName: string,
): Promise<GitRepository>;
// (undocumented)
getRepoBuilds(
projectName: string,
repoName: string,
top: number,
): Promise<RepoBuild[]>;
}
// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type RepoBuild = {
id?: number;
title: string;
link: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
source: string;
};
// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
azureDevOpsApi?: AzureDevOpsApi;
// (undocumented)
config: Config;
// (undocumented)
logger: Logger_2;
}
// (No @packageDocumentation comment for this package)
```
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/** Configuration options for the azure-devops-backend plugin */
azureDevOps?: {
/**
* The hostname of the given Azure instance
*/
host: string;
/**
* Token used to authenticate requests.
* @visibility secret
*/
token?: string;
/**
* The organization of the given Azure instance
*/
organization: string;
};
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@backstage/plugin-azure-devops-backend",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.9.2",
"@backstage/config": "^0.1.9",
"@types/express": "^4.17.6",
"azure-devops-node-api": "^11.0.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.7.11",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2",
"msw": "^0.29.0"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -0,0 +1,97 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { repoBuildFromBuild } from './AzureDevOpsApi';
import { RepoBuild } from './types';
import {
Build,
BuildResult,
BuildStatus,
DefinitionReference,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
describe('AzureDevOpsApi', () => {
describe('repoBuildFromBuild', () => {
it('should return RepoBuild from Build', () => {
const inputBuildDefinition: DefinitionReference = {
name: 'My Build Definition',
};
const inputLinks: any = {
web: {
href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
},
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: inputBuildDefinition,
_links: inputLinks,
};
const outputRepoBuild: RepoBuild = {
id: 1,
title: 'My Build Definition - Build-1',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with no Build definition name', () => {
it('should return RepoBuild with only Build Number for title', () => {
const inputLinks: any = {
web: {
href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
},
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: inputLinks,
};
const outputRepoBuild: RepoBuild = {
id: 1,
title: 'Build-1',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
});
@@ -0,0 +1,106 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Logger } from 'winston';
import { WebApi } from 'azure-devops-node-api';
import { RepoBuild } from './types';
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
export class AzureDevOpsApi {
constructor(
private readonly logger: Logger,
private readonly webApi: WebApi,
) {}
async getGitRepository(projectName: string, repoName: string) {
if (this.logger) {
this.logger.debug(
`Calling Azure DevOps REST API, getting Repository ${repoName} for Project ${projectName}`,
);
}
const client = await this.webApi.getGitApi();
return client.getRepository(repoName, projectName);
}
async getBuildList(projectName: string, repoId: string, top: number) {
if (this.logger) {
this.logger.debug(
`Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`,
);
}
const client = await this.webApi.getBuildApi();
return client.getBuilds(
projectName,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
top,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
repoId,
'TfsGit',
);
}
async getRepoBuilds(projectName: string, repoName: string, top: number) {
if (this.logger) {
this.logger.debug(
`Calling Azure DevOps REST API, getting up to ${top} Builds for Repository ${repoName} for Project ${projectName}`,
);
}
const gitRepository = await this.getGitRepository(projectName, repoName);
const buildList = await this.getBuildList(
projectName,
gitRepository.id as string,
top,
);
const repoBuilds: RepoBuild[] = buildList.map(build => {
return repoBuildFromBuild(build);
});
return repoBuilds;
}
}
export function repoBuildFromBuild(build: Build) {
return {
id: build.id,
title: [build.definition?.name, build.buildNumber]
.filter(Boolean)
.join(' - '),
link: build._links?.web.href,
status: build.status,
result: build.result,
queueTime: build.queueTime,
source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`,
};
}
@@ -0,0 +1,18 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { AzureDevOpsApi } from './AzureDevOpsApi';
export type { RepoBuild } from './types';
@@ -0,0 +1,30 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
export type RepoBuild = {
id?: number;
title: string;
link: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
source: string;
};
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { AzureDevOpsApi } from './api';
export type { RepoBuild } from './api';
export * from './service/router';
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,199 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import { AzureDevOpsApi } from '../api';
import { createRouter } from './router';
import { RepoBuild } from '../api/types';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
Build,
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
describe('createRouter', () => {
let azureDevOpsApi: jest.Mocked<AzureDevOpsApi>;
let app: express.Express;
beforeAll(async () => {
azureDevOpsApi = {
getGitRepository: jest.fn(),
getBuildList: jest.fn(),
getRepoBuilds: jest.fn(),
} as any;
const router = await createRouter({
azureDevOpsApi,
logger: getVoidLogger(),
config: new ConfigReader({
azureDevOps: {
token: 'foo',
host: 'host.com',
organization: 'myOrg',
top: 5,
},
}),
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /health', () => {
it('returns ok', async () => {
const response = await request(app).get('/health');
expect(response.status).toEqual(200);
expect(response.body).toEqual({
status: 'Healthy',
details: 'All required config has been provided',
});
});
});
describe('GET /repository/:projectName/:repoName', () => {
it('fetches a single repository', async () => {
const gitRepository: GitRepository = {
id: 'af4ae3af-e747-4129-9bbc-d1329f6b0998',
name: 'myRepo',
url: 'https://host.com/repo',
defaultBranch: 'refs/heads/develop',
sshUrl: 'ssh://host.com/repo',
webUrl: 'https://host.com/webRepo',
};
azureDevOpsApi.getGitRepository.mockResolvedValueOnce(gitRepository);
const response = await request(app).get('/repository/myProject/myRepo');
expect(azureDevOpsApi.getGitRepository).toHaveBeenCalledWith(
'myProject',
'myRepo',
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(gitRepository);
});
});
describe('GET /builds/:projectName/:repoId', () => {
it('fetches a list of builds', async () => {
const firstBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: undefined,
sourceBranch: 'refs/heads/develop',
sourceVersion: '9bedf67800b2923982bdf60c89c57ce6fd2d9a1c',
};
const secondBuild: Build = {
id: 2,
buildNumber: 'Build-2',
status: BuildStatus.InProgress,
result: BuildResult.None,
queueTime: undefined,
sourceBranch: 'refs/heads/develop',
sourceVersion: '13c988d4f15e06bcdd0b0af290086a3079cdadb0',
};
const thirdBuild: Build = {
id: 3,
buildNumber: 'Build-3',
status: BuildStatus.Completed,
result: BuildResult.PartiallySucceeded,
queueTime: undefined,
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b319c308600eab015a5d6529add21660dc1',
};
const builds: Build[] = [firstBuild, secondBuild, thirdBuild];
azureDevOpsApi.getBuildList.mockResolvedValueOnce(builds);
const response = await request(app)
.get('/builds/myProject/af4ae3af-e747-4129-9bbc-d1329f6b0998')
.query({ top: '40' });
expect(azureDevOpsApi.getBuildList).toHaveBeenCalledWith(
'myProject',
'af4ae3af-e747-4129-9bbc-d1329f6b0998',
40,
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(builds);
});
});
describe('GET /repo-builds/:projectName/:repoName', () => {
it('fetches a list of repo builds', async () => {
const firstRepoBuild: RepoBuild = {
id: 1,
title: 'My Build Definition - Build 1',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
status: BuildStatus.Completed,
result: BuildResult.PartiallySucceeded,
queueTime: undefined,
source: 'refs/heads/develop (f4f78b31)',
};
const secondRepoBuild: RepoBuild = {
id: 2,
title: 'My Build Definition - Build 2',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2',
status: BuildStatus.InProgress,
result: BuildResult.None,
queueTime: undefined,
source: 'refs/heads/develop (13c988d4)',
};
const thirdRepoBuild: RepoBuild = {
id: 3,
title: 'My Build Definition - Build 3',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3',
status: BuildStatus.Completed,
result: BuildResult.Succeeded,
queueTime: undefined,
source: 'refs/heads/develop (9bedf678)',
};
const repoBuilds: RepoBuild[] = [
firstRepoBuild,
secondRepoBuild,
thirdRepoBuild,
];
azureDevOpsApi.getRepoBuilds.mockResolvedValueOnce(repoBuilds);
const response = await request(app)
.get('/repo-builds/myProject/myRepo')
.query({ top: '50' });
expect(azureDevOpsApi.getRepoBuilds).toHaveBeenCalledWith(
'myProject',
'myRepo',
50,
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(repoBuilds);
});
});
});
@@ -0,0 +1,117 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { errorHandler } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
import { AzureDevOpsApi } from '../api';
const DEFAULT_TOP: number = 10;
export interface RouterOptions {
azureDevOpsApi?: AzureDevOpsApi;
logger: Logger;
config: Config;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger } = options;
const config = options.config.getConfig('azureDevOps');
const token: string = config.getString('token');
const host: string = config.getString('host');
const organization: string = config.getString('organization');
const authHandler = getPersonalAccessTokenHandler(token);
const webApi = new WebApi(`https://${host}/${organization}`, authHandler);
const azureDevOpsApi =
options.azureDevOpsApi || new AzureDevOpsApi(logger, webApi);
const router = Router();
router.use(express.json());
router.get('/health', (_, response) => {
let code: number = 200;
let status: string = '';
let details: string = '';
if (token && host && organization) {
code = 200;
status = 'Healthy';
details = 'All required config has been provided';
}
if (!token) {
code = 500;
status = 'Unhealthy';
details = 'Token is missing';
}
if (!host) {
code = 500;
status = 'Unhealthy';
details = 'Host is missing';
}
if (!organization) {
code = 500;
status = 'Unhealthy';
details = 'Organization is missing';
}
response.status(code).json({ status: status, details: details });
});
router.get('/repository/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const gitRepository = await azureDevOpsApi.getGitRepository(
projectName,
repoName,
);
res.status(200).json(gitRepository);
});
router.get('/builds/:projectName/:repoId', async (req, res) => {
const { projectName, repoId } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const buildList = await azureDevOpsApi.getBuildList(
projectName,
repoId,
top,
);
res.status(200).json(buildList);
});
router.get('/repo-builds/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const gitRepository = await azureDevOpsApi.getRepoBuilds(
projectName,
repoName,
top,
);
res.status(200).json(gitRepository);
});
router.use(errorHandler());
return router;
}
@@ -0,0 +1,57 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'azure-devops-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
config,
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/azure-devops', router);
if (options.enableCors) {
service = service.enableCors({ origin: 'http://localhost:3000' });
}
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -0,0 +1,17 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};