diff --git a/.changeset/silent-needles-attack.md b/.changeset/silent-needles-attack.md new file mode 100644 index 0000000000..7ead2b674e --- /dev/null +++ b/.changeset/silent-needles-attack.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Added deprecation warnings for `v1alpha1` templates diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index e8aa76a63a..9ca2eef9e6 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -64,120 +64,6 @@ const mockUrlReader = UrlReaders.default({ config: new ConfigReader({}), }); -describe('createRouter - working directory', () => { - const mockPrepare = jest.fn(); - const mockPreparers = new Preparers(); - - beforeAll(() => { - const mockPreparer = { - prepare: mockPrepare, - }; - mockPreparers.register('dev.azure.com', mockPreparer); - }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - const workDirConfig = (path: string) => ({ - backend: { - workingDirectory: path, - }, - }); - - const template = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - 'backstage.io/managed-by-location': 'url:https://dev.azure.com', - }, - }, - spec: { - owner: 'template@backstage.io', - path: '.', - schema: {}, - }, - }; - - it('should throw an error when working directory does not exist or is not writable', async () => { - mockAccess.mockImplementation(() => { - throw new Error('access error'); - }); - - await expect( - createRouter({ - logger: getVoidLogger(), - preparers: new Preparers(), - templaters: new Templaters(), - publishers: new Publishers(), - config: new ConfigReader(workDirConfig('/path')), - database: createDatabase(), - catalogClient: createCatalogClient([template]), - reader: mockUrlReader, - }), - ).rejects.toThrow('access error'); - }); - - it('should use the working directory when configured', async () => { - const router = await createRouter({ - logger: getVoidLogger(), - preparers: mockPreparers, - templaters: new Templaters(), - publishers: new Publishers(), - config: new ConfigReader(workDirConfig('/path')), - database: createDatabase(), - catalogClient: createCatalogClient([template]), - reader: mockUrlReader, - }); - - const app = express().use(router); - await request(app) - .post('/v1/jobs') - .send({ - templateName: '', - values: { - storePath: 'https://github.com/backstage/good', - }, - }); - - expect(mockPrepare).toBeCalledWith({ - logger: expect.anything(), - workspacePath: expect.stringContaining('path'), - url: expect.anything(), - }); - }); - - it('should not pass along anything when no working directory is configured', async () => { - const router = await createRouter({ - logger: getVoidLogger(), - preparers: mockPreparers, - templaters: new Templaters(), - publishers: new Publishers(), - config: new ConfigReader({}), - database: createDatabase(), - catalogClient: createCatalogClient([template]), - reader: mockUrlReader, - }); - - const app = express().use(router); - await request(app) - .post('/v1/jobs') - .send({ - templateName: '', - values: { - storePath: 'https://github.com/backstage/goodrepo', - }, - }); - - expect(mockPrepare).toBeCalledWith({ - logger: expect.anything(), - workspacePath: expect.anything(), - url: expect.anything(), - }); - }); -}); - describe('createRouter', () => { let app: express.Express; const template = { @@ -239,21 +125,6 @@ describe('createRouter', () => { jest.resetAllMocks(); }); - describe('POST /v1/jobs', () => { - it('rejects template values which do not match the template schema definition', async () => { - const response = await request(app) - .post('/v1/jobs') - .send({ - templateName: '', - values: { - storePath: 'https://github.com/backstage/backstage', - }, - }); - - expect(response.status).toEqual(400); - }); - }); - describe('GET /v2/actions', () => { it('lists available actions', async () => { const response = await request(app).get('/v2/actions').send(); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5e0790526e..dc8e69eb46 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -16,22 +16,16 @@ import { Config } from '@backstage/config'; import express from 'express'; -import { resolve as resolvePath, dirname } from 'path'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { - JobProcessor, PreparerBuilder, TemplaterBuilder, TemplaterValues, PublisherBuilder, - parseLocationAnnotation, - joinGitUrlPath, - FilePreparer, } from '../scaffolder'; import { CatalogEntityClient } from '../lib/catalog'; -import { validate, ValidatorResult } from 'jsonschema'; -import parseGitUrl from 'git-url-parse'; +import { validate } from 'jsonschema'; import { DatabaseTaskStore, StorageTaskBroker, @@ -101,7 +95,6 @@ export async function createRouter( const logger = parentLogger.child({ plugin: 'scaffolder' }); const workingDirectory = await getWorkingDirectory(config, logger); - const jobProcessor = await JobProcessor.fromConfig({ config, logger }); const entityClient = new CatalogEntityClient(catalogClient); const integrations = ScmIntegrations.fromConfig(config); @@ -137,134 +130,6 @@ export async function createRouter( worker.start(); - router - .get('/v1/job/:jobId', ({ params }, res) => { - const job = jobProcessor.get(params.jobId); - - if (!job) { - res.status(404).json({ error: 'job not found' }); - return; - } - - res.json({ - id: job.id, - metadata: { - ...job.context, - logger: undefined, - logStream: undefined, - }, - status: job.status, - stages: job.stages.map(stage => ({ - ...stage, - handler: undefined, - })), - error: job.error, - }); - }) - .post('/v1/jobs', async (req, res) => { - const templateName: string = req.body.templateName; - const values: TemplaterValues = { - ...req.body.values, - destination: { - git: parseGitUrl(req.body.values.storePath), - }, - }; - - // Forward authorization from client - const template = await entityClient.findTemplate(templateName, { - token: getBearerToken(req.headers.authorization), - }); - if (!isAlpha1Template(template)) { - throw new InputError( - `This endpoint does not support templates with version ${template.apiVersion}`, - ); - } - - const validationResult: ValidatorResult = validate( - values, - template.spec.schema, - ); - - if (!validationResult.valid) { - res.status(400).json({ errors: validationResult.errors }); - return; - } - - const job = jobProcessor.create({ - entity: template, - values, - stages: [ - { - name: 'Prepare the skeleton', - async handler(ctx) { - const { - protocol, - location: templateEntityLocation, - } = parseLocationAnnotation(ctx.entity); - - if (protocol === 'file') { - const preparer = new FilePreparer(); - - const path = resolvePath( - dirname(templateEntityLocation), - template.spec.path || '.', - ); - - await preparer.prepare({ - url: `file://${path}`, - logger: ctx.logger, - workspacePath: ctx.workspacePath, - }); - return; - } - - const preparer = preparers.get(templateEntityLocation); - - const url = joinGitUrlPath( - templateEntityLocation, - template.spec.path, - ); - - await preparer.prepare({ - url, - logger: ctx.logger, - workspacePath: ctx.workspacePath, - }); - }, - }, - { - name: 'Run the templater', - async handler(ctx) { - const templater = templaters.get(ctx.entity.spec.templater); - await templater.run({ - workspacePath: ctx.workspacePath, - logStream: ctx.logStream, - values: ctx.values, - }); - }, - }, - { - name: 'Publish template', - handler: async ctx => { - const publisher = publishers.get(ctx.values.storePath); - ctx.logger.info('Will now store the template'); - const result = await publisher.publish({ - values: ctx.values, - workspacePath: ctx.workspacePath, - logger: ctx.logger, - }); - return result; - }, - }, - ], - }); - - jobProcessor.run(job); - - res.status(201).json({ id: job.id }); - }); - - // NOTE: The v2 API is unstable router .get( '/v2/templates/:namespace/:kind/:name/parameter-schema', @@ -359,8 +224,11 @@ export async function createRouter( let taskSpec; if (isAlpha1Template(template)) { - const result = validate(values, template.spec.schema); + logger.warn( + `[DEPRECATION] - Template: ${template.metadata.name} has version ${template.apiVersion} which is going to be deprecated. Please refer to https://backstage.io/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2 for help on migrating`, + ); + const result = validate(values, template.spec.schema); if (!result.valid) { res.status(400).json({ errors: result.errors }); return; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 4a482402f8..bc1bcefe1e 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -175,7 +175,10 @@ export const ScaffolderPageContents = () => { {matchingEntities && matchingEntities?.length > 0 && matchingEntities.map(template => ( - + ))} diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 6da0e3eaa2..bb56f7fb6e 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -22,11 +22,14 @@ import { CardContent, CardMedia, Chip, + Link, makeStyles, + Tooltip, Typography, useTheme, } from '@material-ui/core'; import React from 'react'; +import WarningIcon from '@material-ui/icons/Warning'; import { generatePath } from 'react-router'; import { rootRouteRef } from '../../routes'; import { @@ -66,8 +69,21 @@ const useStyles = makeStyles(theme => ({ }, })); +const useDeprecationStyles = makeStyles(theme => ({ + deprecationIcon: { + position: 'absolute', + top: theme.spacing(0.5), + right: theme.spacing(3.5), + padding: '0.25rem', + }, + link: { + color: theme.palette.warning.light, + }, +})); + export type TemplateCardProps = { template: TemplateEntityV1alpha1; + deprecated?: boolean; }; type TemplateProps = { @@ -91,7 +107,30 @@ const getTemplateCardProps = ( }; }; -export const TemplateCard = ({ template }: TemplateCardProps) => { +const DeprecationWarning = () => { + const styles = useDeprecationStyles(); + + const Title = ( + + This template syntax is deprecated. Click for more info. + + ); + + return ( +
+ + + + + +
+ ); +}; + +export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { const backstageTheme = useTheme(); const rootLink = useRouteRef(rootRouteRef); const templateProps = getTemplateCardProps(template); @@ -110,6 +149,7 @@ export const TemplateCard = ({ template }: TemplateCardProps) => { + {deprecated && }