From 417cee84722e6d35904e6370a1b17fe3ddb0f799 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 13:53:17 +0200 Subject: [PATCH 01/16] chore: added sample cookiecutter module Signed-off-by: blam --- .../.eslintrc.js | 3 ++ .../README.md | 13 +++++ .../package.json | 41 +++++++++++++++ .../src/index.ts | 17 ++++++ .../src/run.ts | 33 ++++++++++++ .../src/service/router.test.ts | 45 ++++++++++++++++ .../src/service/router.ts | 40 ++++++++++++++ .../src/service/standaloneServer.ts | 52 +++++++++++++++++++ .../src/setupTests.ts | 17 ++++++ 9 files changed, 261 insertions(+) create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/.eslintrc.js create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/README.md create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/package.json create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/src/index.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/src/run.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.test.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/src/service/standaloneServer.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter-backend/src/setupTests.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/.eslintrc.js b/plugins/scaffolder-backend-module-cookiecutter-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/README.md b/plugins/scaffolder-backend-module-cookiecutter-backend/README.md new file mode 100644 index 0000000000..c7a2497cce --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/README.md @@ -0,0 +1,13 @@ +# scaffolder-backend-module-cookiecutter + +Welcome to the scaffolder-backend-module-cookiecutter backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/scaffolder-backend-module-cookiecutter](http://localhost:3000/scaffolder-backend-module-cookiecutter). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/package.json b/plugins/scaffolder-backend-module-cookiecutter-backend/package.json new file mode 100644 index 0000000000..db2f197002 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/package.json @@ -0,0 +1,41 @@ +{ + "name": "plugin-scaffolder-backend-module-cookiecutter", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.8.4", + "@backstage/config": "^0.1.5", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "winston": "^3.2.1", + "cross-fetch": "^3.0.6", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.7.3", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/index.ts b/plugins/scaffolder-backend-module-cookiecutter-backend/src/index.ts new file mode 100644 index 0000000000..ca73cb27ba --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 './service/router'; diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/run.ts b/plugins/scaffolder-backend-module-cookiecutter-backend/src/run.ts new file mode 100644 index 0000000000..54d2716290 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: 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); +}); diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.test.ts b/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.test.ts new file mode 100644 index 0000000000..8b77a04348 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.ts b/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.ts new file mode 100644 index 0000000000..9ceaa47627 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.use(errorHandler()); + return router; +} diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/standaloneServer.ts b/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..d35fb1e5ad --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/standaloneServer.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ + service: 'scaffolder-backend-module-cookiecutter-backend', + }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/scaffolder-backend-module-cookiecutter', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/setupTests.ts b/plugins/scaffolder-backend-module-cookiecutter-backend/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 271e13d7238415229ae715c022bbcb0ca1e86c8e Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 15:46:21 +0200 Subject: [PATCH 02/16] chore: move the action to something better Signed-off-by: blam --- .../.eslintrc.js | 0 .../README.md | 0 .../package.json | 0 .../src/index.ts | 0 .../src/run.ts | 0 .../src/service/router.test.ts | 0 .../src/service/router.ts | 0 .../src/service/standaloneServer.ts | 0 .../src/setupTests.ts | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/.eslintrc.js (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/README.md (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/package.json (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/src/index.ts (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/src/run.ts (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/src/service/router.test.ts (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/src/service/router.ts (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/src/service/standaloneServer.ts (100%) rename plugins/{scaffolder-backend-module-cookiecutter-backend => scaffolder-backend-module-cookiecutter}/src/setupTests.ts (100%) diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/.eslintrc.js b/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/.eslintrc.js rename to plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/README.md rename to plugins/scaffolder-backend-module-cookiecutter/README.md diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/package.json rename to plugins/scaffolder-backend-module-cookiecutter/package.json diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/index.ts b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/src/index.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/index.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/run.ts b/plugins/scaffolder-backend-module-cookiecutter/src/run.ts similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/src/run.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/run.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/service/router.test.ts similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.test.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/service/router.test.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.ts b/plugins/scaffolder-backend-module-cookiecutter/src/service/router.ts similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/src/service/router.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/service/router.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/service/standaloneServer.ts b/plugins/scaffolder-backend-module-cookiecutter/src/service/standaloneServer.ts similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/src/service/standaloneServer.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/service/standaloneServer.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter-backend/src/setupTests.ts b/plugins/scaffolder-backend-module-cookiecutter/src/setupTests.ts similarity index 100% rename from plugins/scaffolder-backend-module-cookiecutter-backend/src/setupTests.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/setupTests.ts From b60091b11768203e6871d2eca2d2e18686440582 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 15:53:15 +0200 Subject: [PATCH 03/16] chore: update some packages Signed-off-by: blam --- plugins/scaffolder-backend-module-cookiecutter/package.json | 1 + plugins/scaffolder-backend-module-rails/package.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index db2f197002..5b8c7d2d95 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.8.4", + "@backstage/plugin-scaffolder-backend": "^0.12.2", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 3062136a76..e805473c61 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -20,8 +20,13 @@ "clean": "backstage-cli clean" }, "dependencies": { +<<<<<<< HEAD "@backstage/backend-common": "^0.8.6", "@backstage/plugin-scaffolder-backend": "^0.14.0", +======= + "@backstage/backend-common": "^0.8.4", + "@backstage/plugin-scaffolder-backend": "^0.12.2", +>>>>>>> chore: update some packages "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.8", From 4e23d2276c269ce4d4e6cd16ed57c58ec0c45357 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 17:11:09 +0200 Subject: [PATCH 04/16] chore: moved out the cookiecutter action to its own repo Signed-off-by: blam --- .../package.json | 16 +- .../src/actions/fetch/cookiecutter.test.ts | 215 ++++++++++++++++ .../src/actions/fetch/cookiecutter.ts | 242 ++++++++++++++++++ .../{setupTests.ts => actions/fetch/index.ts} | 5 +- .../src/{ => actions}/index.ts | 5 +- .../src/run.ts | 33 --- .../src/service/router.test.ts | 45 ---- .../src/service/router.ts | 40 --- .../src/service/standaloneServer.ts | 52 ---- 9 files changed, 471 insertions(+), 182 deletions(-) create mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts rename plugins/scaffolder-backend-module-cookiecutter/src/{setupTests.ts => actions/fetch/index.ts} (84%) rename plugins/scaffolder-backend-module-cookiecutter/src/{ => actions}/index.ts (85%) delete mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/run.ts delete mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/service/router.test.ts delete mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/service/router.ts delete mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/service/standaloneServer.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 5b8c7d2d95..373b22c5c2 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "plugin-scaffolder-backend-module-cookiecutter", - "version": "0.1.1", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,19 +21,23 @@ }, "dependencies": { "@backstage/backend-common": "^0.8.4", + "@backstage/errors": "^0.1.1", + "@backstage/integration": "^0.5.7", "@backstage/plugin-scaffolder-backend": "^0.12.2", "@backstage/config": "^0.1.5", - "@types/express": "^4.17.6", - "express": "^4.17.1", - "express-promise-router": "^4.1.0", + "command-exists": "^1.2.9", + "fs-extra": "^10.0.0", "winston": "^3.2.1", "cross-fetch": "^3.0.6", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.7.3", - "@types/supertest": "^2.0.8", - "supertest": "^4.0.2", + "@types/fs-extra": "^9.0.1", + "@types/mock-fs": "^4.13.0", + "@types/jest": "^26.0.7", + "@types/command-exists": "^1.2.0", + "mock-fs": "^4.13.0", "msw": "^0.29.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts new file mode 100644 index 0000000000..bc3fe3eca2 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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. + */ +const runCommand = jest.fn(); +const commandExists = jest.fn(); +const fetchContents = jest.fn(); + +jest.mock('@backstage/plugin-scaffolder-backend', () => ({ + ...jest.requireActual('@backstage/plugin-scaffolder-backend'), + fetchContents, + runCommand, +})); +jest.mock('command-exists', () => commandExists); + +import { + getVoidLogger, + UrlReader, + ContainerRunner, +} from '@backstage/backend-common'; +import { ConfigReader, JsonObject } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import mockFs from 'mock-fs'; +import os from 'os'; +import { PassThrough } from 'stream'; +import { createFetchCookiecutterAction } from './cookiecutter'; +import { join } from 'path'; +import type { ActionContext } from '@backstage/plugin-scaffolder-backend'; + +describe('fetch:cookiecutter', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }), + ); + + const mockTmpDir = os.tmpdir(); + + let mockContext: ActionContext<{ + url: string; + targetPath?: string; + values: JsonObject; + copyWithoutRender?: string[]; + extensions?: string[]; + imageName?: string; + }>; + + const containerRunner: jest.Mocked = { + runContainer: jest.fn(), + }; + + const mockReader: UrlReader = { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const action = createFetchCookiecutterAction({ + integrations, + containerRunner, + reader: mockReader, + }); + + beforeEach(() => { + jest.resetAllMocks(); + + mockContext = { + input: { + url: 'https://google.com/cookie/cutter', + targetPath: 'something', + values: { + help: 'me', + }, + }, + baseUrl: 'somebase', + workspacePath: mockTmpDir, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + + // mock the temp directory + mockFs({ [mockTmpDir]: {} }); + mockFs({ [`${join(mockTmpDir, 'template')}`]: {} }); + + commandExists.mockResolvedValue(null); + + // Mock when run container is called it creates some new files in the mock filesystem + containerRunner.runContainer.mockImplementation(async () => { + mockFs({ + [`${join(mockTmpDir, 'intermediate')}`]: { + 'testfile.json': '{}', + }, + }); + }); + + // Mock when runCommand is called it creats some new files in the mock filesystem + runCommand.mockImplementation(async () => { + mockFs({ + [`${join(mockTmpDir, 'intermediate')}`]: { + 'testfile.json': '{}', + }, + }); + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should throw an error when copyWithoutRender is not an array', async () => { + (mockContext.input as any).copyWithoutRender = 'not an array'; + + await expect(action.handler(mockContext)).rejects.toThrowError( + /Fetch action input copyWithoutRender must be an Array/, + ); + }); + + it('should throw an error when extensions is not an array', async () => { + (mockContext.input as any).extensions = 'not an array'; + + await expect(action.handler(mockContext)).rejects.toThrowError( + /Fetch action input extensions must be an Array/, + ); + }); + + it('should call fetchContents with the correct variables', async () => { + fetchContents.mockImplementation(() => Promise.resolve()); + await action.handler(mockContext); + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + }); + + it('should call out to cookiecutter using runCommand when cookiecutter is installed', async () => { + commandExists.mockResolvedValue(true); + + await action.handler(mockContext); + + expect(runCommand).toHaveBeenCalledWith( + expect.objectContaining({ + command: 'cookiecutter', + args: [ + '--no-input', + '-o', + join(mockTmpDir, 'intermediate'), + join(mockTmpDir, 'template'), + '--verbose', + ], + logStream: mockContext.logStream, + }), + ); + }); + + it('should call out to the containerRunner when there is no cookiecutter installed', async () => { + commandExists.mockResolvedValue(false); + + await action.handler(mockContext); + + expect(containerRunner.runContainer).toHaveBeenCalledWith( + expect.objectContaining({ + imageName: 'spotify/backstage-cookiecutter', + command: 'cookiecutter', + args: ['--no-input', '-o', '/output', '/input', '--verbose'], + mountDirs: { + [join(mockTmpDir, 'intermediate')]: '/output', + [join(mockTmpDir, 'template')]: '/input', + }, + workingDir: '/input', + envVars: { HOME: '/tmp' }, + logStream: mockContext.logStream, + }), + ); + }); + + it('should use a custom imageName when there is an image supplied to the context', async () => { + const imageName = 'test-image'; + mockContext.input.imageName = imageName; + + await action.handler(mockContext); + + expect(containerRunner.runContainer).toHaveBeenCalledWith( + expect.objectContaining({ + imageName, + }), + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts new file mode 100644 index 0000000000..1f6fbdb7d8 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -0,0 +1,242 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { + ContainerRunner, + UrlReader, + resolveSafeChildPath, +} from '@backstage/backend-common'; +import { JsonObject, JsonValue } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { ScmIntegrations } from '@backstage/integration'; +import commandExists from 'command-exists'; +import fs from 'fs-extra'; +import path, { resolve as resolvePath } from 'path'; +import { Writable } from 'stream'; +import { + runCommand, + createTemplateAction, + fetchContents, +} from '@backstage/plugin-scaffolder-backend'; + +export class CookiecutterRunner { + private readonly containerRunner: ContainerRunner; + + constructor({ containerRunner }: { containerRunner: ContainerRunner }) { + this.containerRunner = containerRunner; + } + + private async fetchTemplateCookieCutter( + directory: string, + ): Promise> { + try { + return await fs.readJSON(path.join(directory, 'cookiecutter.json')); + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + + return {}; + } + } + + public async run({ + workspacePath, + values, + logStream, + }: { + workspacePath: string; + values: JsonObject; + logStream: Writable; + }): Promise { + const templateDir = path.join(workspacePath, 'template'); + const intermediateDir = path.join(workspacePath, 'intermediate'); + await fs.ensureDir(intermediateDir); + const resultDir = path.join(workspacePath, 'result'); + + // First lets grab the default cookiecutter.json file + const cookieCutterJson = await this.fetchTemplateCookieCutter(templateDir); + + const { imageName, ...valuesForCookieCutterJson } = values; + const cookieInfo = { + ...cookieCutterJson, + ...valuesForCookieCutterJson, + }; + + await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); + + // Directories to bind on container + const mountDirs = { + [templateDir]: '/input', + [intermediateDir]: '/output', + }; + + // the command-exists package returns `true` or throws an error + const cookieCutterInstalled = await commandExists('cookiecutter').catch( + () => false, + ); + if (cookieCutterInstalled) { + await runCommand({ + command: 'cookiecutter', + args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], + logStream, + }); + } else { + await this.containerRunner.runContainer({ + imageName: (imageName as string) ?? 'spotify/backstage-cookiecutter', + command: 'cookiecutter', + args: ['--no-input', '-o', '/output', '/input', '--verbose'], + mountDirs, + workingDir: '/input', + // Set the home directory inside the container as something that applications can + // write to, otherwise they will just fail trying to write to / + envVars: { HOME: '/tmp' }, + logStream, + }); + } + + // if cookiecutter was successful, intermediateDir will contain + // exactly one directory. + + const [generated] = await fs.readdir(intermediateDir); + + if (generated === undefined) { + throw new Error('No data generated by cookiecutter'); + } + + await fs.move(path.join(intermediateDir, generated), resultDir); + } +} + +export function createFetchCookiecutterAction(options: { + reader: UrlReader; + integrations: ScmIntegrations; + containerRunner: ContainerRunner; +}) { + const { reader, containerRunner, integrations } = options; + + return createTemplateAction<{ + url: string; + targetPath?: string; + values: JsonObject; + copyWithoutRender?: string[]; + extensions?: string[]; + imageName?: string; + }>({ + id: 'fetch:cookiecutter', + description: + 'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.', + schema: { + input: { + type: 'object', + required: ['url'], + properties: { + url: { + title: 'Fetch URL', + description: + 'Relative path or absolute URL pointing to the directory tree to fetch', + type: 'string', + }, + targetPath: { + title: 'Target Path', + description: + 'Target path within the working directory to download the contents to.', + type: 'string', + }, + values: { + title: 'Template Values', + description: 'Values to pass on to cookiecutter for templating', + type: 'object', + }, + copyWithoutRender: { + title: 'Copy Without Render', + description: + 'Avoid rendering directories and files in the template', + type: 'array', + items: { + type: 'string', + }, + }, + extensions: { + title: 'Template Extensions', + description: + "Jinja2 extensions to add filters, tests, globals or extend the parser. Extensions must be installed in the container or on the host where Cookiecutter executes. See the contrib directory in Backstage's repo for more information", + type: 'array', + items: { + type: 'string', + }, + }, + imageName: { + title: 'Cookiecutter Docker image', + description: + "Specify a custom Docker image to run cookiecutter, to override the default: 'spotify/backstage-cookiecutter'. This can be used to execute cookiecutter with Template Extensions. Used only when a local cookiecutter is not found.", + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info('Fetching and then templating using cookiecutter'); + const workDir = await ctx.createTemporaryDirectory(); + const templateDir = resolvePath(workDir, 'template'); + const templateContentsDir = resolvePath( + templateDir, + "{{cookiecutter and 'contents'}}", + ); + const resultDir = resolvePath(workDir, 'result'); + + if ( + ctx.input.copyWithoutRender && + !Array.isArray(ctx.input.copyWithoutRender) + ) { + throw new InputError( + 'Fetch action input copyWithoutRender must be an Array', + ); + } + if (ctx.input.extensions && !Array.isArray(ctx.input.extensions)) { + throw new InputError('Fetch action input extensions must be an Array'); + } + + await fetchContents({ + reader, + integrations, + baseUrl: ctx.baseUrl, + fetchUrl: ctx.input.url, + outputPath: templateContentsDir, + }); + + const cookiecutter = new CookiecutterRunner({ containerRunner }); + const values = { + ...ctx.input.values, + _copy_without_render: ctx.input.copyWithoutRender, + _extensions: ctx.input.extensions, + imageName: ctx.input.imageName, + }; + + // Will execute the template in ./template and put the result in ./result + await cookiecutter.run({ + workspacePath: workDir, + logStream: ctx.logStream, + values, + }); + + // Finally move the template result into the task workspace + const targetPath = ctx.input.targetPath ?? './'; + const outputPath = resolveSafeChildPath(ctx.workspacePath, targetPath); + await fs.copy(resultDir, outputPath); + }, + }); +} diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/setupTests.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/index.ts similarity index 84% rename from plugins/scaffolder-backend-module-cookiecutter/src/setupTests.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/index.ts index d3232290a7..bbe5a16a04 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/setupTests.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2021 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export {}; +export { createFetchCookiecutterAction } from './cookiecutter'; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts similarity index 85% rename from plugins/scaffolder-backend-module-cookiecutter/src/index.ts rename to plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts index ca73cb27ba..1d70b91a34 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2021 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export * from './service/router'; +export { createFetchCookiecutterAction } from './fetch'; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/run.ts b/plugins/scaffolder-backend-module-cookiecutter/src/run.ts deleted file mode 100644 index 54d2716290..0000000000 --- a/plugins/scaffolder-backend-module-cookiecutter/src/run.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { getRootLogger } from '@backstage/backend-common'; -import yn from 'yn'; -import { startStandaloneServer } from './service/standaloneServer'; - -const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; -const enableCors = yn(process.env.PLUGIN_CORS, { default: 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); -}); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/service/router.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/service/router.test.ts deleted file mode 100644 index 8b77a04348..0000000000 --- a/plugins/scaffolder-backend-module-cookiecutter/src/service/router.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { getVoidLogger } from '@backstage/backend-common'; -import express from 'express'; -import request from 'supertest'; - -import { createRouter } from './router'; - -describe('createRouter', () => { - let app: express.Express; - - beforeAll(async () => { - const router = await createRouter({ - logger: getVoidLogger(), - }); - app = express().use(router); - }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('GET /health', () => { - it('returns ok', async () => { - const response = await request(app).get('/health'); - - expect(response.status).toEqual(200); - expect(response.body).toEqual({ status: 'ok' }); - }); - }); -}); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/service/router.ts b/plugins/scaffolder-backend-module-cookiecutter/src/service/router.ts deleted file mode 100644 index 9ceaa47627..0000000000 --- a/plugins/scaffolder-backend-module-cookiecutter/src/service/router.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 } from '@backstage/backend-common'; -import express from 'express'; -import Router from 'express-promise-router'; -import { Logger } from 'winston'; - -export interface RouterOptions { - logger: Logger; -} - -export async function createRouter( - options: RouterOptions, -): Promise { - const { logger } = options; - - const router = Router(); - router.use(express.json()); - - router.get('/health', (_, response) => { - logger.info('PONG!'); - response.send({ status: 'ok' }); - }); - router.use(errorHandler()); - return router; -} diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/service/standaloneServer.ts b/plugins/scaffolder-backend-module-cookiecutter/src/service/standaloneServer.ts deleted file mode 100644 index d35fb1e5ad..0000000000 --- a/plugins/scaffolder-backend-module-cookiecutter/src/service/standaloneServer.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { createServiceBuilder } from '@backstage/backend-common'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './router'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ - service: 'scaffolder-backend-module-cookiecutter-backend', - }); - logger.debug('Starting application server...'); - const router = await createRouter({ - logger, - }); - - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/scaffolder-backend-module-cookiecutter', router); - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); - } - - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); From df7c9243d334ea6c2498b134b20d11df0d9c17b0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Jul 2021 17:17:12 +0200 Subject: [PATCH 05/16] chore: use the cookiecutter templater library Signed-off-by: blam --- .../package.json | 2 +- .../src/index.ts | 16 ++ plugins/scaffolder-backend/package.json | 1 + .../actions/builtin/createBuiltinActions.ts | 2 +- .../builtin/fetch/cookiecutter.test.ts | 212 ---------------- .../actions/builtin/fetch/cookiecutter.ts | 240 ------------------ .../scaffolder/actions/builtin/fetch/index.ts | 1 - 7 files changed, 19 insertions(+), 455 deletions(-) create mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/index.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 373b22c5c2..9a8afa8324 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,5 +1,5 @@ { - "name": "plugin-scaffolder-backend-module-cookiecutter", + "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts new file mode 100644 index 0000000000..e8de675502 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { createFetchCookiecutterAction } from './actions'; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 8f10c9fa53..e44a22403c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -35,6 +35,7 @@ "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.8", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.0", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 6541625035..c367f0abb7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -25,10 +25,10 @@ import { import { createDebugLogAction } from './debug'; import { - createFetchCookiecutterAction, createFetchPlainAction, createFetchTemplateAction, } from './fetch'; +import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createFilesystemDeleteAction, createFilesystemRenameAction, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts deleted file mode 100644 index 86f953a6a5..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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. - */ -const runCommand = jest.fn(); -const commandExists = jest.fn(); -const fetchContents = jest.fn(); - -jest.mock('./helpers', () => ({ fetchContents })); -jest.mock('command-exists', () => commandExists); -jest.mock('../helpers', () => ({ runCommand })); - -import { - getVoidLogger, - UrlReader, - ContainerRunner, -} from '@backstage/backend-common'; -import { ConfigReader, JsonObject } from '@backstage/config'; -import { ScmIntegrations } from '@backstage/integration'; -import mockFs from 'mock-fs'; -import os from 'os'; -import { PassThrough } from 'stream'; -import { createFetchCookiecutterAction } from './cookiecutter'; -import { join } from 'path'; -import { ActionContext } from '../../types'; - -describe('fetch:cookiecutter', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); - - const mockTmpDir = os.tmpdir(); - - let mockContext: ActionContext<{ - url: string; - targetPath?: string; - values: JsonObject; - copyWithoutRender?: string[]; - extensions?: string[]; - imageName?: string; - }>; - - const containerRunner: jest.Mocked = { - runContainer: jest.fn(), - }; - - const mockReader: UrlReader = { - read: jest.fn(), - readTree: jest.fn(), - search: jest.fn(), - }; - - const action = createFetchCookiecutterAction({ - integrations, - containerRunner, - reader: mockReader, - }); - - beforeEach(() => { - jest.resetAllMocks(); - - mockContext = { - input: { - url: 'https://google.com/cookie/cutter', - targetPath: 'something', - values: { - help: 'me', - }, - }, - baseUrl: 'somebase', - workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; - - // mock the temp directory - mockFs({ [mockTmpDir]: {} }); - mockFs({ [`${join(mockTmpDir, 'template')}`]: {} }); - - commandExists.mockResolvedValue(null); - - // Mock when run container is called it creates some new files in the mock filesystem - containerRunner.runContainer.mockImplementation(async () => { - mockFs({ - [`${join(mockTmpDir, 'intermediate')}`]: { - 'testfile.json': '{}', - }, - }); - }); - - // Mock when runCommand is called it creats some new files in the mock filesystem - runCommand.mockImplementation(async () => { - mockFs({ - [`${join(mockTmpDir, 'intermediate')}`]: { - 'testfile.json': '{}', - }, - }); - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('should throw an error when copyWithoutRender is not an array', async () => { - (mockContext.input as any).copyWithoutRender = 'not an array'; - - await expect(action.handler(mockContext)).rejects.toThrowError( - /Fetch action input copyWithoutRender must be an Array/, - ); - }); - - it('should throw an error when extensions is not an array', async () => { - (mockContext.input as any).extensions = 'not an array'; - - await expect(action.handler(mockContext)).rejects.toThrowError( - /Fetch action input extensions must be an Array/, - ); - }); - - it('should call fetchContents with the correct variables', async () => { - fetchContents.mockImplementation(() => Promise.resolve()); - await action.handler(mockContext); - expect(fetchContents).toHaveBeenCalledWith( - expect.objectContaining({ - reader: mockReader, - integrations, - baseUrl: mockContext.baseUrl, - fetchUrl: mockContext.input.url, - outputPath: join( - mockTmpDir, - 'template', - "{{cookiecutter and 'contents'}}", - ), - }), - ); - }); - - it('should call out to cookiecutter using runCommand when cookiecutter is installed', async () => { - commandExists.mockResolvedValue(true); - - await action.handler(mockContext); - - expect(runCommand).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'cookiecutter', - args: [ - '--no-input', - '-o', - join(mockTmpDir, 'intermediate'), - join(mockTmpDir, 'template'), - '--verbose', - ], - logStream: mockContext.logStream, - }), - ); - }); - - it('should call out to the containerRunner when there is no cookiecutter installed', async () => { - commandExists.mockResolvedValue(false); - - await action.handler(mockContext); - - expect(containerRunner.runContainer).toHaveBeenCalledWith( - expect.objectContaining({ - imageName: 'spotify/backstage-cookiecutter', - command: 'cookiecutter', - args: ['--no-input', '-o', '/output', '/input', '--verbose'], - mountDirs: { - [join(mockTmpDir, 'intermediate')]: '/output', - [join(mockTmpDir, 'template')]: '/input', - }, - workingDir: '/input', - envVars: { HOME: '/tmp' }, - logStream: mockContext.logStream, - }), - ); - }); - - it('should use a custom imageName when there is an image supplied to the context', async () => { - const imageName = 'test-image'; - mockContext.input.imageName = imageName; - - await action.handler(mockContext); - - expect(containerRunner.runContainer).toHaveBeenCalledWith( - expect.objectContaining({ - imageName, - }), - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts deleted file mode 100644 index 048b2bba64..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { - ContainerRunner, - UrlReader, - resolveSafeChildPath, -} from '@backstage/backend-common'; -import { JsonObject, JsonValue } from '@backstage/config'; -import { InputError } from '@backstage/errors'; -import { ScmIntegrations } from '@backstage/integration'; -import commandExists from 'command-exists'; -import fs from 'fs-extra'; -import path, { resolve as resolvePath } from 'path'; -import { Writable } from 'stream'; -import { runCommand } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; -import { fetchContents } from './helpers'; - -export class CookiecutterRunner { - private readonly containerRunner: ContainerRunner; - - constructor({ containerRunner }: { containerRunner: ContainerRunner }) { - this.containerRunner = containerRunner; - } - - private async fetchTemplateCookieCutter( - directory: string, - ): Promise> { - try { - return await fs.readJSON(path.join(directory, 'cookiecutter.json')); - } catch (ex) { - if (ex.code !== 'ENOENT') { - throw ex; - } - - return {}; - } - } - - public async run({ - workspacePath, - values, - logStream, - }: { - workspacePath: string; - values: JsonObject; - logStream: Writable; - }): Promise { - const templateDir = path.join(workspacePath, 'template'); - const intermediateDir = path.join(workspacePath, 'intermediate'); - await fs.ensureDir(intermediateDir); - const resultDir = path.join(workspacePath, 'result'); - - // First lets grab the default cookiecutter.json file - const cookieCutterJson = await this.fetchTemplateCookieCutter(templateDir); - - const { imageName, ...valuesForCookieCutterJson } = values; - const cookieInfo = { - ...cookieCutterJson, - ...valuesForCookieCutterJson, - }; - - await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); - - // Directories to bind on container - const mountDirs = { - [templateDir]: '/input', - [intermediateDir]: '/output', - }; - - // the command-exists package returns `true` or throws an error - const cookieCutterInstalled = await commandExists('cookiecutter').catch( - () => false, - ); - if (cookieCutterInstalled) { - await runCommand({ - command: 'cookiecutter', - args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], - logStream, - }); - } else { - await this.containerRunner.runContainer({ - imageName: (imageName as string) ?? 'spotify/backstage-cookiecutter', - command: 'cookiecutter', - args: ['--no-input', '-o', '/output', '/input', '--verbose'], - mountDirs, - workingDir: '/input', - // Set the home directory inside the container as something that applications can - // write to, otherwise they will just fail trying to write to / - envVars: { HOME: '/tmp' }, - logStream, - }); - } - - // if cookiecutter was successful, intermediateDir will contain - // exactly one directory. - - const [generated] = await fs.readdir(intermediateDir); - - if (generated === undefined) { - throw new Error('No data generated by cookiecutter'); - } - - await fs.move(path.join(intermediateDir, generated), resultDir); - } -} - -export function createFetchCookiecutterAction(options: { - reader: UrlReader; - integrations: ScmIntegrations; - containerRunner: ContainerRunner; -}) { - const { reader, containerRunner, integrations } = options; - - return createTemplateAction<{ - url: string; - targetPath?: string; - values: JsonObject; - copyWithoutRender?: string[]; - extensions?: string[]; - imageName?: string; - }>({ - id: 'fetch:cookiecutter', - description: - "Downloads a template from the given URL into the workspace, and runs cookiecutter on it. This action is deprecated in favor of 'fetch:template'. See https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetch-cookiecutter-to-fetch-template for more details.", - schema: { - input: { - type: 'object', - required: ['url'], - properties: { - url: { - title: 'Fetch URL', - description: - 'Relative path or absolute URL pointing to the directory tree to fetch', - type: 'string', - }, - targetPath: { - title: 'Target Path', - description: - 'Target path within the working directory to download the contents to.', - type: 'string', - }, - values: { - title: 'Template Values', - description: 'Values to pass on to cookiecutter for templating', - type: 'object', - }, - copyWithoutRender: { - title: 'Copy Without Render', - description: - 'Avoid rendering directories and files in the template', - type: 'array', - items: { - type: 'string', - }, - }, - extensions: { - title: 'Template Extensions', - description: - "Jinja2 extensions to add filters, tests, globals or extend the parser. Extensions must be installed in the container or on the host where Cookiecutter executes. See the contrib directory in Backstage's repo for more information", - type: 'array', - items: { - type: 'string', - }, - }, - imageName: { - title: 'Cookiecutter Docker image', - description: - "Specify a custom Docker image to run cookiecutter, to override the default: 'spotify/backstage-cookiecutter'. This can be used to execute cookiecutter with Template Extensions. Used only when a local cookiecutter is not found.", - type: 'string', - }, - }, - }, - }, - async handler(ctx) { - ctx.logger.info('Fetching and then templating using cookiecutter'); - const workDir = await ctx.createTemporaryDirectory(); - const templateDir = resolvePath(workDir, 'template'); - const templateContentsDir = resolvePath( - templateDir, - "{{cookiecutter and 'contents'}}", - ); - const resultDir = resolvePath(workDir, 'result'); - - if ( - ctx.input.copyWithoutRender && - !Array.isArray(ctx.input.copyWithoutRender) - ) { - throw new InputError( - 'Fetch action input copyWithoutRender must be an Array', - ); - } - if (ctx.input.extensions && !Array.isArray(ctx.input.extensions)) { - throw new InputError('Fetch action input extensions must be an Array'); - } - - await fetchContents({ - reader, - integrations, - baseUrl: ctx.baseUrl, - fetchUrl: ctx.input.url, - outputPath: templateContentsDir, - }); - - const cookiecutter = new CookiecutterRunner({ containerRunner }); - const values = { - ...ctx.input.values, - _copy_without_render: ctx.input.copyWithoutRender, - _extensions: ctx.input.extensions, - imageName: ctx.input.imageName, - }; - - // Will execute the template in ./template and put the result in ./result - await cookiecutter.run({ - workspacePath: workDir, - logStream: ctx.logStream, - values, - }); - - // Finally move the template result into the task workspace - const targetPath = ctx.input.targetPath ?? './'; - const outputPath = resolveSafeChildPath(ctx.workspacePath, targetPath); - await fs.copy(resultDir, outputPath); - }, - }); -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts index fea32df39c..c9a16c274e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts @@ -15,6 +15,5 @@ */ export { createFetchPlainAction } from './plain'; -export { createFetchCookiecutterAction } from './cookiecutter'; export { createFetchTemplateAction } from './template'; export { fetchContents } from './helpers'; From 7cea90592fb8cf6e11fb728cb98b67dc39a3bf4a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Jul 2021 02:55:42 +0200 Subject: [PATCH 06/16] chore: add changeset Signed-off-by: blam --- .changeset/spotty-pandas-deny.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/spotty-pandas-deny.md diff --git a/.changeset/spotty-pandas-deny.md b/.changeset/spotty-pandas-deny.md new file mode 100644 index 0000000000..dbd4c97c2c --- /dev/null +++ b/.changeset/spotty-pandas-deny.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +--- + +- Align versions for the rails plugin +- Move out the cookiecutter templating to it's own module that is depended on by the `scaffolder-backend` no breaking change yet, but we will drop first class support for `cookiecutter` in the future and it will become an opt-in feature. From 4a087f05ea2925de85d97700f2bf830308c6aa5d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Jul 2021 03:10:52 +0200 Subject: [PATCH 07/16] chore: fix readme Signed-off-by: blam --- plugins/scaffolder-backend-module-cookiecutter/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md index c7a2497cce..a76d8076e4 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/README.md +++ b/plugins/scaffolder-backend-module-cookiecutter/README.md @@ -6,8 +6,4 @@ _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/scaffolder-backend-module-cookiecutter](http://localhost:3000/scaffolder-backend-module-cookiecutter). - -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. + From 4cdc9d1d0b3edf65b09058e03f57f964852a1d34 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Jul 2021 03:34:21 +0200 Subject: [PATCH 08/16] chore: added api for cookiecutter Signed-off-by: blam --- .../api-report.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 plugins/scaffolder-backend-module-cookiecutter/api-report.md diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md new file mode 100644 index 0000000000..fe2f23c1fd --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-scaffolder-backend-module-cookiecutter" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ContainerRunner } from '@backstage/backend-common'; +import { ScmIntegrations } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export function createFetchCookiecutterAction(options: { + reader: UrlReader; + integrations: ScmIntegrations; + containerRunner: ContainerRunner; +}): TemplateAction; + + +// (No @packageDocumentation comment for this package) + +``` From 8b93d985c00ab0f8298583d6830a967127199aa3 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 12:16:56 +0200 Subject: [PATCH 09/16] chore: fixing merge b0rked Signed-off-by: blam --- plugins/scaffolder-backend-module-cookiecutter/package.json | 5 ++--- plugins/scaffolder-backend-module-rails/package.json | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 9a8afa8324..71f7ae0e40 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -4,7 +4,6 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -20,10 +19,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.4", + "@backstage/backend-common": "^0.8.6", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.7", - "@backstage/plugin-scaffolder-backend": "^0.12.2", + "@backstage/plugin-scaffolder-backend": "^0.14.0", "@backstage/config": "^0.1.5", "command-exists": "^1.2.9", "fs-extra": "^10.0.0", diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index e805473c61..3062136a76 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -20,13 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { -<<<<<<< HEAD "@backstage/backend-common": "^0.8.6", "@backstage/plugin-scaffolder-backend": "^0.14.0", -======= - "@backstage/backend-common": "^0.8.4", - "@backstage/plugin-scaffolder-backend": "^0.12.2", ->>>>>>> chore: update some packages "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.8", From a0149f0228797cf621c1f8f2ada3e571f8767c89 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 12:21:53 +0200 Subject: [PATCH 10/16] chore: code review comments Signed-off-by: blam --- .../scaffolder-backend-module-cookiecutter/src/actions/index.ts | 2 +- plugins/scaffolder-backend-module-cookiecutter/src/index.ts | 2 +- .../scaffolder-backend/src/scaffolder/actions/builtin/index.ts | 2 ++ yarn.lock | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts index 1d70b91a34..1b47db9e03 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { createFetchCookiecutterAction } from './fetch'; +export * from './fetch'; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts index e8de675502..4f06b14a86 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { createFetchCookiecutterAction } from './actions'; +export * from './actions'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 849b056ac9..2a583f369a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -20,4 +20,6 @@ export * from './debug'; export * from './fetch'; export * from './filesystem'; export * from './publish'; + +export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; export { runCommand } from './helpers'; diff --git a/yarn.lock b/yarn.lock index bc34978977..766d71e7d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13177,7 +13177,7 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@10.0.0: +fs-extra@10.0.0, fs-extra@^10.0.0: version "10.0.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== From 94c00f9ba7b5ea856eaaec42fca41bb73d395660 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 12:26:48 +0200 Subject: [PATCH 11/16] api-report: update the api reports for the plugins Signed-off-by: blam --- .../api-report.md | 11 ++++++----- plugins/scaffolder-backend/api-report.md | 10 ++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index fe2f23c1fd..8e46667fce 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -3,20 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// import { ContainerRunner } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; import { UrlReader } from '@backstage/backend-common'; +// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export function createFetchCookiecutterAction(options: { - reader: UrlReader; - integrations: ScmIntegrations; - containerRunner: ContainerRunner; + reader: UrlReader; + integrations: ScmIntegrations; + containerRunner: ContainerRunner; }): TemplateAction; - // (No @packageDocumentation comment for this package) - ``` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index f29274cc24..0e7a25534c 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -8,6 +8,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { ContainerRunner } from '@backstage/backend-common'; +import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import express from 'express'; import { JsonObject } from '@backstage/config'; @@ -78,14 +79,7 @@ export function createCatalogWriteAction(): TemplateAction; // @public export function createDebugLogAction(): TemplateAction; -// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function createFetchCookiecutterAction(options: { - reader: UrlReader; - integrations: ScmIntegrations; - containerRunner: ContainerRunner; -}): TemplateAction; +export { createFetchCookiecutterAction }; // Warning: (ae-missing-release-tag) "createFetchPlainAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From 2bffb188596ab5e76dc19b8a6408696aeac65b50 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 12:34:55 +0200 Subject: [PATCH 12/16] chore: run prettier on ugly document Signed-off-by: blam --- .../src/scaffolder/actions/builtin/createBuiltinActions.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index c367f0abb7..ae5a7d0697 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -24,10 +24,7 @@ import { } from './catalog'; import { createDebugLogAction } from './debug'; -import { - createFetchPlainAction, - createFetchTemplateAction, -} from './fetch'; +import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createFilesystemDeleteAction, From 6efd28c44165fae1cd33f54e741719332269c9c1 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 12:51:19 +0200 Subject: [PATCH 13/16] chore: fix changeset and update some deps to avoid bringing in multiple Signed-off-by: blam --- .changeset/spotty-pandas-deny.md | 4 +--- plugins/scaffolder-backend-module-cookiecutter/package.json | 2 +- yarn.lock | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.changeset/spotty-pandas-deny.md b/.changeset/spotty-pandas-deny.md index dbd4c97c2c..51c90515e5 100644 --- a/.changeset/spotty-pandas-deny.md +++ b/.changeset/spotty-pandas-deny.md @@ -1,7 +1,5 @@ --- '@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch --- -- Align versions for the rails plugin -- Move out the cookiecutter templating to it's own module that is depended on by the `scaffolder-backend` no breaking change yet, but we will drop first class support for `cookiecutter` in the future and it will become an opt-in feature. +- Move out the `cookiecutter` templating to it's own module that is depended on by the `scaffolder-backend` no breaking change yet, but we will drop first class support for `cookiecutter` in the future and it will become an opt-in feature. diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 71f7ae0e40..f321d6b45c 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -25,7 +25,7 @@ "@backstage/plugin-scaffolder-backend": "^0.14.0", "@backstage/config": "^0.1.5", "command-exists": "^1.2.9", - "fs-extra": "^10.0.0", + "fs-extra": "10.0.0", "winston": "^3.2.1", "cross-fetch": "^3.0.6", "yn": "^4.0.0" diff --git a/yarn.lock b/yarn.lock index 766d71e7d5..bc34978977 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13177,7 +13177,7 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@10.0.0, fs-extra@^10.0.0: +fs-extra@10.0.0: version "10.0.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== From a7f0e1994a88375b01d84e63b2ce2098c14d0761 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 13:09:25 +0200 Subject: [PATCH 14/16] docs: document the cookiecutter actions Signed-off-by: blam --- .../README.md | 160 +++++++++++++++++- 1 file changed, 156 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md index a76d8076e4..b97b1bbb05 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/README.md +++ b/plugins/scaffolder-backend-module-cookiecutter/README.md @@ -1,9 +1,161 @@ # scaffolder-backend-module-cookiecutter -Welcome to the scaffolder-backend-module-cookiecutter backend plugin! - -_This plugin was created through the Backstage CLI_ +Welcome to the `fetch:cookiecutter` action for the `scaffolder-backend`. ## Getting started - +You need to configure the action in your backend: + +## From your Backstage root directory + +``` +cd packages/backend +yarn add @backstage/plugin-scaffolder-backend-module-cookiecutter +``` + +Configure the action (you can check +the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to +see all options): + +```typescript +const actions = [ + createFetchCookiecutterAction({ + integrations, + reader, + containerRunner, + }), + ...createBuiltInActions({ + ... + }) +]; + +return await createRouter({ + containerRunner, + logger, + config, + database, + catalogClient, + reader, + actions, +}); +``` + +After that you can use the action in your template: + +```yaml +apiVersion: backstage.io/v1beta2 +kind: Template +metadata: + name: cookiecutter-demo + title: Cookiecutter Test + description: Cookiecutter example +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Fill in some steps + required: + - name + - owner + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + allowedKinds: + - Group + system: + title: System + type: string + description: System of the component + ui:field: EntityPicker + ui:options: + allowedKinds: + - System + defaultKind: System + + - title: Choose a location + required: + - repoUrl + - dryRun + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + dryRun: + title: Only perform a dry run, don't publish anything + type: boolean + default: false + + steps: + - id: fetch-base + name: Fetch Base + action: fetch:cookiecutter + input: + url: ./template + values: + name: '{{ parameters.name }}' + owner: '{{ parameters.owner }}' + system: '{{ parameters.system }}' + destination: '{{ parseRepoUrl parameters.repoUrl }}' + + - id: publish + if: '{{ not parameters.dryRun }}' + name: Publish + action: publish:github + input: + allowedHosts: ['github.com'] + description: 'This is {{ parameters.name }}' + repoUrl: '{{ parameters.repoUrl }}' + + - id: register + if: '{{ not parameters.dryRun }}' + name: Register + action: catalog:register + input: + repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + catalogInfoPath: '/catalog-info.yaml' + + - name: Results + if: '{{ parameters.dryRun }}' + action: debug:log + input: + listWorkspace: true + + output: + links: + - title: Repository + url: '{{ steps.publish.output.remoteUrl }}' + - title: Open in catalog + icon: 'catalog' + entityRef: '{{ steps.register.output.entityRef }}' +``` + +You can also visit the `/create/actions` route in your Backstage application to find out more about the parameters this action accepts when it's installed to configure how you like. + +### Environment setup + +The environment needs to have either `cookiecutter` installed and be available in the `PATH` or access to a `docker` daemon so it can spin up a docker container with `cookiecutter` available. + +If you are running Backstage from a Docker container and you want to avoid calling a container inside a container, you can set up `cookiecutter` in your own image, this will use the local installation instead. + +You can do so by including the following lines in the last step of your Dockerfile: + +```dockerfile +RUN apt-get update && apt-get install -y python3 python3-pip +RUN pip3 install cookiecutter +``` From 4af11b2cf585670c5c62195cfad2e159f802a6dc Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 13:11:39 +0200 Subject: [PATCH 15/16] chore: updating the actions documentation again Signed-off-by: blam --- plugins/scaffolder-backend-module-cookiecutter/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md index b97b1bbb05..77ae93c5f3 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/README.md +++ b/plugins/scaffolder-backend-module-cookiecutter/README.md @@ -13,11 +13,12 @@ cd packages/backend yarn add @backstage/plugin-scaffolder-backend-module-cookiecutter ``` -Configure the action (you can check -the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to -see all options): +Configure the action: +(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options): ```typescript +// packages/backend/src/plugins/scaffolder.ts + const actions = [ createFetchCookiecutterAction({ integrations, From 0a7e3d243c33073327bec7062165e1411aea0f11 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jul 2021 13:25:50 +0200 Subject: [PATCH 16/16] chore: fix changeset Signed-off-by: blam --- .changeset/spotty-pandas-deny.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spotty-pandas-deny.md b/.changeset/spotty-pandas-deny.md index 51c90515e5..c3da6afe2b 100644 --- a/.changeset/spotty-pandas-deny.md +++ b/.changeset/spotty-pandas-deny.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': patch --- -- Move out the `cookiecutter` templating to it's own module that is depended on by the `scaffolder-backend` no breaking change yet, but we will drop first class support for `cookiecutter` in the future and it will become an opt-in feature. +- Move out the `cookiecutter` templating to its own module that is depended on by the `scaffolder-backend` plugin. No breaking change yet, but we will drop first class support for `cookiecutter` in the future and it will become an opt-in feature.