Merge pull request #2515 from ebarriosjr/cloudbuild-plugin

Cloudbuild plugin
This commit is contained in:
Ben Lambert
2020-09-29 13:49:11 +02:00
committed by GitHub
29 changed files with 1341 additions and 1 deletions
+1
View File
@@ -7,6 +7,7 @@
* @spotify/backstage-core
/docs/features/techdocs @spotify/techdocs-core
/plugins/cost-insights @spotify/silver-lining
/plugins/cloudbuild @trivago/ebarrios
/plugins/techdocs @spotify/techdocs-core
/plugins/techdocs-backend @spotify/techdocs-core
/packages/techdocs-cli @spotify/techdocs-core
+1
View File
@@ -10,6 +10,7 @@
"@backstage/plugin-api-docs": "^0.1.1-alpha.23",
"@backstage/plugin-catalog": "^0.1.1-alpha.23",
"@backstage/plugin-circleci": "^0.1.1-alpha.23",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.23",
"@backstage/plugin-cost-insights": "^0.1.1-alpha.23",
"@backstage/plugin-explore": "^0.1.1-alpha.23",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.23",
@@ -24,9 +24,13 @@ import {
Router as GitHubActionsRouter,
} from '@backstage/plugin-github-actions';
import {
Router as CloudbuildRouter,
isPluginApplicableToEntity as isCloudbuildAvailable,
} from '@backstage/plugin-cloudbuild';
import {
Router as JenkinsRouter,
isPluginApplicableToEntity as isJenkinsAvailable,
LatestRunCard as JenkinsLatestRunCard,
Router as JenkinsRouter,
} from '@backstage/plugin-jenkins';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
@@ -56,6 +60,8 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => {
return <GitHubActionsRouter entity={entity} />;
case isCircleCIAvailable(entity):
return <CircleCIRouter entity={entity} />;
case isCloudbuildAvailable(entity):
return <CloudbuildRouter entity={entity} />;
case isTravisCIAvailable(entity):
return <TravisCIRouter entity={entity} />;
default:
+1
View File
@@ -34,4 +34,5 @@ export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests';
export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects';
export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+13
View File
@@ -0,0 +1,13 @@
# Google Cloud Build
Welcome to the Google Cloud Build plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/cloudbuild](http://localhost:3000/cloudbuild).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
+57
View File
@@ -0,0 +1,57 @@
{
"name": "@backstage/plugin-cloudbuild",
"version": "0.1.1-alpha.23",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.23",
"@backstage/core": "^0.1.1-alpha.23",
"@backstage/plugin-catalog": "^0.1.1-alpha.23",
"@backstage/theme": "^0.1.1-alpha.23",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.0",
"@octokit/types": "^5.4.1",
"moment": "^2.27.0",
"qs": "^6.9.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-lazylog": "^4.5.3",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.23",
"@backstage/dev-utils": "^0.1.1-alpha.23",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"jest-fetch-mock": "^3.0.3",
"msw": "^0.20.5",
"node-fetch": "^2.6.1"
},
"files": [
"dist"
]
}
@@ -0,0 +1,53 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
} from '../api/types';
export const cloudbuildApiRef = createApiRef<CloudbuildApi>({
id: 'plugin.cloudbuild.service',
description: 'Used by the Cloudbuild plugin to make requests',
});
export type CloudbuildApi = {
listWorkflowRuns: (request: {
projectId: string;
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
getWorkflow: ({
projectId,
id,
}: {
projectId: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
getWorkflowRun: ({
projectId,
id,
}: {
projectId: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
reRunWorkflow: ({
projectId,
runId,
}: {
projectId: string;
runId: string;
}) => Promise<any>;
};
@@ -0,0 +1,122 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CloudbuildApi } from './CloudbuildApi';
import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
} from '../api/types';
import { OAuthApi } from '@backstage/core';
export class CloudbuildClient implements CloudbuildApi {
constructor(private readonly googleAuthApi: OAuthApi) {}
async reRunWorkflow({
projectId,
runId,
}: {
projectId: string;
runId: string;
}): Promise<void> {
await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
projectId,
)}/builds/${encodeURIComponent(runId)}:retry`,
{
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${await this.getToken()}`,
}),
},
);
}
async listWorkflowRuns({
projectId,
}: {
projectId: string;
}): Promise<ActionsListWorkflowRunsForRepoResponseData> {
const workflowRuns = await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
projectId,
)}/builds`,
{
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${await this.getToken()}`,
}),
},
);
const builds: ActionsListWorkflowRunsForRepoResponseData = await workflowRuns.json();
return builds;
}
async getWorkflow({
projectId,
id,
}: {
projectId: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData> {
const workflow = await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
projectId,
)}/builds/${encodeURIComponent(id)}`,
{
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${await this.getToken()}`,
}),
},
);
const build: ActionsGetWorkflowResponseData = await workflow.json();
return build;
}
async getWorkflowRun({
projectId,
id,
}: {
projectId: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData> {
const workflow = await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
projectId,
)}/builds/${encodeURIComponent(id)}`,
{
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${await this.getToken()}`,
}),
},
);
const build: ActionsGetWorkflowResponseData = await workflow.json();
return build;
}
async getToken(): Promise<string> {
// NOTE(freben - gcp-projects): There's a .read-only variant of this scope that we could
// use for readonly operations, but that means we would ask the user for a
// second auth during creation and I decided to keep the wider scope for
// all ops for now
return this.googleAuthApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './CloudbuildApi';
export * from './CloudbuildClient';
export * from './types';
+125
View File
@@ -0,0 +1,125 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface ActionsListWorkflowRunsForRepoResponseData {
builds: ActionsGetWorkflowResponseData[];
}
export type ActionsGetWorkflowResponseData = {
id: string;
status: string;
source: Source;
createTime: string;
startTime: string;
steps: Step[];
timeout: string;
projectId: string;
logsBucket: string;
sourceProvenance: SourceProvenance;
buildTriggerId: string;
options: Options;
logUrl: string;
substitutions: Substitutions;
tags: string[];
queueTtl: string;
name: string;
finishTime: any;
results: Results;
timing: Timing2;
};
export interface Step {
name: string;
args: string[];
id: string;
waitFor: string[];
entrypoint: string;
volumes: Volume[];
dir: string;
timing: Timing;
status: string;
pullTiming: PullTiming;
}
export interface Timing2 {
BUILD: BUILD;
FETCHSOURCE: FETCHSOURCE;
}
export interface SourceProvenance {
resolvedStorageSource: {};
fileHashes: {};
}
export interface Options {
machineType: string;
substitutionOption: string;
logging: string;
dynamicSubstitutions: boolean;
}
export interface Substitutions {
COMMIT_SHA: string;
SHORT_SHA: string;
BRANCH_NAME: string;
REPO_NAME: string;
REVISION_ID: string;
}
export interface Results {
buildStepImages: string[];
buildStepOutputs: string[];
}
export interface BUILD {
startTime: string;
endTime: string;
}
export interface FETCHSOURCE {
startTime: string;
endTime: string;
}
export interface StorageSource {
bucket: string;
object: string;
}
export interface Source {
storageSource: StorageSource;
}
export interface Volume {
name: string;
path: string;
}
export interface Timing {
startTime: string;
endTime: string;
}
export interface PullTiming {
startTime: string;
endTime: string;
}
export interface ResolvedStorageSource {
bucket: string;
object: string;
generation: string;
}
@@ -0,0 +1,116 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect } from 'react';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable';
import { Entity } from '@backstage/catalog-model';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core';
import {
InfoCard,
StructuredMetadataTable,
errorApiRef,
useApi,
WarningPanel,
} from '@backstage/core';
import ExternalLinkIcon from '@material-ui/icons/Launch';
import { CLOUDBUILD_ANNOTATION } from '../useProjectName';
const useStyles = makeStyles<Theme>({
externalLinkIcon: {
fontSize: 'inherit',
verticalAlign: 'bottom',
},
});
const WidgetContent = ({
error,
loading,
lastRun,
branch,
}: {
error?: Error;
loading?: boolean;
lastRun: WorkflowRun;
branch: string;
}) => {
const classes = useStyles();
if (error)
return <WarningPanel>Couldn't fetch latest {branch} run</WarningPanel>;
if (loading) return <LinearProgress />;
return (
<StructuredMetadataTable
metadata={{
status: (
<>
<WorkflowRunStatus status={lastRun.status} />
</>
),
message: lastRun.message,
url: (
<Link href={lastRun.googleUrl} target="_blank">
See more on Google{' '}
<ExternalLinkIcon className={classes.externalLinkIcon} />
</Link>
),
}}
/>
);
};
export const LatestWorkflowRunCard = ({
entity,
branch = 'master',
}: {
entity: Entity;
branch: string;
}) => {
const errorApi = useApi(errorApiRef);
const projectId = entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] || '';
const [{ runs, loading, error }] = useWorkflowRuns({
projectId,
});
const lastRun = runs?.[0] ?? ({} as WorkflowRun);
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error, errorApi]);
return (
<InfoCard title={`Last ${branch} build`}>
<WidgetContent
error={error}
loading={loading}
branch={branch}
lastRun={lastRun}
/>
</InfoCard>
);
};
export const LatestWorkflowsForBranchCard = ({
entity,
branch = 'master',
}: {
entity: Entity;
branch: string;
}) => (
<InfoCard title={`Last ${branch} build`}>
<WorkflowRunsTable entity={entity} />
</InfoCard>
);
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { LatestWorkflowRunCard, LatestWorkflowsForBranchCard } from './Cards';
@@ -0,0 +1,46 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { Routes, Route } from 'react-router';
import { rootRouteRef, buildRouteRef } from '../plugin';
import { WorkflowRunDetails } from './WorkflowRunDetails';
import { WorkflowRunsTable } from './WorkflowRunsTable';
import { CLOUDBUILD_ANNOTATION } from './useProjectName';
import { WarningPanel } from '@backstage/core';
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[CLOUDBUILD_ANNOTATION]);
export const Router = ({ entity }: { entity: Entity }) =>
// TODO(shmidt-i): move warning to a separate standardized component
!isPluginApplicableToEntity(entity) ? (
<WarningPanel title="Cloudbuild plugin:">
<pre>{CLOUDBUILD_ANNOTATION}</pre> annotation is missing on the entity.
</WarningPanel>
) : (
<Routes>
<Route
path={`/${rootRouteRef.path}`}
element={<WorkflowRunsTable entity={entity} />}
/>
<Route
path={`/${buildRouteRef.path}`}
element={<WorkflowRunDetails entity={entity} />}
/>
)
</Routes>
);
@@ -0,0 +1,148 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Link, WarningPanel } from '@backstage/core';
import {
Breadcrumbs,
LinearProgress,
Link as MaterialLink,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Theme,
Typography,
} from '@material-ui/core';
import ExternalLinkIcon from '@material-ui/icons/Launch';
import qs from 'qs';
import React from 'react';
import { useProjectName } from '../useProjectName';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
const useStyles = makeStyles<Theme>(theme => ({
root: {
maxWidth: 720,
margin: theme.spacing(2),
},
title: {
padding: theme.spacing(1, 0, 2, 0),
},
table: {
padding: theme.spacing(1),
},
accordionDetails: {
padding: 0,
},
button: {
order: -1,
marginRight: 0,
marginLeft: '-20px',
},
externalLinkIcon: {
fontSize: 'inherit',
verticalAlign: 'bottom',
},
}));
export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
const { value: projectName, loading, error } = useProjectName(entity);
const [projectId] = (projectName ?? '/').split('/');
const details = useWorkflowRunsDetails(projectId);
const classes = useStyles();
if (error) {
return (
<WarningPanel title="Error:">
Failed to load build, {error.message}.
</WarningPanel>
);
} else if (loading) {
return <LinearProgress />;
} else if (details.value?.logUrl === undefined) {
return <LinearProgress />;
}
const serviceAccount = qs.parse(new URL(details.value?.logUrl).search, {
ignoreQueryPrefix: true,
}).project;
return (
<div className={classes.root}>
<Breadcrumbs aria-label="breadcrumb">
<Link to="..">Workflow runs</Link>
<Typography>Workflow run details</Typography>
</Breadcrumbs>
<TableContainer component={Paper} className={classes.table}>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Branch</Typography>
</TableCell>
<TableCell>{details.value?.substitutions.BRANCH_NAME}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>{details.value?.substitutions.REPO_NAME}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{details.value?.substitutions.COMMIT_SHA}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>
<WorkflowRunStatus status={details.value?.status} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Service Account</Typography>
</TableCell>
<TableCell>
{`${serviceAccount}`}@cloudbuild.gserviceaccount.com
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Links</Typography>
</TableCell>
<TableCell>
{details.value?.logUrl && (
<MaterialLink target="_blank" href={details.value.logUrl}>
Workflow runs on Google{' '}
<ExternalLinkIcon className={classes.externalLinkIcon} />
</MaterialLink>
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WorkflowRunDetails } from './WorkflowRunDetails';
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useAsync } from 'react-use';
import { ActionsListWorkflowRunsForRepoResponseData } from '../../api/types';
export const useWorkflowRunJobs = (jobsUrl?: string) => {
const jobs = useAsync(async (): Promise<
ActionsListWorkflowRunsForRepoResponseData
> => {
if (jobsUrl === undefined) {
return {
builds: [],
};
}
const data = await fetch(jobsUrl).then(d => d.json());
return data;
}, [jobsUrl]);
return jobs;
};
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useApi } from '@backstage/core';
import { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { cloudbuildApiRef } from '../../api';
export const useWorkflowRunsDetails = (projectId: string) => {
const api = useApi(cloudbuildApiRef);
const { id } = useParams();
const details = useAsync(async () => {
return projectId
? api.getWorkflowRun({
projectId,
id: id,
})
: Promise.reject('No projectId provided');
}, [projectId, id]);
return details;
};
@@ -0,0 +1,70 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
StatusPending,
StatusRunning,
StatusOK,
StatusAborted,
StatusError,
} from '@backstage/core';
import React from 'react';
export const WorkflowRunStatus = ({
status,
}: {
status: string | undefined;
}) => {
if (status === undefined) return null;
switch (status.toLowerCase()) {
case 'queued':
return (
<>
<StatusPending /> Queued
</>
);
case 'working':
return (
<>
<StatusRunning /> In progress
</>
);
case 'success':
return (
<>
<StatusOK /> Completed
</>
);
case 'cancelled':
return (
<>
<StatusAborted /> Cancelled
</>
);
case 'failure':
return (
<>
<StatusError /> Failed
</>
);
default:
return (
<>
<StatusPending /> Pending
</>
);
}
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WorkflowRunStatus } from './WorkflowRunStatus';
@@ -0,0 +1,185 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core';
import RetryIcon from '@material-ui/icons/Replay';
import GoogleIcon from '@material-ui/icons/CloudCircle';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { Table, TableColumn } from '@backstage/core';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import SyncIcon from '@material-ui/icons/Sync';
import { useProjectName } from '../useProjectName';
import { Entity } from '@backstage/catalog-model';
import { Substitutions } from '../../api/types';
import { buildRouteRef } from '../../plugin';
import moment from 'moment';
export type WorkflowRun = {
id: string;
message: string;
url?: string;
googleUrl?: string;
status: string;
substitutions: Substitutions;
createTime: string;
rerun: () => void;
};
const generatedColumns: TableColumn[] = [
{
title: 'Status',
width: '150px',
render: (row: Partial<WorkflowRun>) => (
<Box display="flex" alignItems="center">
<WorkflowRunStatus status={row.status} />
</Box>
),
},
{
title: 'Build',
field: 'id',
type: 'numeric',
width: '150px',
render: (row: Partial<WorkflowRun>) => (
<Typography variant="body2" noWrap>
<p>{row.id?.substring(0, 8)}</p>
</Typography>
),
},
{
title: 'Source',
field: 'source',
highlight: true,
width: '200px',
render: (row: Partial<WorkflowRun>) => (
<Link
component={RouterLink}
to={generatePath(buildRouteRef.path, { id: row.id! })}
>
{row.message}
</Link>
),
},
{
title: 'Ref',
render: (row: Partial<WorkflowRun>) => (
<Typography variant="body2" noWrap>
<p>{row.substitutions?.BRANCH_NAME}</p>
</Typography>
),
},
{
title: 'Commit',
render: (row: Partial<WorkflowRun>) => (
<Typography variant="body2" noWrap>
<p>{row.substitutions?.SHORT_SHA}</p>
</Typography>
),
},
{
title: 'Created',
render: (row: Partial<WorkflowRun>) => (
<Typography variant="body2" noWrap>
<p>{moment(row.createTime).format('DD-MM-YYYY hh:mm:ss')}</p>
</Typography>
),
},
{
title: 'Actions',
render: (row: Partial<WorkflowRun>) => (
<Tooltip title="Rerun workflow">
<IconButton onClick={row.rerun}>
<RetryIcon />
</IconButton>
</Tooltip>
),
width: '10%',
},
];
type Props = {
loading: boolean;
retry: () => void;
runs?: WorkflowRun[];
projectName: string;
page: number;
onChangePage: (page: number) => void;
total: number;
pageSize: number;
onChangePageSize: (pageSize: number) => void;
};
export const WorkflowRunsTableView: FC<Props> = ({
projectName,
loading,
pageSize,
page,
retry,
runs,
onChangePage,
onChangePageSize,
total,
}) => {
return (
<Table
isLoading={loading}
options={{ paging: true, pageSize, padding: 'dense' }}
totalCount={total}
page={page}
actions={[
{
icon: () => <SyncIcon />,
tooltip: 'Reload workflow runs',
isFreeAction: true,
onClick: () => retry(),
},
]}
data={runs ?? []}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangePageSize}
style={{ width: '100%' }}
title={
<Box display="flex" alignItems="center">
<GoogleIcon />
<Box mr={1} />
<Typography variant="h6">{projectName}</Typography>
</Box>
}
columns={generatedColumns}
/>
);
};
export const WorkflowRunsTable = ({ entity }: { entity: Entity }) => {
const { value: projectName, loading } = useProjectName(entity);
const [projectId] = (projectName ?? '/').split('/');
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
projectId,
});
return (
<WorkflowRunsTableView
{...tableProps}
loading={loading || tableProps.loading}
retry={retry}
onChangePageSize={setPageSize}
onChangePage={setPage}
/>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WorkflowRunsTable, WorkflowRunsTableView } from './WorkflowRunsTable';
export type { WorkflowRun } from './WorkflowRunsTable';
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useAsync } from 'react-use';
import { Entity } from '@backstage/catalog-model';
export const CLOUDBUILD_ANNOTATION = 'google.com/cloudbuild-project-slug';
export const useProjectName = (entity: Entity) => {
const { value, loading, error } = useAsync(async () => {
return entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] ?? '';
});
return { value, loading, error };
};
@@ -0,0 +1,91 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { useAsyncRetry } from 'react-use';
import { WorkflowRun } from './WorkflowRunsTable/WorkflowRunsTable';
import { cloudbuildApiRef } from '../api/CloudbuildApi';
import { useApi, errorApiRef } from '@backstage/core';
import { ActionsListWorkflowRunsForRepoResponseData } from '../api/types';
export function useWorkflowRuns({ projectId }: { projectId: string }) {
const api = useApi(cloudbuildApiRef);
const errorApi = useApi(errorApiRef);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(5);
const { loading, value: runs, retry, error } = useAsyncRetry<
WorkflowRun[]
>(async () => {
return api
.listWorkflowRuns({
projectId,
})
.then(
(
workflowRunsData: ActionsListWorkflowRunsForRepoResponseData,
): WorkflowRun[] => {
setTotal(workflowRunsData.builds.length);
// Transformation here
return workflowRunsData.builds.map(run => ({
message: run.substitutions.REPO_NAME,
id: run.id,
rerun: async () => {
try {
await api.reRunWorkflow({
projectId,
runId: run.id,
});
} catch (e) {
errorApi.post(e);
}
},
substitutions: run.substitutions,
source: {
branchName: run.substitutions.REPO_NAME,
commit: {
hash: run.substitutions.COMMIT_SHA,
url: run.substitutions.REPO_NAME,
},
},
status: run.status,
url: run.logUrl,
googleUrl: run.logUrl,
createTime: run.createTime,
}));
},
);
}, [page, pageSize, projectId]);
return [
{
page,
pageSize,
loading,
runs,
projectName: `${projectId}`,
total,
error,
},
{
runs,
setPage,
setPageSize,
retry,
},
] as const;
}
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin } from './plugin';
export * from './api';
export { Router, isPluginApplicableToEntity } from './components/Router';
export * from './components/Cards';
export { CLOUDBUILD_ANNOTATION } from './components/useProjectName';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { plugin } from './plugin';
describe('cloudbuild', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createPlugin,
createRouteRef,
createApiFactory,
googleAuthApiRef,
} from '@backstage/core';
import { cloudbuildApiRef, CloudbuildClient } from './api';
export const rootRouteRef = createRouteRef({
path: '',
title: 'Google Cloudbuild',
});
export const buildRouteRef = createRouteRef({
path: ':id',
title: 'Cloudbuild Run',
});
export const plugin = createPlugin({
id: 'cloudbuild',
apis: [
createApiFactory({
api: cloudbuildApiRef,
deps: { googleAuthApi: googleAuthApiRef },
factory({ googleAuthApi }) {
return new CloudbuildClient(googleAuthApi);
},
}),
],
});
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
require('jest-fetch-mock').enableMocks();