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/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'; 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 index 0a03fd95d6..a733b790fa 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template-info.json @@ -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" } diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml new file mode 100644 index 0000000000..17f9ebbf7d --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -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 100755 index 0000000000..be3fa2b6cf --- /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\": \"file\", \"target\": \"$(pwd)/sample-templates/react-ssr-template/template.yaml\"}" + 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"] } }