Merge pull request #1316 from spotify/mob/move-templates-into-service-catalog

Add support for the Template kind in the Catalog
This commit is contained in:
Ben Lambert
2020-06-16 16:58:09 +02:00
committed by GitHub
15 changed files with 183 additions and 86 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ to look at, and then launch the frontend. These commands are run from the
project root, not inside the backend directory.
```bash
yarn lerna run mock-catalog-data
yarn lerna run mock-data
yarn start
```
+1 -1
View File
@@ -43,7 +43,7 @@ To get started, you can issue the following after starting the backend, from ins
the `plugins/catalog-backend` directory:
```bash
yarn mock-catalog-data
yarn mock-data
```
You should then start seeing data on `localhost:7000/catalog/entities`.
@@ -25,6 +25,7 @@ import {
import {
ComponentEntityV1alpha1Policy,
LocationEntityV1alpha1Policy,
TemplateEntityV1alpha1Policy,
} from './kinds';
import { EntityPolicy } from './types';
@@ -74,6 +75,7 @@ export class EntityPolicies implements EntityPolicy {
EntityPolicies.anyOf([
new ComponentEntityV1alpha1Policy(),
new LocationEntityV1alpha1Policy(),
new TemplateEntityV1alpha1Policy(),
]),
]);
}
@@ -0,0 +1,74 @@
/*
* 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 { EntityPolicy } from '../types';
import {
TemplateEntityV1alpha1,
TemplateEntityV1alpha1Policy,
} from './TemplateEntityV1alpha1';
describe('TemplateEntityV1alpah1', () => {
let entity: TemplateEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
name: 'test',
},
spec: {
type: 'cookiecutter',
},
};
policy = new TemplateEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
});
it('rejects unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('acceptps any other type', async () => {
(entity as any).spec.type = 'hallo';
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
});
@@ -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 * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Template' as const;
export interface TemplateEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
spec: {
type: string;
};
}
export class TemplateEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<TemplateEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity> {
return await this.schema.validate(envelope, { strict: true });
}
}
@@ -24,3 +24,8 @@ export type {
LocationEntityV1alpha1 as LocationEntity,
LocationEntityV1alpha1,
} from './LocationEntityV1alpha1';
export { TemplateEntityV1alpha1Policy } from './TemplateEntityV1alpha1';
export type {
TemplateEntityV1alpha1 as TemplateEntity,
TemplateEntityV1alpha1,
} from './TemplateEntityV1alpha1';
+1 -1
View File
@@ -23,7 +23,7 @@ cd packages/backend
yarn start
# open another terminal window, and run the following from the very root of the Backstage project
yarn lerna run mock-catalog-data
yarn lerna run mock-data
```
This will launch the full example backend and populate its catalog with some mock entities.
+1 -1
View File
@@ -13,7 +13,7 @@
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean",
"mock-catalog-data": "./scripts/mock-data"
"mock-data": "./scripts/mock-data"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.8",
+2 -1
View File
@@ -11,7 +11,8 @@
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
"clean": "backstage-cli clean",
"mock-data": "./scripts/mock-data"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.8",
@@ -1,6 +1,6 @@
{
"id": "react-ssr-template",
"name": "SSR React Website",
"description": "Next.js application skeleton for creating isomorphic web applications.",
"ownerId": "something"
"id": "react-ssr-template",
"name": "SSR React Website",
"description": "Next.js application skeleton for creating isomorphic web applications.",
"ownerId": "something"
}
@@ -0,0 +1,10 @@
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: '.'
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
curl \
--location \
--request POST 'localhost:7000/catalog/locations' \
--header 'Content-Type: application/json' \
--data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/sample-templates/react-ssr-template/template.yaml\"}"
@@ -1,55 +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 {
errorHandler,
notFoundHandler,
requestLoggingHandler,
} from '@backstage/backend-common';
import compression from 'compression';
import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { Logger } from 'winston';
import { StorageBase, TemplaterBase } from '../scaffolder';
import { createRouter } from './router';
export interface ApplicationOptions {
enableCors: boolean;
storage: StorageBase;
templater: TemplaterBase;
logger: Logger;
}
export async function createStandaloneApplication(
options: ApplicationOptions,
): Promise<express.Application> {
const { enableCors, storage, templater, logger } = options;
const app = express();
app.use(helmet());
if (enableCors) {
app.use(cors());
}
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
app.use('/', await createRouter({ templater, storage, logger }));
app.use(notFoundHandler());
app.use(errorHandler());
return app;
}
@@ -22,8 +22,8 @@ import {
DiskStorage,
CookieCutter,
} from '../scaffolder';
import { createStandaloneApplication } from './standaloneApplication';
import { createServiceBuilder, useHotMemoize } from '@backstage/backend-common';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
@@ -34,27 +34,24 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'scaffolder-backend' });
const store = new DiskStorage({ logger });
const store = useHotMemoize(module, () => new DiskStorage({ logger }));
const templater = new CookieCutter();
logger.debug('Creating application...');
const app = await createStandaloneApplication({
enableCors: options.enableCors,
const router = await createRouter({
storage: createStorage({ store, logger }),
templater: createTemplater({ templater }),
logger,
});
logger.debug('Starting application server...');
return await new Promise((resolve, reject) => {
const server = app.listen(options.port, (err?: Error) => {
if (err) {
reject(err);
return;
}
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/catalog', router);
logger.info(`Listening on port ${options.port}`);
resolve(server);
});
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
+12 -7
View File
@@ -1,11 +1,16 @@
{
"extends": "../../packages/backend/tsconfig.json",
"include": [
"./src"
],
"include": ["src"],
"compilerOptions": {
"baseUrl": "./src",
"outDir": "./dist",
"skipLibCheck": true
"outDir": "dist",
"incremental": true,
"sourceMap": true,
"declaration": true,
"strict": true,
"target": "es2019",
"module": "commonjs",
"esModuleInterop": true,
"allowJs": true,
"lib": ["es2019"],
"types": ["node", "jest", "webpack-env"]
}
}