Merge pull request #5731 from backstage/blam/deprecate-v1alpha1
Deprecating `v1alpha1` templates
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user