diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0aebb18a41..3df1031034 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/packages/app/package.json b/packages/app/package.json index 5d3e6534f6..2373887cc6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 3438c01abf..0c7dfd926e 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -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 ; case isCircleCIAvailable(entity): return ; + case isCloudbuildAvailable(entity): + return ; case isTravisCIAvailable(entity): return ; default: diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index e936354bd3..85c4977d67 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -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'; diff --git a/plugins/cloudbuild/.eslintrc.js b/plugins/cloudbuild/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/cloudbuild/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/cloudbuild/README.md b/plugins/cloudbuild/README.md new file mode 100644 index 0000000000..5f1f6519dc --- /dev/null +++ b/plugins/cloudbuild/README.md @@ -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. diff --git a/plugins/cloudbuild/dev/index.tsx b/plugins/cloudbuild/dev/index.tsx new file mode 100644 index 0000000000..264d6f801f --- /dev/null +++ b/plugins/cloudbuild/dev/index.tsx @@ -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(); diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json new file mode 100644 index 0000000000..3367bb6cca --- /dev/null +++ b/plugins/cloudbuild/package.json @@ -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" + ] +} diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts new file mode 100644 index 0000000000..5aecf970ef --- /dev/null +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -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({ + id: 'plugin.cloudbuild.service', + description: 'Used by the Cloudbuild plugin to make requests', +}); + +export type CloudbuildApi = { + listWorkflowRuns: (request: { + projectId: string; + }) => Promise; + getWorkflow: ({ + projectId, + id, + }: { + projectId: string; + id: string; + }) => Promise; + getWorkflowRun: ({ + projectId, + id, + }: { + projectId: string; + id: string; + }) => Promise; + reRunWorkflow: ({ + projectId, + runId, + }: { + projectId: string; + runId: string; + }) => Promise; +}; diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts new file mode 100644 index 0000000000..0c3223aa04 --- /dev/null +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + // 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', + ); + } +} diff --git a/plugins/cloudbuild/src/api/index.ts b/plugins/cloudbuild/src/api/index.ts new file mode 100644 index 0000000000..643eecb818 --- /dev/null +++ b/plugins/cloudbuild/src/api/index.ts @@ -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'; diff --git a/plugins/cloudbuild/src/api/types.ts b/plugins/cloudbuild/src/api/types.ts new file mode 100644 index 0000000000..7c64fb52b8 --- /dev/null +++ b/plugins/cloudbuild/src/api/types.ts @@ -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; +} diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx new file mode 100644 index 0000000000..bbfb0b59c3 --- /dev/null +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -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({ + 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 Couldn't fetch latest {branch} run; + if (loading) return ; + return ( + + + + ), + message: lastRun.message, + url: ( + + See more on Google{' '} + + + ), + }} + /> + ); +}; + +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 ( + + + + ); +}; + +export const LatestWorkflowsForBranchCard = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => ( + + + +); diff --git a/plugins/cloudbuild/src/components/Cards/index.ts b/plugins/cloudbuild/src/components/Cards/index.ts new file mode 100644 index 0000000000..8c987ea1d5 --- /dev/null +++ b/plugins/cloudbuild/src/components/Cards/index.ts @@ -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'; diff --git a/plugins/cloudbuild/src/components/Router.tsx b/plugins/cloudbuild/src/components/Router.tsx new file mode 100644 index 0000000000..b2653a05f2 --- /dev/null +++ b/plugins/cloudbuild/src/components/Router.tsx @@ -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) ? ( + +
{CLOUDBUILD_ANNOTATION}
annotation is missing on the entity. +
+ ) : ( + + } + /> + } + /> + ) + + ); diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx new file mode 100644 index 0000000000..94c6fa4d25 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -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 => ({ + 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 ( + + Failed to load build, {error.message}. + + ); + } else if (loading) { + return ; + } else if (details.value?.logUrl === undefined) { + return ; + } + + const serviceAccount = qs.parse(new URL(details.value?.logUrl).search, { + ignoreQueryPrefix: true, + }).project; + + return ( +
+ + Workflow runs + Workflow run details + + + + + + + Branch + + {details.value?.substitutions.BRANCH_NAME} + + + + Message + + {details.value?.substitutions.REPO_NAME} + + + + Commit ID + + {details.value?.substitutions.COMMIT_SHA} + + + + Status + + + + + + + + Service Account + + + {`${serviceAccount}`}@cloudbuild.gserviceaccount.com + + + + + Links + + + {details.value?.logUrl && ( + + Workflow runs on Google{' '} + + + )} + + + +
+
+
+ ); +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts new file mode 100644 index 0000000000..2886a26740 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts @@ -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'; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts new file mode 100644 index 0000000000..2150e01d98 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts @@ -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; +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts new file mode 100644 index 0000000000..ae5e750d15 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts @@ -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; +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx b/plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx new file mode 100644 index 0000000000..45a8fc662c --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx @@ -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 ( + <> + Queued + + ); + case 'working': + return ( + <> + In progress + + ); + case 'success': + return ( + <> + Completed + + ); + case 'cancelled': + return ( + <> + Cancelled + + ); + case 'failure': + return ( + <> + Failed + + ); + default: + return ( + <> + Pending + + ); + } +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts b/plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts new file mode 100644 index 0000000000..8ebca32cbd --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts @@ -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'; diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx new file mode 100644 index 0000000000..254e6f89ed --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -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) => ( + + + + ), + }, + { + title: 'Build', + field: 'id', + type: 'numeric', + width: '150px', + render: (row: Partial) => ( + +

{row.id?.substring(0, 8)}

+
+ ), + }, + { + title: 'Source', + field: 'source', + highlight: true, + width: '200px', + render: (row: Partial) => ( + + {row.message} + + ), + }, + { + title: 'Ref', + render: (row: Partial) => ( + +

{row.substitutions?.BRANCH_NAME}

+
+ ), + }, + { + title: 'Commit', + render: (row: Partial) => ( + +

{row.substitutions?.SHORT_SHA}

+
+ ), + }, + { + title: 'Created', + render: (row: Partial) => ( + +

{moment(row.createTime).format('DD-MM-YYYY hh:mm:ss')}

+
+ ), + }, + { + title: 'Actions', + render: (row: Partial) => ( + + + + + + ), + 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 = ({ + projectName, + loading, + pageSize, + page, + retry, + runs, + onChangePage, + onChangePageSize, + total, +}) => { + return ( + , + tooltip: 'Reload workflow runs', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={runs ?? []} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} + style={{ width: '100%' }} + title={ + + + + {projectName} + + } + 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 ( + + ); +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts b/plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts new file mode 100644 index 0000000000..4e21cc67c2 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts @@ -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'; diff --git a/plugins/cloudbuild/src/components/useProjectName.ts b/plugins/cloudbuild/src/components/useProjectName.ts new file mode 100644 index 0000000000..0c227f690d --- /dev/null +++ b/plugins/cloudbuild/src/components/useProjectName.ts @@ -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 }; +}; diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts new file mode 100644 index 0000000000..b6d6c2c620 --- /dev/null +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -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; +} diff --git a/plugins/cloudbuild/src/index.ts b/plugins/cloudbuild/src/index.ts new file mode 100644 index 0000000000..616bb6b4e7 --- /dev/null +++ b/plugins/cloudbuild/src/index.ts @@ -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'; diff --git a/plugins/cloudbuild/src/plugin.test.ts b/plugins/cloudbuild/src/plugin.test.ts new file mode 100644 index 0000000000..dae16f1d1b --- /dev/null +++ b/plugins/cloudbuild/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/cloudbuild/src/plugin.ts b/plugins/cloudbuild/src/plugin.ts new file mode 100644 index 0000000000..ee997cc7ec --- /dev/null +++ b/plugins/cloudbuild/src/plugin.ts @@ -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); + }, + }), + ], +}); diff --git a/plugins/cloudbuild/src/setupTests.ts b/plugins/cloudbuild/src/setupTests.ts new file mode 100644 index 0000000000..4b4cdbdaaf --- /dev/null +++ b/plugins/cloudbuild/src/setupTests.ts @@ -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();