Merge pull request #1594 from spotify/blam/tidy-up-scaffolder

Use the Templater Property in the Template Definition to choose the templater
This commit is contained in:
Ben Lambert
2020-07-13 14:00:21 +02:00
committed by GitHub
17 changed files with 268 additions and 26 deletions
+1
View File
@@ -29,6 +29,7 @@
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.13",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.13",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.13",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
"knex": "^0.21.1",
+18 -3
View File
@@ -20,19 +20,34 @@ import {
FilePreparer,
GithubPreparer,
Preparers,
GithubPublisher,
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import { Octokit } from '@octokit/rest';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger }: PluginEnvironment) {
const templater = new CookieCutter();
const cookiecutterTemplater = new CookieCutter();
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
const filePreparer = new FilePreparer();
const githubPreparer = new GithubPreparer();
const preparers = new Preparers();
const dockerClient = new Docker();
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
return await createRouter({ preparers, templater, logger, dockerClient });
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
const dockerClient = new Docker();
return await createRouter({
preparers,
templaters,
publisher,
logger,
dockerClient,
});
}
@@ -32,7 +32,8 @@ describe('TemplateEntityV1alpah1', () => {
name: 'test',
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
@@ -78,7 +79,7 @@ describe('TemplateEntityV1alpah1', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('acceptps any other type', async () => {
it('accepts any other type', async () => {
(entity as any).spec.type = 'hallo';
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
@@ -87,4 +88,9 @@ describe('TemplateEntityV1alpah1', () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects missing templater', async () => {
(entity as any).spec.templater = '';
await expect(policy.enforce(entity)).rejects.toThrow(/templater/);
});
});
@@ -26,6 +26,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
kind: typeof KIND;
spec: {
type: string;
templater: string;
path?: string;
schema: JSONSchema;
};
@@ -43,6 +44,7 @@ export class TemplateEntityV1alpha1Policy implements EntityPolicy {
type: yup.string().required().min(1),
path: yup.string(),
schema: yup.object().required(),
templater: yup.string().required(),
})
.required(),
});
@@ -8,7 +8,7 @@ metadata:
- Recommended
- React
spec:
processor: cookiecutter
templater: cookiecutter
type: website
path: '.'
schema:
@@ -8,7 +8,7 @@ metadata:
- Recommended
- Java
spec:
processor: cookiecutter
templater: cookiecutter
type: service
path: '.'
schema:
@@ -37,7 +37,8 @@ describe('JobProcessor', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -47,7 +47,8 @@ describe('GitHubPreparer', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -39,7 +39,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -86,7 +87,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -131,7 +133,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -177,7 +180,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -221,7 +225,8 @@ describe('Helpers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -24,7 +24,7 @@ describe('Preparers', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/Users/blam/dev/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
'file:/Users/bingo/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
},
name: 'react-ssr-template',
title: 'React SSR Template',
@@ -35,8 +35,9 @@ describe('Preparers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
templater: 'cookiecutter',
path: '.',
type: 'website',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
@@ -88,7 +89,8 @@ describe('Preparers', () => {
generation: 1,
},
spec: {
type: 'cookiecutter',
type: 'website',
templater: 'cookiecutter',
path: '.',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './github';
export * from './types';
@@ -16,6 +16,8 @@
import { Writable, PassThrough } from 'stream';
import Docker from 'dockerode';
import fs from 'fs';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { InputError } from '@backstage/backend-common';
export type RunDockerContainerOptions = {
imageName: string;
@@ -26,6 +28,20 @@ export type RunDockerContainerOptions = {
dockerClient: Docker;
};
/**
* Gets the templater key to use for templating from the entity
* @param entity Template entity
*/
export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => {
const { templater } = entity.spec;
if (!templater) {
throw new InputError('Template does not have a required templating key');
}
return templater;
};
/**
*
* @param options the options object
@@ -16,3 +16,4 @@
export * from './cookiecutter';
export * from './types';
export * from './helpers';
export * from './templaters';
@@ -0,0 +1,125 @@
/*
* 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 { Templaters } from '.';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { CookieCutter } from './cookiecutter';
describe('Templaters', () => {
const mockTemplate: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/Users/bingo/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
},
name: 'react-ssr-template',
title: 'React SSR Template',
description:
'Next.js application skeleton for creating isomorphic web applications.',
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
generation: 1,
},
spec: {
templater: 'cookiecutter',
path: '.',
type: 'website',
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 templater is not registered', () => {
const templaters = new Templaters();
expect(() => templaters.get(mockTemplate)).toThrow(
expect.objectContaining({
message: 'No templater registered for template: "cookiecutter"',
}),
);
});
it('should return the correct templater when the templater matches', () => {
const templaters = new Templaters();
const templater = new CookieCutter();
templaters.register('cookiecutter', templater);
expect(templaters.get(mockTemplate)).toBe(templater);
});
it('should throw an error if the templater does not exist in the entity', () => {
const brokenTemplate: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {},
name: 'react-ssr-template',
title: 'React SSR Template',
description:
'Next.js application skeleton for creating isomorphic web applications.',
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
generation: 1,
},
spec: {
type: 'website',
path: '.',
templater: '',
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',
},
},
},
},
};
const templaters = new Templaters();
expect(() => templaters.get(brokenTemplate)).toThrow(
expect.objectContaining({
name: 'InputError',
message: expect.stringContaining(
'Template does not have a required templating key',
),
}),
);
});
});
@@ -0,0 +1,45 @@
/*
* 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 {
TemplaterBase,
SupportedTemplatingKey,
TemplaterBuilder,
} from './types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { getTemplaterKey } from './helpers';
export class Templaters implements TemplaterBuilder {
private preparerMap = new Map<SupportedTemplatingKey, TemplaterBase>();
register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase) {
this.preparerMap.set(templaterKey, templater);
}
get(template: TemplateEntityV1alpha1): TemplaterBase {
const templaterKey = getTemplaterKey(template);
const preparer = this.preparerMap.get(templaterKey);
if (!preparer) {
throw new Error(
`No templater registered for template: "${templaterKey}"`,
);
}
return preparer;
}
}
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Writable } from 'stream';
import Docker from 'dockerode';
import { JsonValue } from '@backstage/config';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
/**
* Currently the required template values. The owner
@@ -55,3 +55,16 @@ export type TemplaterBase = {
export type TemplaterConfig = {
templater?: TemplaterBase;
};
/**
* List of supported templating options
*/
export type SupportedTemplatingKey = 'cookiecutter' | string;
/**
* The templater builder holds the templaters ready for run time
*/
export type TemplaterBuilder = {
register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void;
get(template: TemplateEntityV1alpha1): TemplaterBase;
};
@@ -16,23 +16,24 @@
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
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 {
GithubPublisher,
JobProcessor,
PreparerBuilder,
RequiredTemplateValues,
StageContext,
TemplaterBase,
TemplaterBuilder,
Publisher,
} from '../scaffolder';
export interface RouterOptions {
preparers: PreparerBuilder;
templater: TemplaterBase;
templaters: TemplaterBuilder;
publisher: Publisher;
logger: Logger;
dockerClient: Docker;
}
@@ -42,9 +43,14 @@ export async function createRouter(
): Promise<express.Router> {
const router = Router();
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const { preparers, templater, logger: parentLogger, dockerClient } = options;
const githubPulisher = new GithubPublisher({ client: githubClient });
const {
preparers,
templaters,
publisher,
logger: parentLogger,
dockerClient,
} = options;
const logger = parentLogger.child({ plugin: 'scaffolder' });
const jobProcessor = new JobProcessor();
@@ -106,6 +112,7 @@ export async function createRouter(
{
name: 'Run the templater',
handler: async (ctx: StageContext<{ skeletonDir: string }>) => {
const templater = templaters.get(ctx.entity);
const { resultDir } = await templater.run({
directory: ctx.skeletonDir,
dockerClient,
@@ -120,7 +127,8 @@ export async function createRouter(
name: 'Publish template',
handler: async (ctx: StageContext<{ resultDir: string }>) => {
ctx.logger.info('Should not store the template');
const { remoteUrl } = await githubPulisher.publish({
const { remoteUrl } = await publisher.publish({
entity: ctx.entity,
values: ctx.values,
directory: ctx.resultDir,
});