From ea6cc764473c00436959612c03e14c39c72ac4ee Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 16 Jun 2020 14:30:40 +0200 Subject: [PATCH 1/5] feat(catalog): adding kind:Template validation so that we can add templates into the service catalog --- packages/catalog-model/src/EntityPolicies.ts | 2 + .../src/kinds/TemplateEntityV1alpha1.test.ts | 74 +++++++++++++++++++ .../src/kinds/TemplateEntityV1alpha1.ts | 50 +++++++++++++ packages/catalog-model/src/kinds/index.ts | 5 ++ 4 files changed, 131 insertions(+) create mode 100644 packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts create mode 100644 packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index d761aa9f20..3dc7deae7c 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -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(), ]), ]); } diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts new file mode 100644 index 0000000000..d2e455145f --- /dev/null +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts @@ -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/); + }); +}); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts new file mode 100644 index 0000000000..090c892294 --- /dev/null +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts @@ -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; + + constructor() { + this.schema = yup.object>({ + 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 { + return await this.schema.validate(envelope, { strict: true }); + } +} diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 85e22f7029..b5fad184d4 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -24,3 +24,8 @@ export type { LocationEntityV1alpha1 as LocationEntity, LocationEntityV1alpha1, } from './LocationEntityV1alpha1'; +export { TemplateEntityV1alpha1Policy } from './TemplateEntityV1alpha1'; +export type { + TemplateEntityV1alpha1 as TemplateEntity, + TemplateEntityV1alpha1, +} from './TemplateEntityV1alpha1'; From 461571816729919eba2d9d7cdaaf0e75f4ad1e16 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 16 Jun 2020 14:40:43 +0200 Subject: [PATCH 2/5] feat(catalog): added the ability to load mock data into the catalog from the scaffolder too --- docs/getting-started/README.md | 2 +- packages/backend/README.md | 2 +- plugins/catalog-backend/README.md | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/scaffolder-backend/package.json | 3 ++- .../react-ssr-template/template-info.json | 6 ------ .../sample-templates/react-ssr-template/template.yml | 10 ++++++++++ plugins/scaffolder-backend/scripts/mock-data | 8 ++++++++ 8 files changed, 24 insertions(+), 11 deletions(-) delete mode 100644 plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json create mode 100644 plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml create mode 100644 plugins/scaffolder-backend/scripts/mock-data diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 32830c9bb3..5e404d4cb6 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -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 ``` diff --git a/packages/backend/README.md b/packages/backend/README.md index 64078006e7..cd71b50fb0 100644 --- a/packages/backend/README.md +++ b/packages/backend/README.md @@ -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`. diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 2d6b365f90..246cd83d56 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -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. diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9abfb1646a..e8b43de86a 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -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", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index da87c707aa..2408471229 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -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", diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json deleted file mode 100644 index 0a03fd95d6..0000000000 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json +++ /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" -} diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml new file mode 100644 index 0000000000..17f9ebbf7d --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml @@ -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: '.' + diff --git a/plugins/scaffolder-backend/scripts/mock-data b/plugins/scaffolder-backend/scripts/mock-data new file mode 100644 index 0000000000..e457e32b7b --- /dev/null +++ b/plugins/scaffolder-backend/scripts/mock-data @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +curl \ +--location \ +--request POST 'localhost:7000/catalog/locations' \ +--header 'Content-Type: application/json' \ +--data-raw "{\"type\": \"github\", \"target\": \"https://github.com/spotify/backstage/blob/mob/move-templates-into-service-catalog/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml\"}" + From 45ce4b133aa17ebbb80a1b5cf8758ae3fb81e9ac Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 16 Jun 2020 14:47:15 +0200 Subject: [PATCH 3/5] chore(scaffolder): load from file source instead --- .../react-ssr-template/{template.yml => template.yaml} | 0 plugins/scaffolder-backend/scripts/mock-data | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename plugins/scaffolder-backend/sample-templates/react-ssr-template/{template.yml => template.yaml} (100%) mode change 100644 => 100755 plugins/scaffolder-backend/scripts/mock-data diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml similarity index 100% rename from plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml rename to plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml diff --git a/plugins/scaffolder-backend/scripts/mock-data b/plugins/scaffolder-backend/scripts/mock-data old mode 100644 new mode 100755 index e457e32b7b..be3fa2b6cf --- a/plugins/scaffolder-backend/scripts/mock-data +++ b/plugins/scaffolder-backend/scripts/mock-data @@ -4,5 +4,5 @@ curl \ --location \ --request POST 'localhost:7000/catalog/locations' \ --header 'Content-Type: application/json' \ ---data-raw "{\"type\": \"github\", \"target\": \"https://github.com/spotify/backstage/blob/mob/move-templates-into-service-catalog/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yml\"}" +--data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/sample-templates/react-ssr-template/template.yaml\"}" From 1e5e270bb7bfbbb0f7a20ad0e925fc41abb4176f Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 16 Jun 2020 14:48:46 +0200 Subject: [PATCH 4/5] chore(scaffolder): reset the template-info for now so tests will still pass on PR --- .../sample-templates/react-ssr-template/template-info.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json new file mode 100644 index 0000000000..a733b790fa --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json @@ -0,0 +1,6 @@ +{ + "id": "react-ssr-template", + "name": "SSR React Website", + "description": "Next.js application skeleton for creating isomorphic web applications.", + "ownerId": "something" +} From d5f5e83fe75aab48d4533a5e640c0d2f79ea79e9 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 16 Jun 2020 15:03:21 +0200 Subject: [PATCH 5/5] chore(scaffolder): Updating the boilerplate for the scaffolder backend --- .../src/service/standaloneApplication.ts | 55 ------------------- .../src/service/standaloneServer.ts | 27 ++++----- plugins/scaffolder-backend/tsconfig.json | 19 ++++--- 3 files changed, 24 insertions(+), 77 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/service/standaloneApplication.ts diff --git a/plugins/scaffolder-backend/src/service/standaloneApplication.ts b/plugins/scaffolder-backend/src/service/standaloneApplication.ts deleted file mode 100644 index 3ff1de01eb..0000000000 --- a/plugins/scaffolder-backend/src/service/standaloneApplication.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/plugins/scaffolder-backend/src/service/standaloneServer.ts b/plugins/scaffolder-backend/src/service/standaloneServer.ts index b89e806f72..7f553bf4bf 100644 --- a/plugins/scaffolder-backend/src/service/standaloneServer.ts +++ b/plugins/scaffolder-backend/src/service/standaloneServer.ts @@ -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 { 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(); diff --git a/plugins/scaffolder-backend/tsconfig.json b/plugins/scaffolder-backend/tsconfig.json index 7d4ea182e2..9ca38dca27 100644 --- a/plugins/scaffolder-backend/tsconfig.json +++ b/plugins/scaffolder-backend/tsconfig.json @@ -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"] } }