Skip to content

Commit

Permalink
feature: add crumbIssuer option to jenkins (optional) configuration, …
Browse files Browse the repository at this point in the history
…improve the UI to show a notification after executing the action: re-build

Signed-off-by: Hasan Ozdemir <21654050+nodify-at@users.noreply.github.com>
  • Loading branch information
nodify-at committed Dec 15, 2021
1 parent 1115165 commit eb3fd85
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-tigers-smash.md
@@ -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
5 changes: 5 additions & 0 deletions .changeset/twenty-tigers-ymash.md
@@ -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
4 changes: 4 additions & 0 deletions plugins/jenkins-backend/api-report.md
Expand Up @@ -52,6 +52,8 @@ export interface JenkinsInfo {
// (undocumented)
baseUrl: string;
// (undocumented)
crumbIssuer?: boolean;
// (undocumented)
headers?: Record<string, string | string[]>;
// (undocumented)
jobFullName: string;
Expand All @@ -77,6 +79,8 @@ export interface JenkinsInstanceConfig {
// (undocumented)
baseUrl: string;
// (undocumented)
crumbIssuer?: boolean;
// (undocumented)
name: string;
// (undocumented)
username: string;
Expand Down
13 changes: 13 additions & 0 deletions plugins/jenkins-backend/src/service/jenkinsApi.test.ts
Expand Up @@ -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);
});
});
1 change: 1 addition & 0 deletions plugins/jenkins-backend/src/service/jenkinsApi.ts
Expand Up @@ -146,6 +146,7 @@ export class JenkinsApiImpl {
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
crumbIssuer: jenkinsInfo.crumbIssuer,
}) as any;
}

Expand Down
Expand Up @@ -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=',
Expand Down
8 changes: 7 additions & 1 deletion plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts
Expand Up @@ -38,13 +38,15 @@ export interface JenkinsInfo {
baseUrl: string;
headers?: Record<string, string | string[]>;
jobFullName: string; // TODO: make this an array
crumbIssuer?: boolean;
}

export interface JenkinsInstanceConfig {
name: string;
baseUrl: string;
username: string;
apiKey: string;
crumbIssuer?: boolean;
}

/**
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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([
Expand Down Expand Up @@ -227,6 +232,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
Authorization: `Basic ${creds}`,
},
jobFullName,
crumbIssuer: instanceConfig.crumbIssuer,
};
}

Expand Down
8 changes: 4 additions & 4 deletions plugins/jenkins/api-report.md
Expand Up @@ -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';
Expand Down Expand Up @@ -72,7 +72,7 @@ export interface JenkinsApi {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<void>;
}): Promise<Response>;
}

// Warning: (ae-missing-release-tag) "jenkinsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -117,7 +117,7 @@ export class JenkinsClient implements JenkinsApi {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<void>;
}): Promise<Response>;
}

// Warning: (ae-missing-release-tag) "jenkinsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
25 changes: 15 additions & 10 deletions plugins/jenkins/src/api/JenkinsApi.ts
Expand Up @@ -19,7 +19,7 @@ import {
DiscoveryApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { EntityName, EntityRef } from '@backstage/catalog-model';
import type { EntityName, EntityRef } from '@backstage/catalog-model';

export const jenkinsApiRef = createApiRef<JenkinsApi>({
id: 'plugin.jenkins.service2',
Expand Down Expand Up @@ -66,7 +66,7 @@ export interface Project {
inQueue: string;
// added by us
status: string; // == inQueue ? 'queued' : lastBuild.building ? 'running' : lastBuild.result,
onRestartClick: () => Promise<void>; // TODO rename to handle.* ? also, should this be on lastBuild?
onRestartClick: () => Promise<Response>; // TODO rename to handle.* ? also, should this be on lastBuild?
}

export interface JenkinsApi {
Expand Down Expand Up @@ -106,7 +106,7 @@ export interface JenkinsApi {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<void>;
}): Promise<Response>;
}

export class JenkinsClient implements JenkinsApi {
Expand Down Expand Up @@ -140,7 +140,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: {
Expand All @@ -151,8 +151,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),
Expand All @@ -179,7 +179,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: {
Expand All @@ -198,7 +198,7 @@ export class JenkinsClient implements JenkinsApi {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<void> {
}): Promise<Response> {
const url = `${await this.discoveryApi.getBaseUrl(
'jenkins',
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
Expand All @@ -207,12 +207,17 @@ export class JenkinsClient implements JenkinsApi {
jobFullName,
)}/${encodeURIComponent(buildNumber)}:rebuild`;

const idToken = await this.identityApi.getIdToken();
await fetch(url, {
const idToken = await this.getToken();
return fetch(url, {
method: 'POST',
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
}

private async getToken() {
const { token } = await this.identityApi.getCredentials();
return token;
}
}
60 changes: 49 additions & 11 deletions plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx
Expand Up @@ -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) {
Expand Down Expand Up @@ -173,13 +173,51 @@ 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 {
const response = await row.onRestartClick();
const body = (await response.json()) as {
error?: { message: string };
};
if (response.status !== 200) {
alertApi.post({
message: `Jenkins re-build has been failed. Reason: ${body.error?.message}`,
severity: 'error',
});
} else {
alertApi.post({
message: 'Jenkins re-build has been successfully executed',
severity: 'success',
});
}
} finally {
setIsLoadingRebuild(false);
}
}
};

return (
<Tooltip title="Rerun build">
<>
{isLoadingRebuild && <Progress />}
{!isLoadingRebuild && (
<IconButton onClick={onRebuild}>
<RetryIcon />
</IconButton>
)}
</>
</Tooltip>
);
};
return <ActionWrapper />;
},
width: '10%',
},
];
Expand Down

0 comments on commit eb3fd85

Please sign in to comment.