Update JenkinsAPI to replay build

This uses the Jenkins API directly because Jenkins SDK does not have support for replay

Signed-off-by: Tim Soslow <tsoslow@shutterstock.com>
This commit is contained in:
Tim Soslow
2023-05-09 19:26:49 -05:00
parent 17afe1a0bf
commit 8773027ae8
5 changed files with 72 additions and 78 deletions
+1
View File
@@ -36,6 +36,7 @@
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"jenkins": "^1.0.0",
"node-fetch": "^2.6.7",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
@@ -19,9 +19,10 @@ import jenkins from 'jenkins';
import { JenkinsInfo } from './jenkinsInfoProvider';
import { JenkinsBuild, JenkinsProject } from '../types';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { NotAllowedError } from '@backstage/errors';
import fetch, { Response } from 'node-fetch';
jest.mock('jenkins');
jest.mock('node-fetch');
const mockedJenkinsClient = {
job: {
get: jest.fn(),
@@ -34,7 +35,6 @@ const mockedJenkinsClient = {
const mockedJenkins = jenkins as jest.Mocked<any>;
mockedJenkins.mockReturnValue(mockedJenkinsClient);
const resourceRef = 'component:default/example-component';
const jobFullName = 'example-jobName/foo';
const buildNumber = 19;
const jenkinsInfo: JenkinsInfo = {
@@ -54,6 +54,7 @@ const fakePermissionApi = {
describe('JenkinsApi', () => {
const jenkinsApi = new JenkinsApiImpl(fakePermissionApi);
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
describe('getProjects', () => {
const project: JenkinsProject = {
@@ -684,55 +685,44 @@ describe('JenkinsApi', () => {
buildNumber,
);
});
it('buildProject', async () => {
await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef);
it('getBuildUrl', async () => {
const jenkinsApiProto = Object.getPrototypeOf(jenkinsApi);
const buildUrl = jenkinsApiProto.getBuildUrl(
jenkinsInfo,
jobFullName,
buildNumber,
);
expect(buildUrl).toEqual(
'https://jenkins.example.com/job/example-jobName/job/foo/19',
);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
const buildUrlTriple = jenkinsApiProto.getBuildUrl(
jenkinsInfo,
'example-jobName/foo/bar',
buildNumber,
);
expect(buildUrlTriple).toEqual(
'https://jenkins.example.com/job/example-jobName/job/foo/job/bar/19',
);
});
it('buildProject should fail if it does not have required permissions', async () => {
fakePermissionApi.authorize.mockResolvedValueOnce([
{
result: AuthorizeResult.DENY,
},
]);
await expect(() =>
jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef),
).rejects.toThrow(NotAllowedError);
});
it('buildProject should succeed if it have required permissions', async () => {
fakePermissionApi.authorize.mockResolvedValueOnce([
{
result: AuthorizeResult.ALLOW,
},
]);
await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
describe('rebuildProject', () => {
it('successfully rebuilds', async () => {
mockFetch.mockResolvedValue({ status: 200 } as Response);
const status = await jenkinsApi.rebuildProject(
jenkinsInfo,
jobFullName,
buildNumber,
);
expect(status).toEqual(200);
});
expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
});
it('buildProject with crumbIssuer option', async () => {
const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true };
await jenkinsApi.buildProject(info, jobFullName, resourceRef);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
crumbIssuer: true,
it('fails to rebuild', async () => {
mockFetch.mockResolvedValue({ status: 401 } as Response);
const status = await jenkinsApi.rebuildProject(
jenkinsInfo,
jobFullName,
buildNumber,
);
expect(status).toEqual(401);
});
expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
});
});
@@ -29,6 +29,7 @@ import {
} from '@backstage/plugin-permission-common';
import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common';
import { NotAllowedError } from '@backstage/errors';
import fetch, { HeaderInit } from 'node-fetch';
export class JenkinsApiImpl {
private static readonly lastBuildTreeSpec = `lastBuild[
@@ -144,32 +145,20 @@ export class JenkinsApiImpl {
* Trigger a build of a project
* @see ../../../jenkins/src/api/JenkinsApi.ts#retry
*/
async buildProject(
async rebuildProject(
jenkinsInfo: JenkinsInfo,
jobFullName: string,
resourceRef: string,
options?: { token?: string },
) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
buildNumber: number,
): Promise<number> {
const buildUrl = this.getBuildUrl(jenkinsInfo, jobFullName, buildNumber);
if (this.permissionApi) {
const response = await this.permissionApi.authorize(
[{ permission: jenkinsExecutePermission, resourceRef }],
{ token: options?.token },
);
// permission api returns always at least one item, we need to check only one result since we do not expect any additional results
const { result } = response[0];
if (result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
// looks like the current SDK only supports triggering a new build
// can't see any support for replay (re-running the specific build with the same SCM info)
// Note Jenkins itself has concepts of rebuild and replay on a job.
// The latter should be possible to trigger with a POST to /replay/rebuild
await client.job.build(jobFullName);
// the current SDK only supports triggering a new build
// replay the job by triggering request directly from Jenkins api
const response = await fetch(`${buildUrl}/replay/rebuild`, {
method: 'post',
headers: jenkinsInfo.headers as HeaderInit,
});
return response.status;
}
// private helper methods
@@ -318,4 +307,13 @@ export class JenkinsApiImpl {
})
.pop();
}
private getBuildUrl(
jenkinsInfo: JenkinsInfo,
jobFullName: string,
buildId: number,
): string {
const jobs = jobFullName.split('/');
return `${jenkinsInfo.baseUrl}/job/${jobs.join('/job/')}/${buildId}`;
}
}
@@ -147,7 +147,8 @@ export async function createRouter(
router.post(
'/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber::rebuild',
async (request, response) => {
const { namespace, kind, name, jobFullName } = request.params;
const { namespace, kind, name, jobFullName, buildNumber } =
request.params;
const token = getBearerTokenFromAuthorizationHeader(
request.header('authorization'),
);
@@ -161,11 +162,12 @@ export async function createRouter(
backstageToken: token,
});
const resourceRef = stringifyEntityRef({ kind, namespace, name });
await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef, {
token,
});
response.json({});
const status = await jenkinsApi.rebuildProject(
jenkinsInfo,
jobFullName,
parseInt(buildNumber, 10),
);
response.json({}).status(status);
},
);
router.use(errorHandler());