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> Signed-off-by: Christopher Diaz <cdiaz@rvohealth.com>
This commit is contained in:
committed by
Christopher Diaz
parent
cc2a06d745
commit
5cda5e9338
@@ -1,5 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins-backend': minor
|
||||
'@backstage/plugin-jenkins': minor
|
||||
---
|
||||
|
||||
Updated action column in Jenkins CI/CD table to include links to view and replay. The API based rebuild action was replaced because it does not work for Jenkins workflows that have parameters.
|
||||
Updated rebuild to use Jenkins API replay build, which works for Jenkins jobs that have required parameters. Jenkins SDK could not be used for this request because it does not have support for replay.
|
||||
|
||||
Added link to view build in Jenkins CI/CD table action column.
|
||||
|
||||
@@ -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 = {
|
||||
@@ -689,55 +690,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());
|
||||
|
||||
Reference in New Issue
Block a user