Merge pull request #5731 from backstage/blam/deprecate-v1alpha1

Deprecating `v1alpha1` templates
This commit is contained in:
Ben Lambert
2021-05-20 12:21:55 +02:00
committed by GitHub
5 changed files with 56 additions and 268 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
Added deprecation warnings for `v1alpha1` templates
@@ -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();
@@ -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;
@@ -175,7 +175,10 @@ export const ScaffolderPageContents = () => {
{matchingEntities &&
matchingEntities?.length > 0 &&
matchingEntities.map(template => (
<TemplateCard template={template} />
<TemplateCard
template={template}
deprecated={template.apiVersion === 'backstage.io/v1alpha1'}
/>
))}
</ItemCardGrid>
</div>
@@ -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 = (
<Typography style={{ padding: 10, maxWidth: 300 }}>
This template syntax is deprecated. Click for more info.
</Typography>
);
return (
<div className={styles.deprecationIcon}>
<Tooltip title={Title}>
<Link
href="https://backstage.io/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2"
className={styles.link}
>
<WarningIcon />
</Link>
</Tooltip>
</div>
);
};
export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => {
const backstageTheme = useTheme<BackstageTheme>();
const rootLink = useRouteRef(rootRouteRef);
const templateProps = getTemplateCardProps(template);
@@ -110,6 +149,7 @@ export const TemplateCard = ({ template }: TemplateCardProps) => {
<Card>
<CardMedia className={classes.cardHeader}>
<FavouriteTemplate entity={template} />
{deprecated && <DeprecationWarning />}
<ItemCardHeader
title={templateProps.title}
subtitle={templateProps.type}