Merge pull request #8500 from nodify-at/master
feature: add crumbIssuer option to jenkins and improved the UI
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
---
|
||||
|
||||
feature: add crumbIssuer option to Jenkins (optional) configuration, improve the UI to show a notification after executing the action re-build
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins': patch
|
||||
---
|
||||
|
||||
feature: add crumbIssuer option to Jenkins (optional) configuration, improve the UI to show a notification after executing the action re-build
|
||||
@@ -52,6 +52,8 @@ export interface JenkinsInfo {
|
||||
// (undocumented)
|
||||
baseUrl: string;
|
||||
// (undocumented)
|
||||
crumbIssuer?: boolean;
|
||||
// (undocumented)
|
||||
headers?: Record<string, string | string[]>;
|
||||
// (undocumented)
|
||||
jobFullName: string;
|
||||
@@ -77,6 +79,8 @@ export interface JenkinsInstanceConfig {
|
||||
// (undocumented)
|
||||
baseUrl: string;
|
||||
// (undocumented)
|
||||
crumbIssuer?: boolean;
|
||||
// (undocumented)
|
||||
name: string;
|
||||
// (undocumented)
|
||||
username: string;
|
||||
|
||||
@@ -411,4 +411,17 @@ describe('JenkinsApi', () => {
|
||||
});
|
||||
expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName);
|
||||
});
|
||||
|
||||
it('buildProject with crumbIssuer option', async () => {
|
||||
const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true };
|
||||
await jenkinsApi.buildProject(info, jobFullName);
|
||||
|
||||
expect(mockedJenkins).toHaveBeenCalledWith({
|
||||
baseUrl: jenkinsInfo.baseUrl,
|
||||
headers: jenkinsInfo.headers,
|
||||
promisify: true,
|
||||
crumbIssuer: true,
|
||||
});
|
||||
expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,6 +146,7 @@ export class JenkinsApiImpl {
|
||||
baseUrl: jenkinsInfo.baseUrl,
|
||||
headers: jenkinsInfo.headers,
|
||||
promisify: true,
|
||||
crumbIssuer: jenkinsInfo.crumbIssuer,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,7 @@ describe('DefaultJenkinsInfoProvider', () => {
|
||||
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
|
||||
expect(info).toStrictEqual({
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
crumbIssuer: undefined,
|
||||
headers: {
|
||||
Authorization:
|
||||
'Basic YmFja3N0YWdlIC0gYm90OjEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNlZGYwMTI=',
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface JenkinsInfo {
|
||||
baseUrl: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
jobFullName: string; // TODO: make this an array
|
||||
crumbIssuer?: boolean;
|
||||
}
|
||||
|
||||
export interface JenkinsInstanceConfig {
|
||||
@@ -45,6 +46,7 @@ export interface JenkinsInstanceConfig {
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
apiKey: string;
|
||||
crumbIssuer?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,6 +72,7 @@ export class JenkinsConfig {
|
||||
baseUrl: c.getString('baseUrl'),
|
||||
username: c.getString('username'),
|
||||
apiKey: c.getString('apiKey'),
|
||||
crumbIssuer: c.getOptionalBoolean('crumbIssuer'),
|
||||
})) || [];
|
||||
|
||||
// load unnamed default config
|
||||
@@ -81,6 +84,7 @@ export class JenkinsConfig {
|
||||
const baseUrl = jenkinsConfig.getOptionalString('baseUrl');
|
||||
const username = jenkinsConfig.getOptionalString('username');
|
||||
const apiKey = jenkinsConfig.getOptionalString('apiKey');
|
||||
const crumbIssuer = jenkinsConfig.getOptionalBoolean('crumbIssuer');
|
||||
|
||||
if (hasNamedDefault && (baseUrl || username || apiKey)) {
|
||||
throw new Error(
|
||||
@@ -98,12 +102,13 @@ export class JenkinsConfig {
|
||||
|
||||
if (unnamedAllPresent) {
|
||||
const unnamedInstanceConfig = [
|
||||
{ name: DEFAULT_JENKINS_NAME, baseUrl, username, apiKey },
|
||||
{ name: DEFAULT_JENKINS_NAME, baseUrl, username, apiKey, crumbIssuer },
|
||||
] as {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
apiKey: string;
|
||||
crumbIssuer: boolean;
|
||||
}[];
|
||||
|
||||
return new JenkinsConfig([
|
||||
@@ -227,6 +232,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
|
||||
Authorization: `Basic ${creds}`,
|
||||
},
|
||||
jobFullName,
|
||||
crumbIssuer: instanceConfig.crumbIssuer,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { EntityRef } from '@backstage/catalog-model';
|
||||
import type { EntityName } from '@backstage/catalog-model';
|
||||
import type { EntityRef } from '@backstage/catalog-model';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { InfoCardVariants } from '@backstage/core-components';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@backstage/catalog-model": "^0.9.7",
|
||||
"@backstage/core-components": "^0.8.1",
|
||||
"@backstage/core-plugin-api": "^0.3.1",
|
||||
"@backstage/errors": "^0.1.5",
|
||||
"@backstage/plugin-catalog-react": "^0.6.5",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { EntityName, EntityRef } from '@backstage/catalog-model';
|
||||
import type { EntityName, EntityRef } from '@backstage/catalog-model';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
export const jenkinsApiRef = createApiRef<JenkinsApi>({
|
||||
id: 'plugin.jenkins.service2',
|
||||
@@ -140,7 +141,7 @@ export class JenkinsClient implements JenkinsApi {
|
||||
url.searchParams.append('branch', filter.branch);
|
||||
}
|
||||
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const idToken = await this.getToken();
|
||||
const response = await fetch(url.href, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -151,8 +152,8 @@ export class JenkinsClient implements JenkinsApi {
|
||||
return (
|
||||
(await response.json()).projects?.map((p: Project) => ({
|
||||
...p,
|
||||
onRestartClick: async () => {
|
||||
await this.retry({
|
||||
onRestartClick: () => {
|
||||
return this.retry({
|
||||
entity,
|
||||
jobFullName: p.fullName,
|
||||
buildNumber: String(p.lastBuild.number),
|
||||
@@ -179,7 +180,7 @@ export class JenkinsClient implements JenkinsApi {
|
||||
jobFullName,
|
||||
)}/${encodeURIComponent(buildNumber)}`;
|
||||
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const idToken = await this.getToken();
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -207,12 +208,21 @@ export class JenkinsClient implements JenkinsApi {
|
||||
jobFullName,
|
||||
)}/${encodeURIComponent(buildNumber)}:rebuild`;
|
||||
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
await fetch(url, {
|
||||
const idToken = await this.getToken();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
private async getToken() {
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Box, IconButton, Link, Typography, Tooltip } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, IconButton, Link, Tooltip, Typography } from '@material-ui/core';
|
||||
import RetryIcon from '@material-ui/icons/Replay';
|
||||
import JenkinsLogo from '../../../../assets/JenkinsLogo.svg';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { JenkinsRunStatus } from '../Status';
|
||||
import { useBuilds } from '../../../useBuilds';
|
||||
import { buildRouteRef } from '../../../../plugin';
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { Progress, Table, TableColumn } from '@backstage/core-components';
|
||||
import { Project } from '../../../../api/JenkinsApi';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
const FailCount = ({ count }: { count: number }): JSX.Element | null => {
|
||||
if (count !== 0) {
|
||||
@@ -173,13 +173,46 @@ const generatedColumns: TableColumn[] = [
|
||||
{
|
||||
title: 'Actions',
|
||||
sorting: false,
|
||||
render: (row: Partial<Project>) => (
|
||||
<Tooltip title="Rerun build">
|
||||
<IconButton onClick={row.onRestartClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
render: (row: Partial<Project>) => {
|
||||
const ActionWrapper = () => {
|
||||
const [isLoadingRebuild, setIsLoadingRebuild] = useState(false);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
|
||||
const onRebuild = async () => {
|
||||
if (row.onRestartClick) {
|
||||
setIsLoadingRebuild(true);
|
||||
try {
|
||||
await row.onRestartClick();
|
||||
alertApi.post({
|
||||
message: 'Jenkins re-build has successfully executed',
|
||||
severity: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
alertApi.post({
|
||||
message: `Jenkins re-build has failed. Error: ${e.message}`,
|
||||
severity: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingRebuild(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title="Rerun build">
|
||||
<>
|
||||
{isLoadingRebuild && <Progress />}
|
||||
{!isLoadingRebuild && (
|
||||
<IconButton onClick={onRebuild}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
return <ActionWrapper />;
|
||||
},
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user