From 641e01514ce221673e245ebefac20d39f26a207c Mon Sep 17 00:00:00 2001 From: Marek Calus Date: Sun, 1 Nov 2020 17:49:44 +0100 Subject: [PATCH] Add error handling to the catalog-import plugin --- .../src/api/CatalogImportApi.ts | 5 +- .../src/api/CatalogImportClient.ts | 153 +++++++++++++----- .../src/components/ComponentConfigDisplay.tsx | 19 ++- plugins/catalog-import/src/util/types.ts | 5 +- .../catalog-import/src/util/useGithubRepos.ts | 33 ++-- 5 files changed, 149 insertions(+), 66 deletions(-) diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 28749d02cb..aba921b883 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -28,12 +28,11 @@ export interface CatalogImportApi { owner: string; repo: string; fileContent: string; - }): Promise<{ errorMessage: string | null; link: string }>; + }): Promise<{ link: string }>; createRepositoryLocation(options: { - token: string; owner: string; repo: string; - }): Promise<{ errorMessage: string | null }>; + }): Promise; generateEntityDefinitions(options: { repo: string; }): Promise; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 2da0ff02a7..7300db45dd 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -45,7 +45,15 @@ export class CatalogImportClient implements CatalogImportApi { location: { type: 'github', target: repo }, }), }, - ); + ).catch(e => { + throw new Error(`Failed to generate entity definitions, ${e.message}`); + }); + if (!response.ok) { + throw new Error( + `Failed to generate entity definitions. Received http response ${response.status}: ${response.statusText}`, + ); + } + const payload = (await response.json()) as AnalyzeLocationResponse; return payload.generateEntities.map(x => x.entity); } @@ -54,22 +62,28 @@ export class CatalogImportClient implements CatalogImportApi { owner, repo, }: { - token: string; owner: string; repo: string; - }): Promise<{ errorMessage: string | null }> { - await fetch(`${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { - headers: { - 'Content-Type': 'application/json', + }): Promise { + const response = await fetch( + `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, + { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + type: 'github', + target: `https://github.com/${owner}/${repo}/blob/master/catalog-info.yaml`, + presence: 'optional', + }), }, - method: 'POST', - body: JSON.stringify({ - type: 'github', - target: `https://github.com/${owner}/${repo}/blob/master/catalog-info.yaml`, - presence: 'optional', - }), - }); - return { errorMessage: null }; + ); + if (!response.ok) { + throw new Error( + `Received http response ${response.status}: ${response.statusText}`, + ); + } } async submitPRToRepo({ @@ -82,38 +96,93 @@ export class CatalogImportClient implements CatalogImportApi { owner: string; repo: string; fileContent: string; - }): Promise<{ errorMessage: string | null; link: string }> { + }): Promise<{ link: string }> { const octo = new Octokit({ auth: token, }); - const parentRef = await octo.git.getRef({ - owner, - repo, - ref: 'heads/master', - }); - await octo.git.createRef({ - owner, - repo, - ref: 'refs/heads/backstage-integration', - sha: parentRef.data.object.sha, - }); - await octo.repos.createOrUpdateFileContents({ - owner, - repo, - path: 'catalog-info.yaml', - message: 'Add backstage.yaml config file', - content: btoa(fileContent), - branch: 'backstage-integration', - }); - const pullRequestRespone = await octo.pulls.create({ - owner, - repo, - title: 'Add catalog-info.yaml config file', - head: 'backstage-integration', - base: 'master', - }); + const branchName = 'backstage-integration'; + const fileName = 'catalog-info.yaml'; - return { errorMessage: null, link: pullRequestRespone.data.html_url }; + const repoData = await octo.repos + .get({ + owner, + repo, + }) + .catch(e => { + throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e)); + }); + + const parentRef = await octo.git + .getRef({ + owner, + repo, + ref: `heads/${repoData.data.default_branch}`, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage("Couldn't fetch default branch data", e), + ); + }); + + await octo.git + .createRef({ + owner, + repo, + ref: `refs/heads/${branchName}`, + sha: parentRef.data.object.sha, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage( + `Couldn't create a new branch with name '${branchName}'`, + e, + ), + ); + }); + + await octo.repos + .createOrUpdateFileContents({ + owner, + repo, + path: fileName, + message: `Add ${fileName} config file`, + content: btoa(fileContent), + branch: branchName, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage( + `Couldn't create a commit with ${fileName} file added`, + e, + ), + ); + }); + + const pullRequestRespone = await octo.pulls + .create({ + owner, + repo, + title: `Add ${fileName} config file`, + head: branchName, + base: repoData.data.default_branch, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage( + `Couldn't create a pull request for ${branchName} branch`, + e, + ), + ); + }); + + return { link: pullRequestRespone.data.html_url }; } } + +function formatHttpErrorMessage( + message: string, + error: { status: number; message: string }, +) { + return `${message}, received http response status code ${error.status}: ${error.message}`; +} diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx index 1fe1543bdd..a9a7ef118e 100644 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx @@ -19,6 +19,7 @@ import { Button, CircularProgress, Grid, Tooltip } from '@material-ui/core'; import Alert from '@material-ui/lab/Alert'; import { useGithubRepos } from '../util/useGithubRepos'; import { ConfigSpec } from './ImportComponentPage'; +import { errorApiRef, useApi } from '@backstage/core'; type Props = { nextStep: () => void; @@ -32,14 +33,20 @@ const ComponentConfigDisplay: React.FC = ({ savePRLink, }) => { const [submitting, setSubmitting] = useState(false); + const errorApi = useApi(errorApiRef); const { submitPRToRepo } = useGithubRepos(); const onNext = useCallback(async () => { - setSubmitting(true); - const result = await submitPRToRepo(configFile); - savePRLink(result.link); - setSubmitting(false); - nextStep(); - }, [submitPRToRepo, configFile, nextStep, savePRLink]); + try { + setSubmitting(true); + const result = await submitPRToRepo(configFile); + savePRLink(result.link); + setSubmitting(false); + nextStep(); + } catch (e) { + setSubmitting(false); + errorApi.post(e); + } + }, [submitPRToRepo, configFile, nextStep, savePRLink, errorApi]); return ( diff --git a/plugins/catalog-import/src/util/types.ts b/plugins/catalog-import/src/util/types.ts index e4ff9d6d04..f23649a2a9 100644 --- a/plugins/catalog-import/src/util/types.ts +++ b/plugins/catalog-import/src/util/types.ts @@ -1,5 +1,3 @@ -import { Entity } from '@backstage/catalog-model'; - /* * Copyright 2020 Spotify AB * @@ -15,6 +13,9 @@ import { Entity } from '@backstage/catalog-model'; * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Entity } from '@backstage/catalog-model'; + export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial[] diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index 16ccbf572e..9290d5e993 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -30,20 +30,27 @@ export function useGithubRepos() { selectedRepo.repo.split('/')[0], selectedRepo.repo.split('/')[1], ]; - const submitPRResponse = await api.submitPRToRepo({ - token, - owner: ownerName, - repo: repoName, - fileContent: selectedRepo.config - .map(entity => `---\n${YAML.stringify(entity)}`) - .join('\n'), - }); + const submitPRResponse = await api + .submitPRToRepo({ + token, + owner: ownerName, + repo: repoName, + fileContent: selectedRepo.config + .map(entity => `---\n${YAML.stringify(entity)}`) + .join('\n'), + }) + .catch(e => { + throw new Error(`Failed to submit PR to repo:\n${e.message}`); + }); - await api.createRepositoryLocation({ - token, - owner: selectedRepo.repo.split('/')[0], - repo: selectedRepo.repo.split('/')[1], - }); + await api + .createRepositoryLocation({ + owner: selectedRepo.repo.split('/')[0], + repo: selectedRepo.repo.split('/')[1], + }) + .catch(e => { + throw new Error(`Failed to create repository location:\n${e.message}`); + }); return submitPRResponse; };