Merge pull request #1371 from spotify/mob/prepare-from-catalog

File Preparer for the Scaffolder using the Catalog Template Entity
This commit is contained in:
Ben Lambert
2020-06-22 16:50:30 +02:00
committed by GitHub
52 changed files with 445 additions and 436 deletions
+6 -4
View File
@@ -156,10 +156,12 @@ Integrators also configure closed source plugins locally from the monorepo.
We chose GitHub because it is the tool that we are most familiar with, so that
will naturally lead to integrations for GitHub being developed at an early
stage. Hosting this project on GitHub does not exclude integrations with
alternatives, such as [GitLab](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+GitLab) or Bitbucket. We believe that in time there will be
plugins that will provide functionality for these tools as well. Hopefully,
contributed by the community! Also note, implementations of Backstage can be
hosted wherever you feel suits your needs best.
alternatives, such as
[GitLab](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+GitLab)
or Bitbucket. We believe that in time there will be plugins that will provide
functionality for these tools as well. Hopefully, contributed by the community!
Also note, implementations of Backstage can be hosted wherever you feel suits
your needs best.
### Who maintains Backstage?
+7 -3
View File
@@ -17,13 +17,17 @@
import {
CookieCutter,
createRouter,
DiskStorage,
FilePreparer,
Preparers,
} from '@backstage/plugin-scaffolder-backend';
import type { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
const storage = new DiskStorage({ logger });
const templater = new CookieCutter();
const filePreparer = new FilePreparer();
const preparers = new Preparers();
return await createRouter({ storage, templater, logger });
preparers.register('file', filePreparer);
return await createRouter({ preparers, templater, logger });
}
@@ -26,6 +26,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
kind: typeof KIND;
spec: {
type: string;
path?: string;
};
}
@@ -39,6 +40,7 @@ export class TemplateEntityV1alpha1Policy implements EntityPolicy {
spec: yup
.object({
type: yup.string().required().min(1),
path: yup.string(),
})
.required(),
});
@@ -13,22 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getRootLogger } from '@backstage/backend-common';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3004;
const enableCors = process.env.PLUGIN_CORS
? Boolean(process.env.PLUGIN_CORS)
: false;
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch((err) => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
console.warn('here!');
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Template
metadata:
name: react-ssr-template
title: React SSR Template
description: Next.js application skeleton for creating isomorphic web applications.
spec:
type: cookiecutter
path: ./template-1
@@ -0,0 +1,16 @@
/*
* 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.
*/
console.warn('here!');
@@ -0,0 +1,8 @@
apiVersion: backstage.io/v1alpha1
kind: Template
metadata:
name: react-ssr-template
title: React SSR Template
description: Next.js application skeleton for creating isomorphic web applications.
spec:
type: cookiecutter
+3 -1
View File
@@ -22,6 +22,7 @@
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.9",
"@backstage/catalog-model": "^0.1.1-alpha.9",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -38,7 +39,8 @@
"@backstage/cli": "^0.1.1-alpha.9",
"@types/fs-extra": "^9.0.1",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2"
"supertest": "^4.0.2",
"yaml": "^1.10.0"
},
"files": [
"dist/**/*.{js,d.ts}"
@@ -9,4 +9,4 @@ sed -i -e "s/__component_id__/{{ cookiecutter.component_id }}/g" package.json
mv ../../node_modules.tmp ../../\{\{cookiecutter.component_id\}\}/node_modules 2>/dev/null ||:
# move back the build directory that was moved out in the pre_gen hook (if it exists)
mv ../../build.tmp ../../\{\{cookiecutter.component_id\}\}/build 2>/dev/null ||:
mv ../../build.tmp ../../\{\{cookiecutter.component_id\}\}/build 2>/dev/null ||:
@@ -1,6 +0,0 @@
{
"id": "react-ssr-template",
"name": "SSR React Website",
"description": "Next.js application skeleton for creating isomorphic web applications.",
"ownerId": "something"
}
@@ -13,8 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './storage';
export * from './templater';
export * from './storage/disk';
export * from './prepare';
export * from './templater/cookiecutter';
@@ -0,0 +1,65 @@
/*
* 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 fs from 'fs-extra';
import YAML from 'yaml';
import { FilePreparer } from './file';
import path from 'path';
import {
TemplateEntityV1alpha1,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
const setupTest = async (fixturePath: string) => {
const locationForTemplateYaml = path.resolve(
path.dirname(
require.resolve('@backstage/plugin-scaffolder-backend/package'),
),
'fixtures',
fixturePath,
);
const [parsedDocument] = YAML.parseAllDocuments(
await fs.readFile(locationForTemplateYaml, 'utf-8'),
);
const template: TemplateEntityV1alpha1 = parsedDocument.toJSON();
template.metadata.annotations = {
[LOCATION_ANNOTATION]: `file:${locationForTemplateYaml}`,
};
const filePreparer = new FilePreparer();
const resultDir = await filePreparer.prepare(template);
return { filePreparer, template, resultDir };
};
describe('File preparer', () => {
it('excludes the yaml file from the temp folder', async () => {
const { resultDir } = await setupTest('test-simple-template/template.yaml');
expect(fs.existsSync(`${resultDir}/template.yaml`)).toBe(false);
});
it('resolves relative path from the template', async () => {
const { resultDir } = await setupTest('test-simple-template/template.yaml');
expect(fs.existsSync(`${resultDir}/expected_file.ts`)).toBe(true);
});
it('resolves relative path from the nested template', async () => {
const { resultDir } = await setupTest('test-nested-template/template.yaml');
expect(fs.existsSync(`${resultDir}/expected_file.ts`)).toBe(true);
});
});
@@ -0,0 +1,50 @@
/*
* 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 fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from './helpers';
import { InputError } from '@backstage/backend-common';
import { PreparerBase } from './types';
export class FilePreparer implements PreparerBase {
async prepare(template: TemplateEntityV1alpha1): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
if (protocol !== 'file') {
throw new InputError(
`Wrong location protocol: ${protocol}, should be 'file'`,
);
}
const templateId = template.metadata.name;
const tempDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), templateId),
);
const parentDirectory = path.resolve(
path.dirname(location),
template.spec.path ?? '.',
);
await fs.copy(parentDirectory, tempDir, {
filter: src => src !== location,
});
return tempDir;
}
}
@@ -0,0 +1,56 @@
/*
* 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 {
TemplateEntityV1alpha1,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { InputError } from '@backstage/backend-common';
import { RemoteProtocol } from './types';
export type ParsedLocationAnnotation = {
protocol: RemoteProtocol;
location: string;
};
export const parseLocationAnnotation = (
entity: TemplateEntityV1alpha1,
): ParsedLocationAnnotation => {
const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION];
if (!annotation) {
throw new InputError(
`No location annotation provided in entity: ${entity.metadata.name}`,
);
}
// split on the first colon for the protocol and the rest after the first split
// is the location.
const [protocol, location] = annotation.split(/:(.+)/) as [
RemoteProtocol?,
string?,
];
if (!protocol || !location) {
throw new InputError(
`Failure to parse either protocol or location for entity: ${entity.metadata.name}`,
);
}
return {
protocol,
location,
};
};
@@ -0,0 +1,19 @@
/*
* 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 * from './preparers';
export * from './types';
export * from './helpers';
export * from './file';
@@ -0,0 +1,88 @@
/*
* 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 { Preparers } from '.';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { FilePreparer } from './file';
describe('Preparers', () => {
const mockTemplate: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/Users/blam/dev/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: {
type: 'cookiecutter',
path: '.',
},
};
it('should throw an error when the preparer for the source location is not registered', () => {
const preparers = new Preparers();
expect(() => preparers.get(mockTemplate)).toThrow(
expect.objectContaining({
message: 'No preparer registered for type: "file"',
}),
);
});
it('should return the correct preparer when the source matches', () => {
const preparers = new Preparers();
const preparer = new FilePreparer();
preparers.register('file', preparer);
expect(preparers.get(mockTemplate)).toBe(preparer);
});
it('should throw an error if the metadata tag 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: 'cookiecutter',
path: '.',
},
};
const preparers = new Preparers();
expect(() => preparers.get(brokenTemplate)).toThrow(
expect.objectContaining({
message: expect.stringContaining('No location annotation provided'),
}),
);
});
});
@@ -0,0 +1,38 @@
/*
* 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 { PreparerBase, RemoteProtocol, PreparerBuilder } from './types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from './helpers';
export class Preparers implements PreparerBuilder {
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
register(protocol: RemoteProtocol, preparer: PreparerBase) {
this.preparerMap.set(protocol, preparer);
}
get(template: TemplateEntityV1alpha1): PreparerBase {
const { protocol } = parseLocationAnnotation(template);
const preparer = this.preparerMap.get(protocol);
if (!preparer) {
throw new Error(`No preparer registered for type: "${protocol}"`);
}
return preparer;
}
}
@@ -0,0 +1,32 @@
/*
* 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 type { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
export type PreparerBase = {
/**
* Given an Entity definition from the Service Catalog, go and prepare a directory
* with contents from the remote location in temporary storage and return the path
* @param template The template entity from the Service Catalog
*/
prepare(template: TemplateEntityV1alpha1): Promise<string>;
};
export type PreparerBuilder = {
register(protocol: RemoteProtocol, preparer: PreparerBase): void;
get(template: TemplateEntityV1alpha1): PreparerBase;
};
export type RemoteProtocol = 'file';
@@ -1,84 +0,0 @@
/*
* 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 { DiskStorage } from './disk';
import * as path from 'path';
describe('Disk Storage', () => {
it('should load a simple template from a simple directory', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-simple-template-dir',
);
const templateInfo = require(`${testTemplateDir}/mock-template/template-info.json`);
const repository = new DiskStorage({ directory: testTemplateDir });
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(1);
expect(templates[0].id).toBe(templateInfo.id);
expect(templates[0].name).toBe(templateInfo.name);
expect(templates[0].description).toBe(templateInfo.description);
expect(templates[0].ownerId).toBe(templateInfo.ownerId);
});
it('should successfully load multiple templates from the same folder', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-multiple-templates-dir',
);
const repository = new DiskStorage({ directory: testTemplateDir });
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(2);
});
it('should return empty array when there are no templates', async () => {
const testTemplateDir = path.resolve(
__dirname,
'/some-folder-that-deffo-does-not-exist',
);
const repository = new DiskStorage({ directory: testTemplateDir });
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(0);
});
it('should be able to handle templates with invalid json and ignore them from the returned array', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-failing-template-dir',
);
const repository = new DiskStorage({ directory: testTemplateDir });
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(1);
});
});
@@ -1,99 +0,0 @@
/*
* 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 globby from 'globby';
import fs from 'fs-extra';
import { Template, StorageBase as Base } from '.';
import { Logger } from 'winston';
interface DiskIndexEntry {
contents: Template;
location: string;
}
export class DiskStorage implements Base {
private repository: Template[] = [];
private localIndex: DiskIndexEntry[] = [];
private repoDir: string;
private logger?: Logger;
constructor({
directory = `${__dirname}/../../../sample-templates`,
logger,
}: {
directory?: string;
logger?: Logger;
}) {
this.repoDir = directory;
this.logger = logger;
}
public async list(): Promise<Template[]> {
if (this.repository.length === 0) {
await this.reindex();
}
return this.repository;
}
public async reindex(): Promise<void> {
this.localIndex = await this.index();
this.repository = this.localIndex.map(({ contents }) => contents);
}
public async prepare(templateId: string): Promise<string> {
const template = this.localIndex.find(
({ contents }) => contents.id === templateId,
);
if (!template) {
throw new Error('Template no found');
}
const tempDir = await fs.promises.mkdtemp(templateId);
await fs.copy(template.location, tempDir);
return tempDir;
}
private async index(): Promise<DiskIndexEntry[]> {
const matches = await globby(`${this.repoDir}/**/template-info.json`);
const fileContents: Array<{
location: string;
contents: string;
}> = await Promise.all(
matches.map(async (location: string) => ({
location,
contents: await fs.readFile(location, 'utf-8'),
})),
);
const validFiles: DiskIndexEntry[] = [];
for (const file of fileContents) {
try {
const contents: Template = JSON.parse(file.contents);
validFiles.push({ location: file.location, contents });
} catch (ex) {
this.logger?.error('Failure parsing JSON for template', {
path: file.location,
});
}
}
return validFiles;
}
}
@@ -1,58 +0,0 @@
/*
* 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 { StorageBase, createStorage } from '.';
import * as winston from 'winston';
describe('Storage Interface Test', () => {
const mockStore = new (class MockStorage implements StorageBase {
list = jest.fn();
prepare = jest.fn();
reindex = jest.fn();
public reset = () => {
this.list.mockReset();
this.prepare.mockReset();
this.reindex.mockReset();
};
})();
const logger = winston.createLogger();
afterEach(() => mockStore.reset());
it('should call list of the set repo when calling list', async () => {
const store = createStorage({ store: mockStore, logger });
await store.list();
expect(mockStore.list).toHaveBeenCalled();
});
it('should reindex on the repo when calling reindex', async () => {
const store = createStorage({ store: mockStore, logger });
await store.reindex();
expect(mockStore.reindex).toHaveBeenCalled();
});
it('should call prepare with the correct id when calling prepare', async () => {
const store = createStorage({ store: mockStore, logger });
await store.prepare('testid');
expect(mockStore.prepare).toHaveBeenCalledWith('testid');
});
});
@@ -1,53 +0,0 @@
import { Logger } from 'winston';
/*
* 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 interface Template {
id: string;
name: string;
description: string;
ownerId: string;
}
export abstract class StorageBase {
// lists all templates available
abstract async list(): Promise<Template[]>;
// can be used to build an index of the available templates;
abstract async reindex(): Promise<void>;
// returns a directory to run the templaterin
abstract async prepare(id: string): Promise<string>;
}
export interface StorageConfig {
store?: StorageBase;
logger?: Logger;
}
class Storage implements StorageBase {
store?: StorageBase;
constructor({ store }: StorageConfig) {
this.store = store;
}
list = () => this.store!.list();
prepare = (id: string) => this.store!.prepare(id);
reindex = () => this.store!.reindex();
}
export const createStorage = (storageConfig: StorageConfig): StorageBase => {
return new Storage(storageConfig);
};
@@ -25,7 +25,7 @@ export class CookieCutter implements TemplaterBase {
...options.values,
};
await fs.writeJSON(options.directory, cookieInfo);
await fs.writeJSON(`${options.directory}/cookiecutter.json`, cookieInfo);
return '';
// run cookie cutter with new json
}
@@ -15,7 +15,7 @@
*/
export interface RequiredTemplateValues {
componentId: string;
component_id: string;
}
export interface TemplaterRunOptions {
@@ -17,10 +17,11 @@
import { Logger } from 'winston';
import Router from 'express-promise-router';
import express from 'express';
import { StorageBase, TemplaterBase } from '../scaffolder';
import { PreparerBuilder, TemplaterBase } from '../scaffolder';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
export interface RouterOptions {
storage: StorageBase;
preparers: PreparerBuilder;
templater: TemplaterBase;
logger: Logger;
}
@@ -29,22 +30,47 @@ export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const router = Router();
const { storage, templater, logger: parentLogger } = options;
const { preparers, templater, logger: parentLogger } = options;
const logger = parentLogger.child({ plugin: 'scaffolder' });
router
.get('/v1/templates', async (_, res) => {
const templates = await storage.list();
res.status(200).json(templates);
})
.post('/v1/jobs', async (_, res) => {
// TODO(blam): Actually make this function work
const mock = 'templateid';
res.status(201).json({ accepted: true });
router.post('/v1/jobs', async (_, res) => {
// TODO(blam): Create a unique job here and return the ID so that
// The end user can poll for updates on the current job
res.status(201).json({ accepted: true });
const path = await storage.prepare(mock);
await templater.run({ directory: path, values: { componentId: 'test' } });
});
// TODO(blam): Take this entity from the post body sent from the frontend
const mockEntity: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
'backstage.io/managed-by-location': `file:${__dirname}/../../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: {
type: 'cookiecutter',
path: '.',
},
};
// Get the preparer for the mock entity
const preparer = preparers.get(mockEntity);
// Run the preparer for the mock entity to produce a temporary directory with template in
const path = await preparer.prepare(mockEntity);
// Run the templater on the mock directory with values from the post body
await templater.run({ directory: path, values: { component_id: 'test' } });
console.warn(path);
});
const app = express();
app.set('logger', logger);
@@ -1,57 +0,0 @@
/*
* 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 { Server } from 'http';
import { Logger } from 'winston';
import {
createStorage,
createTemplater,
DiskStorage,
CookieCutter,
} from '../scaffolder';
import { createServiceBuilder, useHotMemoize } from '@backstage/backend-common';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'scaffolder-backend' });
const store = useHotMemoize(module, () => new DiskStorage({ logger }));
const templater = new CookieCutter();
logger.debug('Creating application...');
const router = await createRouter({
storage: createStorage({ store, logger }),
templater: createTemplater({ templater }),
logger,
});
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/catalog', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -1,6 +0,0 @@
{
"id": "mock-template-2",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -1,7 +0,0 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam",
<heres some invalid hson>
}
@@ -1,6 +0,0 @@
{
"id": "mock-template-2",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -1,6 +0,0 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}
@@ -1,6 +0,0 @@
{
"id": "mock-template",
"name": "mockmannen",
"description": "mock template for building stuff in backstage",
"ownerId": "blam"
}