Merge pull request #17597 from tsoslow/jenkins-customize-action

Configuration for action column in Jenkins table
This commit is contained in:
Patrik Oldsberg
2023-05-16 11:15:58 +02:00
committed by GitHub
8 changed files with 121 additions and 71 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(),
@@ -54,6 +55,11 @@ const fakePermissionApi = {
describe('JenkinsApi', () => {
const jenkinsApi = new JenkinsApiImpl(fakePermissionApi);
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
afterEach(() => {
jest.clearAllMocks();
});
describe('getProjects', () => {
const project: JenkinsProject = {
@@ -689,55 +695,63 @@ 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.mockResolvedValueOnce({ status: 200 } as Response);
const status = await jenkinsApi.rebuildProject(
jenkinsInfo,
jobFullName,
buildNumber,
resourceRef,
);
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.mockResolvedValueOnce({ status: 401 } as Response);
const status = await jenkinsApi.rebuildProject(
jenkinsInfo,
jobFullName,
buildNumber,
resourceRef,
);
expect(status).toEqual(401);
});
it('should fail if it does not have required permissions', async () => {
fakePermissionApi.authorize.mockResolvedValueOnce([
{
result: AuthorizeResult.DENY,
},
]);
mockFetch.mockResolvedValueOnce({ status: 200 } as Response);
const status = await jenkinsApi.rebuildProject(
jenkinsInfo,
jobFullName,
buildNumber,
resourceRef,
);
expect(status).toEqual(401);
});
expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
});
});
@@ -28,7 +28,7 @@ import {
PermissionEvaluator,
} 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,14 +144,13 @@ export class JenkinsApiImpl {
* Trigger a build of a project
* @see ../../../jenkins/src/api/JenkinsApi.ts#retry
*/
async buildProject(
async rebuildProject(
jenkinsInfo: JenkinsInfo,
jobFullName: string,
buildNumber: number,
resourceRef: string,
options?: { token?: string },
) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
): Promise<number> {
if (this.permissionApi) {
const response = await this.permissionApi.authorize(
[{ permission: jenkinsExecutePermission, resourceRef }],
@@ -160,16 +159,19 @@ export class JenkinsApiImpl {
// 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();
return 401;
}
}
// 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)
const buildUrl = this.getBuildUrl(jenkinsInfo, jobFullName, buildNumber);
// 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 +320,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}`;
}
}
+13 -6
View File
@@ -145,9 +145,10 @@ export async function createRouter(
);
router.post(
'/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber::rebuild',
'/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber',
async (request, response) => {
const { namespace, kind, name, jobFullName } = request.params;
const { namespace, kind, name, jobFullName, buildNumber } =
request.params;
const token = getBearerTokenFromAuthorizationHeader(
request.header('authorization'),
);
@@ -162,10 +163,16 @@ export async function createRouter(
});
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),
resourceRef,
{
token,
},
);
response.json({}).status(status);
},
);
router.use(errorHandler());