diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index f149b8c9b4..9469319b28 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -18,5 +18,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityPolicy } from './types'; +export type { EntityPolicy, JSONSchema } from './types'; export * from './validation'; diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts index a8cfe9f3ea..191c949c22 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts @@ -39,6 +39,22 @@ describe('JobProcessor', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 86377be6d0..e1f7bdc4a8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -50,6 +50,22 @@ describe('GitHubPreparer', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts index 6aa940783f..c5380fa5d1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts @@ -41,6 +41,22 @@ describe('Helpers', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; @@ -72,6 +88,22 @@ describe('Helpers', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; @@ -101,6 +133,22 @@ describe('Helpers', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; @@ -131,6 +179,22 @@ describe('Helpers', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; @@ -159,6 +223,22 @@ describe('Helpers', () => { spec: { type: 'cookiecutter', path: './template', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index a8f4b4ae99..9807c63c08 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -37,6 +37,22 @@ describe('Preparers', () => { spec: { type: 'cookiecutter', path: '.', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; it('should throw an error when the preparer for the source location is not registered', () => { @@ -74,6 +90,22 @@ describe('Preparers', () => { spec: { type: 'cookiecutter', path: '.', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, }, }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 1511a2b726..b370c23fa3 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,22 +14,22 @@ * limitations under the License. */ -import { Logger } from 'winston'; -import Router from 'express-promise-router'; -import express from 'express'; -import { - PreparerBuilder, - TemplaterBase, - JobProcessor, - RequiredTemplateValues, -} from '../scaffolder'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import Docker from 'dockerode'; -import { InputError } from '@backstage/backend-common'; -import { StageContext } from '../scaffolder/jobs/types'; -import { Octokit } from '@octokit/rest'; -import { GithubStorer } from '../scaffolder/stages/store/github'; import { JsonValue } from '@backstage/config'; +import { Octokit } from '@octokit/rest'; +import Docker from 'dockerode'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { + JobProcessor, + PreparerBuilder, + RequiredTemplateValues, + TemplaterBase, +} from '../scaffolder'; +import { StageContext } from '../scaffolder/jobs/types'; +import { GithubStorer } from '../scaffolder/stages/store/github'; + export interface RouterOptions { preparers: PreparerBuilder; templater: TemplaterBase; diff --git a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx index 4fb924e1f3..a2ee93c6f8 100644 --- a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -25,6 +25,7 @@ import { Header, Lifecycle, InfoCard, + errorApiRef, } from '@backstage/core'; import { TemplateEntityV1alpha1, @@ -36,6 +37,7 @@ import { scaffolderApiRef } from '../../api'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { Job } from '../JobStatusModal/types'; export const CreatePage = () => { + const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); @@ -68,16 +70,24 @@ export const CreatePage = () => { null, ); const handleCreateComplete = async (job: Job) => { + const componentYaml = job.metadata.remoteUrl?.replace( + /\.git$/, + '/blob/master/component-info.yaml', + ); + if (!componentYaml) { + errorApi.post( + new Error( + `Failed to find component-info.yaml file in ${job.metadata.remoteUrl}`, + ), + ); + return; + } + const { entities: [createdEntity], - } = await catalogApi.addLocation( - 'github', - job.metadata.remoteUrl.replace( - /\.git$/, - '/blob/master/component-info.yaml', - ), - ); - setEntity(createdEntity); + } = await catalogApi.addLocation('github', componentYaml); + + setEntity(createdEntity as ComponentEntityV1alpha1); }; if (!template && isValidating) return ; diff --git a/plugins/scaffolder/src/components/JobStatusModal/types.ts b/plugins/scaffolder/src/components/JobStatusModal/types.ts index 2ace1f760f..7106444904 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/types.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/types.ts @@ -1,23 +1,34 @@ -import { Writable } from 'stream'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; - +/* + * 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 type Job = { id: string; metadata: { entity: any; values: any; + remoteUrl?: string; }; - status: 'PENDING' | 'STARTED' | 'COMPLETE' | 'FAILED'; + status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; stages: Stage[]; - logStream?: Writable; - logger?: Logger; error?: Error; }; export type Stage = { name: string; log: string[]; - status: 'PENDING' | 'STARTED' | 'COMPLETE' | 'FAILED'; + status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; startedAt: string; - finishedAt?: string; + endedAt?: string; }; diff --git a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts index 89b7228a9c..4762838a9c 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts @@ -1,8 +1,24 @@ +/* + * 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, useEffect } from 'react'; import { Job } from './types'; import { useApi } from '@backstage/core'; import { scaffolderApiRef } from '../../api'; +const DEFAULT_POLLING_INTERVAL = 1000; const poll = (thunk: () => Promise, ms: number) => { let shouldStop = false; (async () => { @@ -17,12 +33,15 @@ const poll = (thunk: () => Promise, ms: number) => { }; }; -export const useJobPolling = (jobId: string | null) => { +export const useJobPolling = ( + jobId: string | null, + pollingInterval = DEFAULT_POLLING_INTERVAL, +) => { const scaffolderApi = useApi(scaffolderApiRef); + const [job, setJob] = useState(null); - const [job, setJob] = useState(undefined); useEffect(() => { - if (!jobId) return; + if (!jobId) return () => {}; const stopPolling = poll(async () => { const nextJobState = await scaffolderApi.getJob(jobId); if ( @@ -32,10 +51,11 @@ export const useJobPolling = (jobId: string | null) => { stopPolling(); } setJob(nextJobState); - }, 500); + }, pollingInterval); return () => { stopPolling(); }; - }, [jobId, setJob]); + }, [jobId, setJob, scaffolderApi, pollingInterval]); + return job; }; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 7e8f470fd9..03c513a437 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -1,5 +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. + */ import React, { useState } from 'react'; -import { withTheme, FormProps, IChangeEvent } from '@rjsf/core'; +import { withTheme, IChangeEvent, FormProps } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import { Stepper, @@ -12,10 +27,11 @@ import { Box, } from '@material-ui/core'; import { Content, StructuredMetadataTable } from '@backstage/core'; +import { JSONSchema } from '@backstage/catalog-model'; const Form = withTheme(MuiTheme); type Step = { - schema: FormProps['schema']; + schema: JSONSchema; label: string; }; type Props = { @@ -52,7 +68,7 @@ export const MultistepJsonForm = ({ noHtml5Validate formData={formData} onChange={onChange} - schema={schema} + schema={schema as FormProps['schema']} onSubmit={e => { if (e.errors.length === 0) handleNext(); }}